diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorageProviderFactory.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorageProviderFactory.java index 67e7359fb7..b886b5c803 100644 --- a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorageProviderFactory.java +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/HotRodMapStorageProviderFactory.java @@ -26,6 +26,7 @@ import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.model.Scope; import org.keycloak.common.Profile; import org.keycloak.component.AmphibianProviderFactory; +import org.keycloak.models.ActionTokenValueModel; import org.keycloak.events.Event; import org.keycloak.events.admin.AdminEvent; import org.keycloak.models.AuthenticatedClientSessionModel; @@ -66,6 +67,7 @@ import org.keycloak.models.map.realm.entity.MapRequiredActionProviderEntity; import org.keycloak.models.map.realm.entity.MapRequiredCredentialEntity; import org.keycloak.models.map.realm.entity.MapWebAuthnPolicyEntity; import org.keycloak.models.map.role.MapRoleEntity; +import org.keycloak.models.map.singleUseObject.MapSingleUseObjectEntity; import org.keycloak.models.map.storage.hotRod.authSession.HotRodAuthenticationSessionEntityDelegate; import org.keycloak.models.map.storage.hotRod.authSession.HotRodRootAuthenticationSessionEntity; import org.keycloak.models.map.storage.hotRod.authSession.HotRodRootAuthenticationSessionEntityDelegate; @@ -116,6 +118,8 @@ import org.keycloak.models.map.storage.hotRod.realm.entity.HotRodOTPPolicyEntity import org.keycloak.models.map.storage.hotRod.realm.entity.HotRodRequiredActionProviderEntityDelegate; import org.keycloak.models.map.storage.hotRod.realm.entity.HotRodRequiredCredentialEntityDelegate; import org.keycloak.models.map.storage.hotRod.realm.entity.HotRodWebAuthnPolicyEntityDelegate; +import org.keycloak.models.map.storage.hotRod.singleUseObject.HotRodSingleUseObjectEntity; +import org.keycloak.models.map.storage.hotRod.singleUseObject.HotRodSingleUseObjectEntityDelegate; import org.keycloak.models.map.storage.hotRod.user.HotRodUserConsentEntityDelegate; import org.keycloak.models.map.storage.hotRod.user.HotRodUserCredentialEntityDelegate; import org.keycloak.models.map.storage.hotRod.user.HotRodUserEntity; @@ -159,6 +163,8 @@ public class HotRodMapStorageProviderFactory implements AmphibianProviderFactory .constructor(MapRoleEntity.class, HotRodRoleEntityDelegate::new) + .constructor(MapSingleUseObjectEntity.class, HotRodSingleUseObjectEntityDelegate::new) + .constructor(MapUserEntity.class, HotRodUserEntityDelegate::new) .constructor(MapUserCredentialEntity.class, HotRodUserCredentialEntityDelegate::new) .constructor(MapUserFederatedIdentityEntity.class, HotRodUserFederatedIdentityEntityDelegate::new) @@ -242,6 +248,12 @@ public class HotRodMapStorageProviderFactory implements AmphibianProviderFactory HotRodRealmEntity.class, HotRodRealmEntityDelegate::new)); + // single-use object storage descriptor + ENTITY_DESCRIPTOR_MAP.put(ActionTokenValueModel.class, + new HotRodEntityDescriptor<>(ActionTokenValueModel.class, + HotRodSingleUseObjectEntity.class, + HotRodSingleUseObjectEntityDelegate::new)); + // User sessions descriptor ENTITY_DESCRIPTOR_MAP.put(UserSessionModel.class, new HotRodEntityDescriptor<>(UserSessionModel.class, @@ -339,6 +351,8 @@ public class HotRodMapStorageProviderFactory implements AmphibianProviderFactory HotRodMapStorage clientSessionStore = getHotRodStorage(session, AuthenticatedClientSessionModel.class); return new HotRodUserSessionMapStorage(clientSessionStore, connectionProvider.getRemoteCache(entityDescriptor.getCacheName()), StringKeyConverter.StringKey.INSTANCE, entityDescriptor, CLONER); + } else if (modelType == ActionTokenValueModel.class) { + return new SingleUseObjectHotRodMapStorage(connectionProvider.getRemoteCache(entityDescriptor.getCacheName()), StringKeyConverter.StringKey.INSTANCE, entityDescriptor, CLONER); } return new HotRodMapStorage<>(connectionProvider.getRemoteCache(entityDescriptor.getCacheName()), StringKeyConverter.StringKey.INSTANCE, entityDescriptor, CLONER); } diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/SingleUseObjectHotRodMapStorage.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/SingleUseObjectHotRodMapStorage.java new file mode 100644 index 0000000000..6dba4a1eca --- /dev/null +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/SingleUseObjectHotRodMapStorage.java @@ -0,0 +1,109 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.models.map.storage.hotRod; + +import org.infinispan.client.hotrod.RemoteCache; +import org.keycloak.models.ActionTokenValueModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.map.common.AbstractEntity; +import org.keycloak.models.map.common.DeepCloner; +import org.keycloak.models.map.common.StringKeyConverter; +import org.keycloak.models.map.storage.chm.MapModelCriteriaBuilder; +import org.keycloak.models.map.storage.chm.SingleUseObjectKeycloakTransaction; +import org.keycloak.models.map.storage.MapKeycloakTransaction; +import org.keycloak.models.map.storage.QueryParameters; +import org.keycloak.models.map.storage.chm.MapFieldPredicates; +import org.keycloak.models.map.storage.chm.SingleUseObjectModelCriteriaBuilder; +import org.keycloak.models.map.storage.criteria.DefaultModelCriteria; +import org.keycloak.models.map.storage.hotRod.common.AbstractHotRodEntity; +import org.keycloak.models.map.storage.hotRod.common.HotRodEntityDelegate; +import org.keycloak.models.map.storage.hotRod.common.HotRodEntityDescriptor; +import org.keycloak.models.map.storage.hotRod.singleUseObject.HotRodSingleUseObjectEntity; +import org.keycloak.models.map.storage.hotRod.singleUseObject.HotRodSingleUseObjectEntityDelegate; +import org.keycloak.storage.SearchableModelField; + +import java.util.Map; +import java.util.stream.Stream; + + +/** + * @author Martin Kanis + */ +public class SingleUseObjectHotRodMapStorage & AbstractEntity, M> + extends HotRodMapStorage { + + private final StringKeyConverter keyConverter; + private final HotRodEntityDescriptor storedEntityDescriptor; + private final DeepCloner cloner; + + public SingleUseObjectHotRodMapStorage(RemoteCache remoteCache, StringKeyConverter keyConverter, + HotRodEntityDescriptor storedEntityDescriptor, + DeepCloner cloner) { + super(remoteCache, keyConverter, storedEntityDescriptor, cloner); + this.keyConverter = keyConverter; + this.storedEntityDescriptor = storedEntityDescriptor; + this.cloner = cloner; + } + + @Override + public MapKeycloakTransaction createTransaction(KeycloakSession session) { + MapKeycloakTransaction transaction = session.getAttribute("map-transaction-" + hashCode(), MapKeycloakTransaction.class); + + if (transaction == null) { + Map, MapModelCriteriaBuilder.UpdatePredicatesFunc> fieldPredicates = + MapFieldPredicates.getPredicates((Class) storedEntityDescriptor.getModelTypeClass()); + transaction = new SingleUseObjectKeycloakTransaction(this, keyConverter, cloner, fieldPredicates); + session.setAttribute("map-transaction-" + hashCode(), transaction); + } + + return transaction; + } + + @Override + public HotRodSingleUseObjectEntityDelegate create(HotRodSingleUseObjectEntityDelegate value) { + if (value.getId() == null) { + if (value.getUserId() != null && value.getActionId() != null && value.getActionVerificationNonce() != null) { + value.setId(value.getUserId() + ":" + value.getActionId() + ":" + value.getActionVerificationNonce()); + } + } + return super.create(value); + } + + @Override + public Stream read(QueryParameters queryParameters) { + DefaultModelCriteria criteria = queryParameters.getModelCriteriaBuilder(); + + if (criteria == null) { + return Stream.empty(); + } + + SingleUseObjectModelCriteriaBuilder mcb = criteria.flashToModelCriteriaBuilder(createSingleUseObjectCriteriaBuilder()); + if (mcb.isValid()) { + HotRodSingleUseObjectEntityDelegate value = read(mcb.getKey()); + + return value != null && value.getHotRodEntity() != null ? Stream.of(value) : Stream.empty(); + } + + return super.read(queryParameters); + } + + private SingleUseObjectModelCriteriaBuilder createSingleUseObjectCriteriaBuilder() { + return new SingleUseObjectModelCriteriaBuilder(); + } + +} diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/ProtoSchemaInitializer.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/ProtoSchemaInitializer.java index 7da80cf3a0..abe8c90931 100644 --- a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/ProtoSchemaInitializer.java +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/common/ProtoSchemaInitializer.java @@ -54,6 +54,7 @@ import org.keycloak.models.map.storage.hotRod.realm.entity.HotRodRequiredCredent import org.keycloak.models.map.storage.hotRod.realm.entity.HotRodRequirement; import org.keycloak.models.map.storage.hotRod.realm.entity.HotRodWebAuthnPolicyEntity; import org.keycloak.models.map.storage.hotRod.role.HotRodRoleEntity; +import org.keycloak.models.map.storage.hotRod.singleUseObject.HotRodSingleUseObjectEntity; import org.keycloak.models.map.storage.hotRod.user.HotRodUserConsentEntity; import org.keycloak.models.map.storage.hotRod.user.HotRodUserCredentialEntity; import org.keycloak.models.map.storage.hotRod.user.HotRodUserEntity; @@ -110,6 +111,9 @@ import org.keycloak.models.map.storage.hotRod.userSession.HotRodUserSessionEntit HotRodWebAuthnPolicyEntity.class, HotRodRealmEntity.class, + // single-use objects + HotRodSingleUseObjectEntity.class, + // User sessions HotRodUserSessionEntity.class, HotRodSessionState.class, diff --git a/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/singleUseObject/HotRodSingleUseObjectEntity.java b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/singleUseObject/HotRodSingleUseObjectEntity.java new file mode 100644 index 0000000000..1b2c1938fc --- /dev/null +++ b/model/map-hot-rod/src/main/java/org/keycloak/models/map/storage/hotRod/singleUseObject/HotRodSingleUseObjectEntity.java @@ -0,0 +1,85 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.models.map.storage.hotRod.singleUseObject; + +import org.infinispan.protostream.annotations.ProtoDoc; +import org.infinispan.protostream.annotations.ProtoField; +import org.keycloak.models.map.annotations.GenerateHotRodEntityImplementation; +import org.keycloak.models.map.singleUseObject.MapSingleUseObjectEntity; +import org.keycloak.models.map.storage.hotRod.common.AbstractHotRodEntity; +import org.keycloak.models.map.storage.hotRod.common.HotRodPair; +import org.keycloak.models.map.storage.hotRod.common.UpdatableHotRodEntityDelegateImpl; + +import java.util.Set; + +@GenerateHotRodEntityImplementation( + implementInterface = "org.keycloak.models.map.singleUseObject.MapSingleUseObjectEntity", + inherits = "org.keycloak.models.map.storage.hotRod.singleUseObject.HotRodSingleUseObjectEntity.AbstractHotRodSingleUseObjectEntityDelegate" +) +@ProtoDoc("@Indexed") +public class HotRodSingleUseObjectEntity extends AbstractHotRodEntity { + + @ProtoDoc("@Field(index = Index.YES, store = Store.YES)") + @ProtoField(number = 1) + public Integer entityVersion = 1; + + @ProtoField(number = 2) + public String id; + + @ProtoField(number = 3) + public String userId; + + @ProtoField(number = 4) + public String actionId; + + @ProtoField(number = 5) + public String actionVerificationNonce; + + @ProtoField(number = 6) + public Long expiration; + + @ProtoField(number = 7) + public Set> notes; + + public static abstract class AbstractHotRodSingleUseObjectEntityDelegate extends UpdatableHotRodEntityDelegateImpl implements MapSingleUseObjectEntity { + + @Override + public String getId() { + HotRodSingleUseObjectEntity hotRodEntity = getHotRodEntity(); + return hotRodEntity != null ? hotRodEntity.id : null; + } + + @Override + public void setId(String id) { + HotRodSingleUseObjectEntity entity = getHotRodEntity(); + if (entity.id != null) throw new IllegalStateException("Id cannot be changed"); + entity.id = id; + entity.updated |= id != null; + } + } + + @Override + public boolean equals(Object o) { + return HotRodSingleUseObjectEntityDelegate.entityEquals(this, o); + } + + @Override + public int hashCode() { + return HotRodSingleUseObjectEntityDelegate.entityHashCode(this); + } +} diff --git a/model/map-hot-rod/src/main/resources/config/cacheConfig.xml b/model/map-hot-rod/src/main/resources/config/cacheConfig.xml index b77ea94787..51807208ba 100644 --- a/model/map-hot-rod/src/main/resources/config/cacheConfig.xml +++ b/model/map-hot-rod/src/main/resources/config/cacheConfig.xml @@ -116,5 +116,13 @@ + + + + kc.HotRodSingleUseObjectEntity + + + + diff --git a/model/map-hot-rod/src/main/resources/config/infinispan.xml b/model/map-hot-rod/src/main/resources/config/infinispan.xml index 02c48c787a..ab351a748e 100644 --- a/model/map-hot-rod/src/main/resources/config/infinispan.xml +++ b/model/map-hot-rod/src/main/resources/config/infinispan.xml @@ -118,5 +118,13 @@ + + + + kc.HotRodSingleUseObjectEntity + + + + diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/chm/SingleUseObjectConcurrentHashMapStorage.java b/model/map/src/main/java/org/keycloak/models/map/storage/chm/SingleUseObjectConcurrentHashMapStorage.java index b218b700eb..275d37fcc7 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/chm/SingleUseObjectConcurrentHashMapStorage.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/chm/SingleUseObjectConcurrentHashMapStorage.java @@ -26,9 +26,7 @@ import org.keycloak.models.map.singleUseObject.MapSingleUseObjectEntity; import org.keycloak.models.map.storage.MapKeycloakTransaction; import org.keycloak.models.map.storage.QueryParameters; import org.keycloak.models.map.storage.criteria.DefaultModelCriteria; -import org.keycloak.storage.SearchableModelField; -import java.util.Map; import java.util.stream.Stream; /** @@ -44,7 +42,13 @@ public class SingleUseObjectConcurrentHashMapStorage createTransaction(KeycloakSession session) { MapKeycloakTransaction actionTokenTransaction = session.getAttribute("map-transaction-" + hashCode(), MapKeycloakTransaction.class); - return actionTokenTransaction == null ? new SingleUseObjectConcurrentHashMapStorage.Transaction(getKeyConverter(), cloner, fieldPredicates) : actionTokenTransaction; + + if (actionTokenTransaction == null) { + actionTokenTransaction = new SingleUseObjectKeycloakTransaction(this, keyConverter, cloner, fieldPredicates); + session.setAttribute("map-transaction-" + hashCode(), actionTokenTransaction); + } + + return actionTokenTransaction; } @Override @@ -78,21 +82,4 @@ public class SingleUseObjectConcurrentHashMapStorage { - - public Transaction(StringKeyConverter keyConverter, DeepCloner cloner, - Map, MapModelCriteriaBuilder.UpdatePredicatesFunc> fieldPredicates) { - super(SingleUseObjectConcurrentHashMapStorage.this, keyConverter, cloner, fieldPredicates); - } - - @Override - public MapSingleUseObjectEntity create(MapSingleUseObjectEntity value) { - if (value.getId() == null) { - if (value.getUserId() != null && value.getActionId() != null && value.getActionVerificationNonce() != null) { - value.setId(value.getUserId() + ":" + value.getActionId() + ":" + value.getActionVerificationNonce()); - } - } - return super.create(value); - } - } } diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/chm/SingleUseObjectKeycloakTransaction.java b/model/map/src/main/java/org/keycloak/models/map/storage/chm/SingleUseObjectKeycloakTransaction.java new file mode 100644 index 0000000000..c811a1a760 --- /dev/null +++ b/model/map/src/main/java/org/keycloak/models/map/storage/chm/SingleUseObjectKeycloakTransaction.java @@ -0,0 +1,50 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.models.map.storage.chm; + +import org.keycloak.models.ActionTokenValueModel; +import org.keycloak.models.map.common.DeepCloner; +import org.keycloak.models.map.common.StringKeyConverter; +import org.keycloak.models.map.singleUseObject.MapSingleUseObjectEntity; +import org.keycloak.storage.SearchableModelField; + +import java.util.Map; + +/** + * @author Martin Kanis + */ +public class SingleUseObjectKeycloakTransaction extends ConcurrentHashMapKeycloakTransaction { + + public SingleUseObjectKeycloakTransaction(ConcurrentHashMapCrudOperations map, + StringKeyConverter keyConverter, + DeepCloner cloner, + Map, + MapModelCriteriaBuilder.UpdatePredicatesFunc> fieldPredicates) { + super(map, keyConverter, cloner, fieldPredicates); + } + + @Override + public MapSingleUseObjectEntity create(MapSingleUseObjectEntity value) { + if (value.getId() == null) { + if (value.getUserId() != null && value.getActionId() != null && value.getActionVerificationNonce() != null) { + value.setId(value.getUserId() + ":" + value.getActionId() + ":" + value.getActionVerificationNonce()); + } + } + return super.create(value); + } +} diff --git a/testsuite/integration-arquillian/tests/pom.xml b/testsuite/integration-arquillian/tests/pom.xml index 88ffc42f4d..f6c6417688 100755 --- a/testsuite/integration-arquillian/tests/pom.xml +++ b/testsuite/integration-arquillian/tests/pom.xml @@ -1507,6 +1507,8 @@ hotrod hotrod hotrod + hotrod + hotrod diff --git a/testsuite/model/src/main/resources/hotrod/infinispan.xml b/testsuite/model/src/main/resources/hotrod/infinispan.xml index 39203384d2..966f772a4d 100644 --- a/testsuite/model/src/main/resources/hotrod/infinispan.xml +++ b/testsuite/model/src/main/resources/hotrod/infinispan.xml @@ -114,5 +114,13 @@ + + + + kc.HotRodSingleUseObjectEntity + + + + diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/HotRodMapStorage.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/HotRodMapStorage.java index fbf6b9fac3..044d83de33 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/HotRodMapStorage.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/HotRodMapStorage.java @@ -76,8 +76,8 @@ public class HotRodMapStorage extends KeycloakModelParameters { @Override public void updateConfig(Config cf) { cf.spi(AuthenticationSessionSpi.PROVIDER_ID).provider(MapRootAuthenticationSessionProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, HotRodMapStorageProviderFactory.PROVIDER_ID) - .spi(ActionTokenStoreSpi.NAME).provider(MapSingleUseObjectProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID) - .spi(SingleUseObjectSpi.NAME).provider(MapSingleUseObjectProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID) + .spi(ActionTokenStoreSpi.NAME).provider(MapSingleUseObjectProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, HotRodMapStorageProviderFactory.PROVIDER_ID) + .spi(SingleUseObjectSpi.NAME).provider(MapSingleUseObjectProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, HotRodMapStorageProviderFactory.PROVIDER_ID) .spi("client").provider(MapClientProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, HotRodMapStorageProviderFactory.PROVIDER_ID) .spi("clientScope").provider(MapClientScopeProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, HotRodMapStorageProviderFactory.PROVIDER_ID) .spi("group").provider(MapGroupProviderFactory.PROVIDER_ID).config(STORAGE_CONFIG, HotRodMapStorageProviderFactory.PROVIDER_ID)