Single Use Objects Map JPA implementation

Closes #9852
This commit is contained in:
Stefan Guilhen 2022-06-02 15:36:56 -03:00 committed by Bruno Oliveira da Silva
parent 4b20e90292
commit 007fa1f374
20 changed files with 639 additions and 12 deletions

View file

@ -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<HotRodPair<String, String>> notes;
public static abstract class AbstractHotRodSingleUseObjectEntityDelegate extends UpdatableHotRodEntityDelegateImpl<HotRodSingleUseObjectEntity> implements MapSingleUseObjectEntity {

View file

@ -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;
}

View file

@ -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

View file

@ -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

View file

@ -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 <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/
public class JpaSingleUseObjectMigration {
public static final List<Function<ObjectNode, ObjectNode>> MIGRATORS = Arrays.asList(
o -> o // no migration yet
);
}

View file

@ -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 <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/
public class JpaSingleUseObjectMapKeycloakTransaction extends JpaMapKeycloakTransaction<JpaSingleUseObjectEntity, MapSingleUseObjectEntity, ActionTokenValueModel> {
public JpaSingleUseObjectMapKeycloakTransaction(final EntityManager em) {
super(JpaSingleUseObjectEntity.class, ActionTokenValueModel.class, em);
}
@Override
protected Selection<? extends JpaSingleUseObjectEntity> selectCbConstruct(CriteriaBuilder cb, Root<JpaSingleUseObjectEntity> 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;
}
}

View file

@ -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 <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/
public class JpaSingleUseObjectModelCriteriaBuilder extends JpaModelCriteriaBuilder<JpaSingleUseObjectEntity, ActionTokenValueModel, JpaSingleUseObjectModelCriteriaBuilder> {
private static final Map<String, String> 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<CriteriaBuilder, Root<JpaSingleUseObjectEntity>, Predicate> predicateFunc) {
super(JpaSingleUseObjectModelCriteriaBuilder::new, predicateFunc);
}
@Override
public JpaSingleUseObjectModelCriteriaBuilder compare(SearchableModelField<? super ActionTokenValueModel> 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);
}
}
}

View file

@ -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 <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/
@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<JpaSingleUseObjectNoteEntity> 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<String, String> 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<String, String> 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());
}
}

View file

@ -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 <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/
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;
}
}

View file

@ -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 <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/
@Entity
@Table(name = "kc_single_use_obj_note", uniqueConstraints = {
@UniqueConstraint(columnNames = {"fk_root", "name"})
})
public class JpaSingleUseObjectNoteEntity extends JpaAttributeEntity<JpaSingleUseObjectEntity> {
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());
}
}

View file

@ -27,5 +27,6 @@ limitations under the License.
<include file="META-INF/jpa-groups-changelog.xml"/>
<include file="META-INF/jpa-realms-changelog.xml"/>
<include file="META-INF/jpa-roles-changelog.xml"/>
<include file="META-INF/jpa-single-use-objects-changelog.xml"/>
<include file="META-INF/jpa-user-login-failures-changelog.xml"/>
</databaseChangeLog>

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<!-- format of id of changelog file names: jpa-single-use-objects-changelog-${org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_SINGLE_USE_OBJECT}.xml -->
<include file="META-INF/single-use-objects/jpa-single-use-objects-changelog-1.xml"/>
</databaseChangeLog>

View file

@ -33,6 +33,9 @@
<class>org.keycloak.models.map.storage.jpa.role.entity.JpaRoleEntity</class>
<class>org.keycloak.models.map.storage.jpa.role.entity.JpaRoleCompositeEntity</class>
<class>org.keycloak.models.map.storage.jpa.role.entity.JpaRoleAttributeEntity</class>
<!--sinle-use-objects-->
<class>org.keycloak.models.map.storage.jpa.singleUseObject.entity.JpaSingleUseObjectEntity</class>
<class>org.keycloak.models.map.storage.jpa.singleUseObject.entity.JpaSingleUseObjectNoteEntity</class>
<!--user-login-failures-->
<class>org.keycloak.models.map.storage.jpa.loginFailure.entity.JpaUserLoginFailureEntity</class>
</persistence-unit>

View file

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
<changeSet author="keycloak" id="single-use-object-1">
<createTable tableName="kc_single_use_obj">
<column name="id" type="UUID">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="version" type="INTEGER" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="metadata" type="json"/>
</createTable>
<ext:addGeneratedColumn tableName="kc_single_use_obj">
<ext:column name="entityversion" type="INTEGER" jsonColumn="metadata" jsonProperty="entityVersion"/>
<ext:column name="objectkey" type="VARCHAR(255)" jsonColumn="metadata" jsonProperty="fObjectKey"/>
<ext:column name="expiration" type="BIGINT" jsonColumn="metadata" jsonProperty="fExpiration"/>
</ext:addGeneratedColumn>
<createIndex tableName="kc_single_use_obj" indexName="single_use_obj_entityVersion">
<column name="entityversion"/>
</createIndex>
<createIndex tableName="kc_single_use_obj" indexName="single_use_obj_objectKey">
<column name="objectkey"/>
</createIndex>
<createIndex tableName="kc_single_use_obj" indexName="single_use_obj_expiration">
<column name="expiration"/>
</createIndex>
<ext:createJsonIndex tableName="kc_single_use_obj" indexName="kc_single_use_obj_nonce">
<ext:column jsonColumn="metadata" jsonProperty="fActionVerificationNonce"/>
</ext:createJsonIndex>
<createTable tableName="kc_single_use_obj_note">
<column name="id" type="UUID">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="fk_root" type="UUID">
<constraints foreignKeyName="single_use_obj_note_fk_root_fkey" references="kc_single_use_obj(id)" deleteCascade="true"/>
</column>
<column name="name" type="VARCHAR(255)"/>
<column name="value" type="TEXT"/>
</createTable>
<createIndex tableName="kc_single_use_obj_note" indexName="single_use_obj_note_fk_root">
<column name="fk_root"/>
</createIndex>
</changeSet>
</databaseChangeLog>

View file

@ -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);

View file

@ -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<String, String> 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<ActionTokenValueModel> 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;
}

View file

@ -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 {

View file

@ -28,6 +28,7 @@ public interface ActionTokenValueModel {
class SearchableFields {
public static final SearchableModelField<ActionTokenValueModel> ID = new SearchableModelField<>("id", String.class);
public static final SearchableModelField<ActionTokenValueModel> OBJECT_KEY = new SearchableModelField<>("objectKey", String.class);
public static final SearchableModelField<ActionTokenValueModel> USER_ID = new SearchableModelField<>("userId", String.class);
public static final SearchableModelField<ActionTokenValueModel> ACTION_ID = new SearchableModelField<>("actionId", String.class);
public static final SearchableModelField<ActionTokenValueModel> ACTION_VERIFICATION_NONCE = new SearchableModelField<>("actionVerificationNonce", String.class);

View file

@ -897,6 +897,7 @@
<keycloak.loginFailure.map.storage.provider>jpa</keycloak.loginFailure.map.storage.provider>
<keycloak.realm.map.storage.provider>jpa</keycloak.realm.map.storage.provider>
<keycloak.role.map.storage.provider>jpa</keycloak.role.map.storage.provider>
<keycloak.singleUseObject.map.storage.provider>jpa</keycloak.singleUseObject.map.storage.provider>
</systemPropertyVariables>
</configuration>
</plugin>

View file

@ -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)