From 007fa1f37435b06b56e5d0ce6229f4ed6b554dfa Mon Sep 17 00:00:00 2001 From: Stefan Guilhen Date: Thu, 2 Jun 2022 15:36:56 -0300 Subject: [PATCH] Single Use Objects Map JPA implementation Closes #9852 --- .../HotRodSingleUseObjectEntity.java | 11 +- .../models/map/storage/jpa/Constants.java | 1 + .../jpa/JpaMapStorageProviderFactory.java | 8 +- .../hibernate/jsonb/JpaEntityMigration.java | 4 + .../JpaSingleUseObjectMigration.java | 35 +++ ...SingleUseObjectMapKeycloakTransaction.java | 63 +++++ ...paSingleUseObjectModelCriteriaBuilder.java | 81 +++++++ .../entity/JpaSingleUseObjectEntity.java | 227 ++++++++++++++++++ .../entity/JpaSingleUseObjectMetadata.java | 48 ++++ .../entity/JpaSingleUseObjectNoteEntity.java | 54 +++++ .../META-INF/jpa-aggregate-changelog.xml | 1 + .../jpa-single-use-objects-changelog.xml | 23 ++ .../main/resources/META-INF/persistence.xml | 3 + .../jpa-single-use-objects-changelog-1.xml | 69 ++++++ .../MapSingleUseObjectEntity.java | 3 + .../MapSingleUseObjectProvider.java | 15 +- .../map/storage/chm/MapFieldPredicates.java | 1 + .../models/ActionTokenValueModel.java | 1 + .../integration-arquillian/tests/base/pom.xml | 1 + .../model/parameters/JpaMapStorage.java | 2 +- 20 files changed, 639 insertions(+), 12 deletions(-) create mode 100644 model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/migration/JpaSingleUseObjectMigration.java create mode 100644 model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/singleUseObject/JpaSingleUseObjectMapKeycloakTransaction.java create mode 100644 model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/singleUseObject/JpaSingleUseObjectModelCriteriaBuilder.java create mode 100644 model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/singleUseObject/entity/JpaSingleUseObjectEntity.java create mode 100644 model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/singleUseObject/entity/JpaSingleUseObjectMetadata.java create mode 100644 model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/singleUseObject/entity/JpaSingleUseObjectNoteEntity.java create mode 100644 model/map-jpa/src/main/resources/META-INF/jpa-single-use-objects-changelog.xml create mode 100644 model/map-jpa/src/main/resources/META-INF/single-use-objects/jpa-single-use-objects-changelog-1.xml 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 index 6fe51283a5..13d64a99ad 100644 --- 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 @@ -64,18 +64,21 @@ public class HotRodSingleUseObjectEntity extends AbstractHotRodEntity { public String id; @ProtoField(number = 3) - public String userId; + public String objectKey; @ProtoField(number = 4) - public String actionId; + public String userId; @ProtoField(number = 5) - public String actionVerificationNonce; + public String actionId; @ProtoField(number = 6) - public Long expiration; + public String actionVerificationNonce; @ProtoField(number = 7) + public Long expiration; + + @ProtoField(number = 8) public Set> notes; public static abstract class AbstractHotRodSingleUseObjectEntityDelegate extends UpdatableHotRodEntityDelegateImpl implements MapSingleUseObjectEntity { diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/Constants.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/Constants.java index 0d2e4f6875..dceb5e853b 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/Constants.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/Constants.java @@ -32,5 +32,6 @@ public interface Constants { public static final Integer CURRENT_SCHEMA_VERSION_REALM = 1; public static final Integer CURRENT_SCHEMA_VERSION_ROLE = 1; public static final Integer CURRENT_SCHEMA_VERSION_ROOT_AUTH_SESSION = 1; + public static final Integer CURRENT_SCHEMA_VERSION_SINGLE_USE_OBJECT = 1; public static final Integer CURRENT_SCHEMA_VERSION_USER_LOGIN_FAILURE = 1; } diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaMapStorageProviderFactory.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaMapStorageProviderFactory.java index 1ef7593c0a..374c3f7faf 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaMapStorageProviderFactory.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaMapStorageProviderFactory.java @@ -58,6 +58,7 @@ import org.keycloak.common.util.StringPropertyReplacer; import org.keycloak.component.AmphibianProviderFactory; import org.keycloak.events.Event; import org.keycloak.events.admin.AdminEvent; +import org.keycloak.models.ActionTokenValueModel; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientScopeModel; import org.keycloak.models.GroupModel; @@ -126,6 +127,8 @@ import org.keycloak.models.map.storage.jpa.realm.entity.JpaComponentEntity; import org.keycloak.models.map.storage.jpa.realm.entity.JpaRealmEntity; import org.keycloak.models.map.storage.jpa.role.JpaRoleMapKeycloakTransaction; import org.keycloak.models.map.storage.jpa.role.entity.JpaRoleEntity; +import org.keycloak.models.map.storage.jpa.singleUseObject.JpaSingleUseObjectMapKeycloakTransaction; +import org.keycloak.models.map.storage.jpa.singleUseObject.entity.JpaSingleUseObjectEntity; import org.keycloak.models.map.storage.jpa.updater.MapJpaUpdaterProvider; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.provider.EnvironmentDependentProviderFactory; @@ -184,7 +187,9 @@ public class JpaMapStorageProviderFactory implements .constructor(MapWebAuthnPolicyEntity.class, MapWebAuthnPolicyEntityImpl::new) //role .constructor(JpaRoleEntity.class, JpaRoleEntity::new) - //user login-failure + //single-use-object + .constructor(JpaSingleUseObjectEntity.class, JpaSingleUseObjectEntity::new) + //user-login-failure .constructor(JpaUserLoginFailureEntity.class, JpaUserLoginFailureEntity::new) .build(); @@ -199,6 +204,7 @@ public class JpaMapStorageProviderFactory implements MODEL_TO_TX.put(GroupModel.class, JpaGroupMapKeycloakTransaction::new); MODEL_TO_TX.put(RealmModel.class, JpaRealmMapKeycloakTransaction::new); MODEL_TO_TX.put(RoleModel.class, JpaRoleMapKeycloakTransaction::new); + MODEL_TO_TX.put(ActionTokenValueModel.class, JpaSingleUseObjectMapKeycloakTransaction::new); MODEL_TO_TX.put(UserLoginFailureModel.class, JpaUserLoginFailureMapKeycloakTransaction::new); //authz diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/JpaEntityMigration.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/JpaEntityMigration.java index 5b1cc2e992..1eada01e1e 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/JpaEntityMigration.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/JpaEntityMigration.java @@ -50,11 +50,13 @@ import org.keycloak.models.map.storage.jpa.hibernate.jsonb.migration.JpaResource import org.keycloak.models.map.storage.jpa.hibernate.jsonb.migration.JpaRoleMigration; import org.keycloak.models.map.storage.jpa.hibernate.jsonb.migration.JpaRootAuthenticationSessionMigration; import org.keycloak.models.map.storage.jpa.hibernate.jsonb.migration.JpaScopeMigration; +import org.keycloak.models.map.storage.jpa.hibernate.jsonb.migration.JpaSingleUseObjectMigration; import org.keycloak.models.map.storage.jpa.hibernate.jsonb.migration.JpaUserLoginFailureMigration; import org.keycloak.models.map.storage.jpa.loginFailure.entity.JpaUserLoginFailureMetadata; import org.keycloak.models.map.storage.jpa.realm.entity.JpaComponentMetadata; import org.keycloak.models.map.storage.jpa.realm.entity.JpaRealmMetadata; import org.keycloak.models.map.storage.jpa.role.entity.JpaRoleMetadata; +import org.keycloak.models.map.storage.jpa.singleUseObject.entity.JpaSingleUseObjectMetadata; import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_ADMIN_EVENT; import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_AUTHZ_PERMISSION; @@ -69,6 +71,7 @@ import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSI import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_GROUP; import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_REALM; import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_ROLE; +import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_SINGLE_USE_OBJECT; import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_USER_LOGIN_FAILURE; public class JpaEntityMigration { @@ -83,6 +86,7 @@ public class JpaEntityMigration { MIGRATIONS.put(JpaGroupMetadata.class, (tree, entityVersion) -> migrateTreeTo(entityVersion, CURRENT_SCHEMA_VERSION_GROUP, tree, JpaGroupMigration.MIGRATORS)); MIGRATIONS.put(JpaRealmMetadata.class, (tree, entityVersion) -> migrateTreeTo(entityVersion, CURRENT_SCHEMA_VERSION_REALM, tree, JpaRealmMigration.MIGRATORS)); MIGRATIONS.put(JpaRoleMetadata.class, (tree, entityVersion) -> migrateTreeTo(entityVersion, CURRENT_SCHEMA_VERSION_ROLE, tree, JpaRoleMigration.MIGRATORS)); + MIGRATIONS.put(JpaSingleUseObjectMetadata.class, (tree, entityVersion) -> migrateTreeTo(entityVersion, CURRENT_SCHEMA_VERSION_SINGLE_USE_OBJECT, tree, JpaSingleUseObjectMigration.MIGRATORS)); MIGRATIONS.put(JpaUserLoginFailureMetadata.class, (tree, entityVersion) -> migrateTreeTo(entityVersion, CURRENT_SCHEMA_VERSION_USER_LOGIN_FAILURE,tree, JpaUserLoginFailureMigration.MIGRATORS)); //authz diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/migration/JpaSingleUseObjectMigration.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/migration/JpaSingleUseObjectMigration.java new file mode 100644 index 0000000000..1afe80a3c3 --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/migration/JpaSingleUseObjectMigration.java @@ -0,0 +1,35 @@ +/* + * 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.jpa.hibernate.jsonb.migration; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; + +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * Migration functions for single-use objects. + * + * @author Stefan Guilhen + */ +public class JpaSingleUseObjectMigration { + + public static final List> MIGRATORS = Arrays.asList( + o -> o // no migration yet + ); +} \ No newline at end of file diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/singleUseObject/JpaSingleUseObjectMapKeycloakTransaction.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/singleUseObject/JpaSingleUseObjectMapKeycloakTransaction.java new file mode 100644 index 0000000000..d47bfbda2b --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/singleUseObject/JpaSingleUseObjectMapKeycloakTransaction.java @@ -0,0 +1,63 @@ +/* + * 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.jpa.singleUseObject; + +import javax.persistence.EntityManager; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.Root; +import javax.persistence.criteria.Selection; + +import org.keycloak.models.ActionTokenValueModel; +import org.keycloak.models.map.singleUseObject.MapSingleUseObjectEntity; +import org.keycloak.models.map.storage.jpa.JpaMapKeycloakTransaction; +import org.keycloak.models.map.storage.jpa.JpaModelCriteriaBuilder; +import org.keycloak.models.map.storage.jpa.JpaRootEntity; +import org.keycloak.models.map.storage.jpa.singleUseObject.entity.JpaSingleUseObjectEntity; + +import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_SINGLE_USE_OBJECT; + +/** + * A {@link org.keycloak.models.map.storage.MapKeycloakTransaction} implementation for single-use object entities. + * + * @author Stefan Guilhen + */ +public class JpaSingleUseObjectMapKeycloakTransaction extends JpaMapKeycloakTransaction { + + public JpaSingleUseObjectMapKeycloakTransaction(final EntityManager em) { + super(JpaSingleUseObjectEntity.class, ActionTokenValueModel.class, em); + } + + @Override + protected Selection selectCbConstruct(CriteriaBuilder cb, Root root) { + return root; + } + + @Override + protected void setEntityVersion(JpaRootEntity entity) { + entity.setEntityVersion(CURRENT_SCHEMA_VERSION_SINGLE_USE_OBJECT); + } + + @Override + protected JpaModelCriteriaBuilder createJpaModelCriteriaBuilder() { + return new JpaSingleUseObjectModelCriteriaBuilder(); + } + + @Override + protected MapSingleUseObjectEntity mapToEntityDelegate(JpaSingleUseObjectEntity original) { + return original; + } +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/singleUseObject/JpaSingleUseObjectModelCriteriaBuilder.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/singleUseObject/JpaSingleUseObjectModelCriteriaBuilder.java new file mode 100644 index 0000000000..9145d6326b --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/singleUseObject/JpaSingleUseObjectModelCriteriaBuilder.java @@ -0,0 +1,81 @@ +/* + * 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.jpa.singleUseObject; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiFunction; + +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; + +import org.keycloak.models.ActionTokenValueModel; +import org.keycloak.models.map.storage.CriterionNotSupportedException; +import org.keycloak.models.map.storage.jpa.JpaModelCriteriaBuilder; +import org.keycloak.models.map.storage.jpa.singleUseObject.entity.JpaSingleUseObjectEntity; +import org.keycloak.storage.SearchableModelField; + +/** + * A {@link JpaModelCriteriaBuilder} implementation for single-use objects. + * + * @author Stefan Guilhen + */ +public class JpaSingleUseObjectModelCriteriaBuilder extends JpaModelCriteriaBuilder { + + private static final Map FIELD_TO_JSON_PROP = new HashMap<>(); + static { + FIELD_TO_JSON_PROP.put(ActionTokenValueModel.SearchableFields.USER_ID.getName(), "fUserId"); + FIELD_TO_JSON_PROP.put(ActionTokenValueModel.SearchableFields.ACTION_ID.getName(), "fActionId"); + FIELD_TO_JSON_PROP.put(ActionTokenValueModel.SearchableFields.ACTION_VERIFICATION_NONCE.getName(), "fActionVerificationNonce"); + } + + public JpaSingleUseObjectModelCriteriaBuilder() { + super(JpaSingleUseObjectModelCriteriaBuilder::new); + } + + public JpaSingleUseObjectModelCriteriaBuilder(BiFunction, Predicate> predicateFunc) { + super(JpaSingleUseObjectModelCriteriaBuilder::new, predicateFunc); + } + + @Override + public JpaSingleUseObjectModelCriteriaBuilder compare(SearchableModelField modelField, Operator op, Object... value) { + switch (op) { + case EQ: + if (modelField == ActionTokenValueModel.SearchableFields.USER_ID || + modelField == ActionTokenValueModel.SearchableFields.ACTION_ID || + modelField == ActionTokenValueModel.SearchableFields.ACTION_VERIFICATION_NONCE) { + + validateValue(value, modelField, op, String.class); + + return new JpaSingleUseObjectModelCriteriaBuilder((cb, root) -> + cb.equal(cb.function("->>", String.class, root.get("metadata"), + cb.literal(FIELD_TO_JSON_PROP.get(modelField.getName()))), value[0]) + ); + } else if(modelField == ActionTokenValueModel.SearchableFields.OBJECT_KEY) { + validateValue(value, modelField, op, String.class); + return new JpaSingleUseObjectModelCriteriaBuilder((cb, root) -> + cb.equal(root.get(modelField.getName()), value[0]) + ); + } else { + throw new CriterionNotSupportedException(modelField, op); + } + default: + throw new CriterionNotSupportedException(modelField, op); + } + } +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/singleUseObject/entity/JpaSingleUseObjectEntity.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/singleUseObject/entity/JpaSingleUseObjectEntity.java new file mode 100644 index 0000000000..a92bcf1fc3 --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/singleUseObject/entity/JpaSingleUseObjectEntity.java @@ -0,0 +1,227 @@ +/* + * 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.jpa.singleUseObject.entity; + +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +import javax.persistence.Basic; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.OneToMany; +import javax.persistence.Table; +import javax.persistence.Version; + +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; +import org.hibernate.annotations.TypeDefs; +import org.keycloak.models.map.common.DeepCloner; +import org.keycloak.models.map.common.UuidValidator; +import org.keycloak.models.map.singleUseObject.MapSingleUseObjectEntity; +import org.keycloak.models.map.storage.jpa.JpaRootVersionedEntity; +import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType; + +import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_SINGLE_USE_OBJECT; + +/** + * JPA {@link MapSingleUseObjectEntity} implementation. Some fields are annotated with {@code @Column(insertable = false, updatable = false)} + * to indicate that they are automatically generated from json fields. As such, these fields are non-insertable and non-updatable. + * + * @author Stefan Guilhen + */ +@Entity +@Table(name = "kc_single_use_obj") +@TypeDefs({@TypeDef(name = "jsonb", typeClass = JsonbType.class)}) +public class JpaSingleUseObjectEntity extends MapSingleUseObjectEntity.AbstractSingleUseObjectEntity implements JpaRootVersionedEntity { + + @Id + @Column + private UUID id; + + //used for implicit optimistic locking + @Version + @Column + private int version; + + @Type(type = "jsonb") + @Column(columnDefinition = "jsonb") + private final JpaSingleUseObjectMetadata metadata; + + @Column(insertable = false, updatable = false) + @Basic(fetch = FetchType.LAZY) + private Integer entityVersion; + + @Column(insertable = false, updatable = false) + @Basic(fetch = FetchType.LAZY) + private String objectKey; + + @Column(insertable = false, updatable = false) + @Basic(fetch = FetchType.LAZY) + private Long expiration; + + @OneToMany(mappedBy = "root", cascade = CascadeType.PERSIST, orphanRemoval = true) + private final Set notes = new HashSet<>(); + + /** + * No-argument constructor, used by hibernate to instantiate entities. + */ + public JpaSingleUseObjectEntity() { + this.metadata = new JpaSingleUseObjectMetadata(); + } + + public JpaSingleUseObjectEntity(final DeepCloner cloner) { + this.metadata = new JpaSingleUseObjectMetadata(cloner); + } + + public boolean isMetadataInitialized() { + return this.metadata != null; + } + + @Override + public int getVersion() { + return this.version; + } + + @Override + public Integer getEntityVersion() { + if (this.isMetadataInitialized()) return this.metadata.getEntityVersion(); + return this.entityVersion; + } + + @Override + public void setEntityVersion(Integer entityVersion) { + this.metadata.setEntityVersion(entityVersion); + } + + @Override + public String getId() { + return id == null ? null : id.toString(); + } + + @Override + public void setId(String id) { + String validatedId = UuidValidator.validateAndConvert(id); + this.id = UUID.fromString(validatedId); + } + + @Override + public String getObjectKey() { + if (this.isMetadataInitialized()) return this.metadata.getObjectKey(); + return this.objectKey; + } + + @Override + public void setObjectKey(final String objectKey) { + this.metadata.setObjectKey(objectKey); + } + + @Override + public Integer getCurrentSchemaVersion() { + return CURRENT_SCHEMA_VERSION_SINGLE_USE_OBJECT; + } + + @Override + public String getActionId() { + return this.metadata.getActionId(); + } + + @Override + public void setActionId(String actionId) { + this.metadata.setActionId(actionId); + } + + @Override + public String getActionVerificationNonce() { + return this.metadata.getActionVerificationNonce(); + } + + @Override + public void setActionVerificationNonce(String actionVerificationNonce) { + this.metadata.setActionVerificationNonce(actionVerificationNonce); + } + + @Override + public Map getNotes() { + return this.notes.stream() + .collect(Collectors.toMap(JpaSingleUseObjectNoteEntity::getName, JpaSingleUseObjectNoteEntity::getValue)); + } + + @Override + public String getNote(String name) { + return this.notes.stream().filter(note -> Objects.equals(note.getName(), name)) + .findFirst() + .map(JpaSingleUseObjectNoteEntity::getValue) + .orElse(null); + } + + @Override + public void setNotes(Map notes) { + this.notes.clear(); + if (notes != null) { + notes.forEach(this::setNote); + } + } + + @Override + public void setNote(String name, String value) { + if (name != null) { + this.notes.removeIf(note -> Objects.equals(note.getName(), name)); + if (value != null && !value.trim().isEmpty()) + this.notes.add(new JpaSingleUseObjectNoteEntity(this, name, value)); + } + } + + @Override + public String getUserId() { + return this.metadata.getUserId(); + } + + @Override + public void setUserId(String userId) { + this.metadata.setUserId(userId); + } + + @Override + public Long getExpiration() { + if (this.isMetadataInitialized()) return this.metadata.getExpiration(); + return this.expiration; + } + + @Override + public void setExpiration(Long expiration) { + this.metadata.setExpiration(expiration); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof JpaSingleUseObjectEntity)) return false; + return Objects.equals(getId(), ((JpaSingleUseObjectEntity) obj).getId()); + } +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/singleUseObject/entity/JpaSingleUseObjectMetadata.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/singleUseObject/entity/JpaSingleUseObjectMetadata.java new file mode 100644 index 0000000000..ef15405ef3 --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/singleUseObject/entity/JpaSingleUseObjectMetadata.java @@ -0,0 +1,48 @@ +/* + * 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.jpa.singleUseObject.entity; + +import java.io.Serializable; + +import org.keycloak.models.map.common.DeepCloner; +import org.keycloak.models.map.singleUseObject.MapSingleUseObjectEntityImpl; + +/** + * Class that contains all the single-use object metadata that is written as JSON into the database. + * + * @author Stefan Guilhen + */ +public class JpaSingleUseObjectMetadata extends MapSingleUseObjectEntityImpl implements Serializable { + + public JpaSingleUseObjectMetadata() { + super(); + } + + public JpaSingleUseObjectMetadata(final DeepCloner cloner) { + super(cloner); + } + + private Integer entityVersion; + + public Integer getEntityVersion() { + return entityVersion; + } + + public void setEntityVersion(Integer entityVersion) { + this.entityVersion = entityVersion; + } +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/singleUseObject/entity/JpaSingleUseObjectNoteEntity.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/singleUseObject/entity/JpaSingleUseObjectNoteEntity.java new file mode 100644 index 0000000000..54dfc35764 --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/singleUseObject/entity/JpaSingleUseObjectNoteEntity.java @@ -0,0 +1,54 @@ +/* + * 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.jpa.singleUseObject.entity; + +import java.util.Objects; + +import javax.persistence.Entity; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; + +import org.keycloak.models.map.storage.jpa.JpaAttributeEntity; + +/** + * JPA implementation for single-use object notes. This entity represents a note and has a many-to-one relationship + * with the single-use object entity. + * + * @author Stefan Guilhen + */ +@Entity +@Table(name = "kc_single_use_obj_note", uniqueConstraints = { + @UniqueConstraint(columnNames = {"fk_root", "name"}) +}) +public class JpaSingleUseObjectNoteEntity extends JpaAttributeEntity { + + public JpaSingleUseObjectNoteEntity() { + } + + public JpaSingleUseObjectNoteEntity(final JpaSingleUseObjectEntity root, final String name, final String value) { + super(root, name, value); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof JpaSingleUseObjectNoteEntity)) return false; + JpaSingleUseObjectNoteEntity that = (JpaSingleUseObjectNoteEntity) obj; + return Objects.equals(getParent(), that.getParent()) && + Objects.equals(getName(), that.getName()); + } +} diff --git a/model/map-jpa/src/main/resources/META-INF/jpa-aggregate-changelog.xml b/model/map-jpa/src/main/resources/META-INF/jpa-aggregate-changelog.xml index 94763879ec..df681dc39b 100644 --- a/model/map-jpa/src/main/resources/META-INF/jpa-aggregate-changelog.xml +++ b/model/map-jpa/src/main/resources/META-INF/jpa-aggregate-changelog.xml @@ -27,5 +27,6 @@ limitations under the License. + \ No newline at end of file diff --git a/model/map-jpa/src/main/resources/META-INF/jpa-single-use-objects-changelog.xml b/model/map-jpa/src/main/resources/META-INF/jpa-single-use-objects-changelog.xml new file mode 100644 index 0000000000..d0250a21cd --- /dev/null +++ b/model/map-jpa/src/main/resources/META-INF/jpa-single-use-objects-changelog.xml @@ -0,0 +1,23 @@ + + + + + + + + diff --git a/model/map-jpa/src/main/resources/META-INF/persistence.xml b/model/map-jpa/src/main/resources/META-INF/persistence.xml index 80c96925d5..5a0e36e637 100644 --- a/model/map-jpa/src/main/resources/META-INF/persistence.xml +++ b/model/map-jpa/src/main/resources/META-INF/persistence.xml @@ -33,6 +33,9 @@ org.keycloak.models.map.storage.jpa.role.entity.JpaRoleEntity org.keycloak.models.map.storage.jpa.role.entity.JpaRoleCompositeEntity org.keycloak.models.map.storage.jpa.role.entity.JpaRoleAttributeEntity + + org.keycloak.models.map.storage.jpa.singleUseObject.entity.JpaSingleUseObjectEntity + org.keycloak.models.map.storage.jpa.singleUseObject.entity.JpaSingleUseObjectNoteEntity org.keycloak.models.map.storage.jpa.loginFailure.entity.JpaUserLoginFailureEntity diff --git a/model/map-jpa/src/main/resources/META-INF/single-use-objects/jpa-single-use-objects-changelog-1.xml b/model/map-jpa/src/main/resources/META-INF/single-use-objects/jpa-single-use-objects-changelog-1.xml new file mode 100644 index 0000000000..ad2e6fc4c2 --- /dev/null +++ b/model/map-jpa/src/main/resources/META-INF/single-use-objects/jpa-single-use-objects-changelog-1.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/model/map/src/main/java/org/keycloak/models/map/singleUseObject/MapSingleUseObjectEntity.java b/model/map/src/main/java/org/keycloak/models/map/singleUseObject/MapSingleUseObjectEntity.java index 0863f6a893..5330cd287a 100644 --- a/model/map/src/main/java/org/keycloak/models/map/singleUseObject/MapSingleUseObjectEntity.java +++ b/model/map/src/main/java/org/keycloak/models/map/singleUseObject/MapSingleUseObjectEntity.java @@ -54,6 +54,9 @@ public interface MapSingleUseObjectEntity extends AbstractEntity, UpdatableEntit String getUserId(); void setUserId(String userId); + String getObjectKey(); + void setObjectKey(String objectKey); + String getActionId(); void setActionId(String actionId); diff --git a/model/map/src/main/java/org/keycloak/models/map/singleUseObject/MapSingleUseObjectProvider.java b/model/map/src/main/java/org/keycloak/models/map/singleUseObject/MapSingleUseObjectProvider.java index 6ff0666216..2586f6afc1 100644 --- a/model/map/src/main/java/org/keycloak/models/map/singleUseObject/MapSingleUseObjectProvider.java +++ b/model/map/src/main/java/org/keycloak/models/map/singleUseObject/MapSingleUseObjectProvider.java @@ -142,11 +142,11 @@ public class MapSingleUseObjectProvider implements ActionTokenStoreProvider, Sin MapSingleUseObjectEntity singleUseEntity = getWithExpiration(key); if (singleUseEntity != null) { - throw new ModelDuplicateException("Single-use object entity exists: " + singleUseEntity.getId()); + throw new ModelDuplicateException("Single-use object entity exists: " + singleUseEntity.getObjectKey()); } singleUseEntity = new MapSingleUseObjectEntityImpl(); - singleUseEntity.setId(key); + singleUseEntity.setObjectKey(key); singleUseEntity.setExpiration(Time.currentTimeMillis() + TimeAdapter.fromSecondsToMilliseconds(lifespanSeconds)); singleUseEntity.setNotes(notes); @@ -174,7 +174,7 @@ public class MapSingleUseObjectProvider implements ActionTokenStoreProvider, Sin if (singleUseEntity != null) { Map notes = singleUseEntity.getNotes(); - if (actionTokenStoreTx.delete(key)) { + if (actionTokenStoreTx.delete(singleUseEntity.getId())) { return notes == null ? Collections.emptyMap() : Collections.unmodifiableMap(notes); } } @@ -204,7 +204,7 @@ public class MapSingleUseObjectProvider implements ActionTokenStoreProvider, Sin return false; } else { singleUseEntity = new MapSingleUseObjectEntityImpl(); - singleUseEntity.setId(key); + singleUseEntity.setObjectKey(key); singleUseEntity.setExpiration(Time.currentTimeMillis() + TimeAdapter.fromSecondsToMilliseconds(lifespanInSeconds)); actionTokenStoreTx.create(singleUseEntity); @@ -228,10 +228,13 @@ public class MapSingleUseObjectProvider implements ActionTokenStoreProvider, Sin } private MapSingleUseObjectEntity getWithExpiration(String key) { - MapSingleUseObjectEntity singleUseEntity = actionTokenStoreTx.read(key); + DefaultModelCriteria mcb = criteria(); + mcb = mcb.compare(ActionTokenValueModel.SearchableFields.OBJECT_KEY, ModelCriteriaBuilder.Operator.EQ, key); + + MapSingleUseObjectEntity singleUseEntity = actionTokenStoreTx.read(withCriteria(mcb)).findFirst().orElse(null); if (singleUseEntity != null) { if (isExpired(singleUseEntity, false)) { - actionTokenStoreTx.delete(key); + actionTokenStoreTx.delete(singleUseEntity.getId()); } else { return singleUseEntity; } diff --git a/model/map/src/main/java/org/keycloak/models/map/storage/chm/MapFieldPredicates.java b/model/map/src/main/java/org/keycloak/models/map/storage/chm/MapFieldPredicates.java index 636630fc31..33548104ef 100644 --- a/model/map/src/main/java/org/keycloak/models/map/storage/chm/MapFieldPredicates.java +++ b/model/map/src/main/java/org/keycloak/models/map/storage/chm/MapFieldPredicates.java @@ -230,6 +230,7 @@ public class MapFieldPredicates { put(ACTION_TOKEN_PREDICATES, ActionTokenValueModel.SearchableFields.USER_ID, MapSingleUseObjectEntity::getUserId); put(ACTION_TOKEN_PREDICATES, ActionTokenValueModel.SearchableFields.ACTION_ID, MapSingleUseObjectEntity::getActionId); put(ACTION_TOKEN_PREDICATES, ActionTokenValueModel.SearchableFields.ACTION_VERIFICATION_NONCE, MapSingleUseObjectEntity::getActionVerificationNonce); + put(ACTION_TOKEN_PREDICATES, ActionTokenValueModel.SearchableFields.OBJECT_KEY, MapSingleUseObjectEntity::getObjectKey); } static { diff --git a/server-spi/src/main/java/org/keycloak/models/ActionTokenValueModel.java b/server-spi/src/main/java/org/keycloak/models/ActionTokenValueModel.java index 170dd0ff68..a853cb1404 100644 --- a/server-spi/src/main/java/org/keycloak/models/ActionTokenValueModel.java +++ b/server-spi/src/main/java/org/keycloak/models/ActionTokenValueModel.java @@ -28,6 +28,7 @@ public interface ActionTokenValueModel { class SearchableFields { public static final SearchableModelField ID = new SearchableModelField<>("id", String.class); + public static final SearchableModelField OBJECT_KEY = new SearchableModelField<>("objectKey", String.class); public static final SearchableModelField USER_ID = new SearchableModelField<>("userId", String.class); public static final SearchableModelField ACTION_ID = new SearchableModelField<>("actionId", String.class); public static final SearchableModelField ACTION_VERIFICATION_NONCE = new SearchableModelField<>("actionVerificationNonce", String.class); diff --git a/testsuite/integration-arquillian/tests/base/pom.xml b/testsuite/integration-arquillian/tests/base/pom.xml index e08790531f..b1ac5996e7 100644 --- a/testsuite/integration-arquillian/tests/base/pom.xml +++ b/testsuite/integration-arquillian/tests/base/pom.xml @@ -897,6 +897,7 @@ jpa jpa jpa + jpa diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/JpaMapStorage.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/JpaMapStorage.java index e33ad7be54..716391d488 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/JpaMapStorage.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/JpaMapStorage.java @@ -98,7 +98,7 @@ public class JpaMapStorage extends KeycloakModelParameters { .spi(UserLoginFailureSpi.NAME).provider(MapUserLoginFailureProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, JpaMapStorageProviderFactory.PROVIDER_ID) .spi("dblock").provider(NoLockingDBLockProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.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(SingleUseObjectSpi.NAME).provider(MapSingleUseObjectProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, JpaMapStorageProviderFactory.PROVIDER_ID) .spi(UserSessionSpi.NAME).provider(MapUserSessionProviderFactory.PROVIDER_ID) .config("storage-user-sessions.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID) .config("storage-client-sessions.provider", ConcurrentHashMapStorageProviderFactory.PROVIDER_ID) .spi(EventStoreSpi.NAME).provider(MapUserSessionProviderFactory.PROVIDER_ID) .config("storage-admin-events.provider", JpaMapStorageProviderFactory.PROVIDER_ID)