parent
4b20e90292
commit
007fa1f374
20 changed files with 639 additions and 12 deletions
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue