Users Map JPA implementation (#12871)

This commit is contained in:
Stefan Guilhen 2022-07-05 11:19:31 -03:00 committed by GitHub
parent b5ca03222f
commit dc88dd5286
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 1851 additions and 61 deletions

View file

@ -16,6 +16,8 @@
*/
package org.keycloak.models.map.storage.jpa;
import javax.persistence.criteria.CriteriaBuilder;
public interface Constants {
public static final Integer CURRENT_SCHEMA_VERSION_ADMIN_EVENT = 1;
public static final Integer CURRENT_SCHEMA_VERSION_AUTH_EVENT = 1;
@ -34,4 +36,8 @@ public interface Constants {
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;
public static final Integer CURRENT_SCHEMA_VERSION_USER = 1;
public static final Integer CURRENT_SCHEMA_VERSION_USER_CONSENT = 1;
public static final Integer CURRENT_SCHEMA_VERSION_USER_FEDERATED_IDENTITY = 1;
}

View file

@ -67,6 +67,7 @@ import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserLoginFailureModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.dblock.DBLockProvider;
import org.keycloak.models.map.client.MapProtocolMapperEntity;
import org.keycloak.models.map.client.MapProtocolMapperEntityImpl;
@ -130,6 +131,12 @@ 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.map.storage.jpa.user.JpaUserMapKeycloakTransaction;
import org.keycloak.models.map.storage.jpa.user.entity.JpaUserConsentEntity;
import org.keycloak.models.map.storage.jpa.user.entity.JpaUserEntity;
import org.keycloak.models.map.storage.jpa.user.entity.JpaUserFederatedIdentityEntity;
import org.keycloak.models.map.user.MapUserCredentialEntity;
import org.keycloak.models.map.user.MapUserCredentialEntityImpl;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.sessions.RootAuthenticationSessionModel;
@ -153,26 +160,26 @@ public class JpaMapStorageProviderFactory implements
private final String sessionTxKey;
public final static DeepCloner CLONER = new DeepCloner.Builder()
//auth-session
//auth-sessions
.constructor(JpaRootAuthenticationSessionEntity.class, JpaRootAuthenticationSessionEntity::new)
.constructor(JpaAuthenticationSessionEntity.class, JpaAuthenticationSessionEntity::new)
//authz
//authorization
.constructor(JpaResourceServerEntity.class, JpaResourceServerEntity::new)
.constructor(JpaResourceEntity.class, JpaResourceEntity::new)
.constructor(JpaScopeEntity.class, JpaScopeEntity::new)
.constructor(JpaPermissionEntity.class, JpaPermissionEntity::new)
.constructor(JpaPolicyEntity.class, JpaPolicyEntity::new)
//client
//clients
.constructor(JpaClientEntity.class, JpaClientEntity::new)
.constructor(MapProtocolMapperEntity.class, MapProtocolMapperEntityImpl::new)
//client-scope
//client-scopes
.constructor(JpaClientScopeEntity.class, JpaClientScopeEntity::new)
//event
//events
.constructor(JpaAdminEventEntity.class, JpaAdminEventEntity::new)
.constructor(JpaAuthEventEntity.class, JpaAuthEventEntity::new)
//group
//groups
.constructor(JpaGroupEntity.class, JpaGroupEntity::new)
//realm
//realms
.constructor(JpaRealmEntity.class, JpaRealmEntity::new)
.constructor(JpaComponentEntity.class, JpaComponentEntity::new)
.constructor(MapAuthenticationExecutionEntity.class, MapAuthenticationExecutionEntityImpl::new)
@ -185,34 +192,48 @@ public class JpaMapStorageProviderFactory implements
.constructor(MapRequiredActionProviderEntity.class, MapRequiredActionProviderEntityImpl::new)
.constructor(MapRequiredCredentialEntity.class, MapRequiredCredentialEntityImpl::new)
.constructor(MapWebAuthnPolicyEntity.class, MapWebAuthnPolicyEntityImpl::new)
//role
//roles
.constructor(JpaRoleEntity.class, JpaRoleEntity::new)
//single-use-object
//single-use-objects
.constructor(JpaSingleUseObjectEntity.class, JpaSingleUseObjectEntity::new)
//user-login-failure
//user-login-failures
.constructor(JpaUserLoginFailureEntity.class, JpaUserLoginFailureEntity::new)
//users
.constructor(JpaUserEntity.class, JpaUserEntity::new)
.constructor(JpaUserConsentEntity.class, JpaUserConsentEntity::new)
.constructor(JpaUserFederatedIdentityEntity.class, JpaUserFederatedIdentityEntity::new)
.constructor(MapUserCredentialEntity.class, MapUserCredentialEntityImpl::new)
.build();
private static final Map<Class<?>, Function<EntityManager, MapKeycloakTransaction>> MODEL_TO_TX = new HashMap<>();
static {
//auth-sessions
MODEL_TO_TX.put(RootAuthenticationSessionModel.class, JpaRootAuthenticationSessionMapKeycloakTransaction::new);
MODEL_TO_TX.put(ClientScopeModel.class, JpaClientScopeMapKeycloakTransaction::new);
MODEL_TO_TX.put(ClientModel.class, JpaClientMapKeycloakTransaction::new);
//event
MODEL_TO_TX.put(AdminEvent.class, JpaAdminEventMapKeycloakTransaction::new);
MODEL_TO_TX.put(Event.class, JpaAuthEventMapKeycloakTransaction::new);
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
//authorization
MODEL_TO_TX.put(ResourceServer.class, JpaResourceServerMapKeycloakTransaction::new);
MODEL_TO_TX.put(Resource.class, JpaResourceMapKeycloakTransaction::new);
MODEL_TO_TX.put(Scope.class, JpaScopeMapKeycloakTransaction::new);
MODEL_TO_TX.put(PermissionTicket.class, JpaPermissionMapKeycloakTransaction::new);
MODEL_TO_TX.put(Policy.class, JpaPolicyMapKeycloakTransaction::new);
//clients
MODEL_TO_TX.put(ClientModel.class, JpaClientMapKeycloakTransaction::new);
//client-scopes
MODEL_TO_TX.put(ClientScopeModel.class, JpaClientScopeMapKeycloakTransaction::new);
//events
MODEL_TO_TX.put(AdminEvent.class, JpaAdminEventMapKeycloakTransaction::new);
MODEL_TO_TX.put(Event.class, JpaAuthEventMapKeycloakTransaction::new);
//groups
MODEL_TO_TX.put(GroupModel.class, JpaGroupMapKeycloakTransaction::new);
//realms
MODEL_TO_TX.put(RealmModel.class, JpaRealmMapKeycloakTransaction::new);
//roles
MODEL_TO_TX.put(RoleModel.class, JpaRoleMapKeycloakTransaction::new);
//single-use-objects
MODEL_TO_TX.put(ActionTokenValueModel.class, JpaSingleUseObjectMapKeycloakTransaction::new);
//user-login-failures
MODEL_TO_TX.put(UserLoginFailureModel.class, JpaUserLoginFailureMapKeycloakTransaction::new);
//users
MODEL_TO_TX.put(UserModel.class, JpaUserMapKeycloakTransaction::new);
}
public JpaMapStorageProviderFactory() {

View file

@ -51,12 +51,18 @@ import org.keycloak.models.map.storage.jpa.hibernate.jsonb.migration.JpaRoleMigr
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.JpaUserConsentMigration;
import org.keycloak.models.map.storage.jpa.hibernate.jsonb.migration.JpaUserFederatedIdentityMigration;
import org.keycloak.models.map.storage.jpa.hibernate.jsonb.migration.JpaUserLoginFailureMigration;
import org.keycloak.models.map.storage.jpa.hibernate.jsonb.migration.JpaUserMigration;
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 org.keycloak.models.map.storage.jpa.user.entity.JpaUserConsentMetadata;
import org.keycloak.models.map.storage.jpa.user.entity.JpaUserFederatedIdentityMetadata;
import org.keycloak.models.map.storage.jpa.user.entity.JpaUserMetadata;
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;
@ -72,33 +78,47 @@ import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSI
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;
import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_USER_CONSENT;
import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_USER_FEDERATED_IDENTITY;
import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_USER_LOGIN_FAILURE;
public class JpaEntityMigration {
static final Map<Class<?>, BiFunction<ObjectNode, Integer, ObjectNode>> MIGRATIONS = new HashMap<>();
static {
//auth-sessions
MIGRATIONS.put(JpaAuthenticationSessionMetadata.class, (tree, entityVersion) -> migrateTreeTo(entityVersion, CURRENT_SCHEMA_VERSION_AUTH_SESSION, tree, JpaAuthenticationSessionMigration.MIGRATORS));
MIGRATIONS.put(JpaRootAuthenticationSessionMetadata.class, (tree, entityVersion) -> migrateTreeTo(entityVersion, CURRENT_SCHEMA_VERSION_AUTH_SESSION, tree, JpaRootAuthenticationSessionMigration.MIGRATORS));
MIGRATIONS.put(JpaClientMetadata.class, (tree, entityVersion) -> migrateTreeTo(entityVersion, CURRENT_SCHEMA_VERSION_CLIENT, tree, JpaClientMigration.MIGRATORS));
MIGRATIONS.put(JpaClientScopeMetadata.class, (tree, entityVersion) -> migrateTreeTo(entityVersion, CURRENT_SCHEMA_VERSION_CLIENT_SCOPE, tree, JpaClientScopeMigration.MIGRATORS));
MIGRATIONS.put(JpaComponentMetadata.class, (tree, entityVersion) -> migrateTreeTo(entityVersion, CURRENT_SCHEMA_VERSION_REALM, tree, JpaComponentMigration.MIGRATORS));
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
//authorization
MIGRATIONS.put(JpaPermissionMetadata.class, (tree, entityVersion) -> migrateTreeTo(entityVersion, CURRENT_SCHEMA_VERSION_AUTHZ_PERMISSION, tree, JpaPermissionMigration.MIGRATORS));
MIGRATIONS.put(JpaPolicyMetadata.class, (tree, entityVersion) -> migrateTreeTo(entityVersion, CURRENT_SCHEMA_VERSION_AUTHZ_POLICY, tree, JpaPolicyMigration.MIGRATORS));
MIGRATIONS.put(JpaResourceMetadata.class, (tree, entityVersion) -> migrateTreeTo(entityVersion, CURRENT_SCHEMA_VERSION_AUTHZ_RESOURCE, tree, JpaResourceMigration.MIGRATORS));
MIGRATIONS.put(JpaResourceServerMetadata.class, (tree, entityVersion) -> migrateTreeTo(entityVersion, CURRENT_SCHEMA_VERSION_AUTHZ_RESOURCE_SERVER, tree, JpaResourceServerMigration.MIGRATORS));
MIGRATIONS.put(JpaScopeMetadata.class, (tree, entityVersion) -> migrateTreeTo(entityVersion, CURRENT_SCHEMA_VERSION_AUTHZ_SCOPE, tree, JpaScopeMigration.MIGRATORS));
// events
//clients
MIGRATIONS.put(JpaClientMetadata.class, (tree, entityVersion) -> migrateTreeTo(entityVersion, CURRENT_SCHEMA_VERSION_CLIENT, tree, JpaClientMigration.MIGRATORS));
//client-scopes
MIGRATIONS.put(JpaClientScopeMetadata.class, (tree, entityVersion) -> migrateTreeTo(entityVersion, CURRENT_SCHEMA_VERSION_CLIENT_SCOPE, tree, JpaClientScopeMigration.MIGRATORS));
//events
MIGRATIONS.put(JpaAdminEventMetadata.class, (tree, entityVersion) -> migrateTreeTo(entityVersion, CURRENT_SCHEMA_VERSION_ADMIN_EVENT, tree, JpaAdminEventMigration.MIGRATORS));
MIGRATIONS.put(JpaAuthEventMetadata.class, (tree, entityVersion) -> migrateTreeTo(entityVersion, CURRENT_SCHEMA_VERSION_AUTH_EVENT, tree, JpaAuthEventMigration.MIGRATORS));
//groups
MIGRATIONS.put(JpaGroupMetadata.class, (tree, entityVersion) -> migrateTreeTo(entityVersion, CURRENT_SCHEMA_VERSION_GROUP, tree, JpaGroupMigration.MIGRATORS));
//realms
MIGRATIONS.put(JpaComponentMetadata.class, (tree, entityVersion) -> migrateTreeTo(entityVersion, CURRENT_SCHEMA_VERSION_REALM, tree, JpaComponentMigration.MIGRATORS));
MIGRATIONS.put(JpaRealmMetadata.class, (tree, entityVersion) -> migrateTreeTo(entityVersion, CURRENT_SCHEMA_VERSION_REALM, tree, JpaRealmMigration.MIGRATORS));
//roles
MIGRATIONS.put(JpaRoleMetadata.class, (tree, entityVersion) -> migrateTreeTo(entityVersion, CURRENT_SCHEMA_VERSION_ROLE, tree, JpaRoleMigration.MIGRATORS));
//single-use-objects
MIGRATIONS.put(JpaSingleUseObjectMetadata.class, (tree, entityVersion) -> migrateTreeTo(entityVersion, CURRENT_SCHEMA_VERSION_SINGLE_USE_OBJECT, tree, JpaSingleUseObjectMigration.MIGRATORS));
//user-login-failures
MIGRATIONS.put(JpaUserLoginFailureMetadata.class, (tree, entityVersion) -> migrateTreeTo(entityVersion, CURRENT_SCHEMA_VERSION_USER_LOGIN_FAILURE,tree, JpaUserLoginFailureMigration.MIGRATORS));
//users
MIGRATIONS.put(JpaUserMetadata.class, (tree, entityVersion) -> migrateTreeTo(entityVersion, CURRENT_SCHEMA_VERSION_USER, tree, JpaUserMigration.MIGRATORS));
MIGRATIONS.put(JpaUserConsentMetadata.class, (tree, entityVersion) -> migrateTreeTo(entityVersion, CURRENT_SCHEMA_VERSION_USER_CONSENT, tree, JpaUserConsentMigration.MIGRATORS));
MIGRATIONS.put(JpaUserFederatedIdentityMetadata.class, (tree, entityVersion) -> migrateTreeTo(entityVersion, CURRENT_SCHEMA_VERSION_USER_FEDERATED_IDENTITY, tree, JpaUserFederatedIdentityMigration.MIGRATORS));
}
private static ObjectNode migrateTreeTo(int entityVersion, Integer supportedVersion, ObjectNode node, List<Function<ObjectNode, ObjectNode>> migrators) {

View file

@ -78,6 +78,8 @@ import org.keycloak.models.map.realm.entity.MapRequiredCredentialEntity;
import org.keycloak.models.map.realm.entity.MapRequiredCredentialEntityImpl;
import org.keycloak.models.map.realm.entity.MapWebAuthnPolicyEntity;
import org.keycloak.models.map.realm.entity.MapWebAuthnPolicyEntityImpl;
import org.keycloak.models.map.user.MapUserCredentialEntity;
import org.keycloak.models.map.user.MapUserCredentialEntityImpl;
import org.keycloak.util.EnumWithStableIndex;
public class JsonbType extends AbstractSingleColumnStandardBasicType<Object> implements DynamicParameterizedType {
@ -102,7 +104,9 @@ public class JsonbType extends AbstractSingleColumnStandardBasicType<Object> imp
.addAbstractTypeMapping(MapOTPPolicyEntity.class, MapOTPPolicyEntityImpl.class)
.addAbstractTypeMapping(MapRequiredActionProviderEntity.class, MapRequiredActionProviderEntityImpl.class)
.addAbstractTypeMapping(MapRequiredCredentialEntity.class, MapRequiredCredentialEntityImpl.class)
.addAbstractTypeMapping(MapWebAuthnPolicyEntity.class, MapWebAuthnPolicyEntityImpl.class))
.addAbstractTypeMapping(MapWebAuthnPolicyEntity.class, MapWebAuthnPolicyEntityImpl.class)
// user abstract type mappings
.addAbstractTypeMapping(MapUserCredentialEntity.class, MapUserCredentialEntityImpl.class))
.addMixIn(UpdatableEntity.class, IgnoreUpdatedMixIn.class)
.addMixIn(DeepCloner.class, IgnoredTypeMixIn.class)
.addMixIn(EntityWithAttributes.class, IgnoredMetadataFieldsMixIn.class)

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

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

View file

@ -0,0 +1,34 @@
/*
* 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 com.fasterxml.jackson.databind.node.ObjectNode;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
/**
* Migration functions for users.
*
* @author <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/
public class JpaUserMigration {
public static final List<Function<ObjectNode, ObjectNode>> MIGRATORS = Arrays.asList(
o -> o // no migration yet
);
}

View file

@ -0,0 +1,79 @@
/*
* 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.user;
import javax.persistence.EntityManager;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.Root;
import javax.persistence.criteria.Selection;
import org.keycloak.models.UserModel;
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.user.delegate.JpaUserDelegateProvider;
import org.keycloak.models.map.storage.jpa.user.entity.JpaUserEntity;
import org.keycloak.models.map.user.MapUserEntity;
import org.keycloak.models.map.user.MapUserEntityDelegate;
import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_USER;
/**
* A {@link org.keycloak.models.map.storage.MapKeycloakTransaction} implementation for user entities.
*
* @author <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/
public class JpaUserMapKeycloakTransaction extends JpaMapKeycloakTransaction<JpaUserEntity, MapUserEntity, UserModel> {
public JpaUserMapKeycloakTransaction(final EntityManager em) {
super(JpaUserEntity.class, UserModel.class, em);
}
@Override
protected Selection<? extends JpaUserEntity> selectCbConstruct(CriteriaBuilder cb, Root<JpaUserEntity> root) {
return cb.construct(JpaUserEntity.class,
root.get("id"),
root.get("version"),
root.get("entityVersion"),
root.get("realmId"),
root.get("username"),
root.get("firstName"),
root.get("lastName"),
root.get("email"),
root.get("emailConstraint"),
root.get("federationLink"),
root.get("enabled"),
root.get("emailVerified"),
root.get("timestamp")
);
}
@Override
protected void setEntityVersion(JpaRootEntity entity) {
entity.setEntityVersion(CURRENT_SCHEMA_VERSION_USER);
}
@Override
protected JpaModelCriteriaBuilder createJpaModelCriteriaBuilder() {
return new JpaUserModelCriteriaBuilder();
}
@Override
protected MapUserEntity mapToEntityDelegate(JpaUserEntity original) {
return new MapUserEntityDelegate(new JpaUserDelegateProvider(original, this.em));
}
}

View file

@ -0,0 +1,195 @@
/*
* 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.user;
import java.util.Arrays;
import java.util.Collection;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.JoinType;
import org.keycloak.models.UserModel;
import org.keycloak.models.map.storage.CriterionNotSupportedException;
import org.keycloak.models.map.storage.jpa.JpaModelCriteriaBuilder;
import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType;
import org.keycloak.models.map.storage.jpa.role.JpaPredicateFunction;
import org.keycloak.models.map.storage.jpa.user.entity.JpaUserAttributeEntity;
import org.keycloak.models.map.storage.jpa.user.entity.JpaUserConsentEntity;
import org.keycloak.models.map.storage.jpa.user.entity.JpaUserEntity;
import org.keycloak.models.map.storage.jpa.user.entity.JpaUserFederatedIdentityEntity;
import org.keycloak.storage.SearchableModelField;
import org.keycloak.storage.StorageId;
/**
* A {@link JpaModelCriteriaBuilder} implementation for users.
*
* @author <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/
public class JpaUserModelCriteriaBuilder extends JpaModelCriteriaBuilder<JpaUserEntity, UserModel, JpaUserModelCriteriaBuilder> {
public JpaUserModelCriteriaBuilder() {
super(JpaUserModelCriteriaBuilder::new);
}
private JpaUserModelCriteriaBuilder(final JpaPredicateFunction<JpaUserEntity> predicateFunc) {
super(JpaUserModelCriteriaBuilder::new, predicateFunc);
}
@Override
public JpaUserModelCriteriaBuilder compare(SearchableModelField<? super UserModel> modelField, Operator op, Object... value) {
switch(op) {
case EQ:
if (modelField == UserModel.SearchableFields.REALM_ID ||
modelField == UserModel.SearchableFields.USERNAME ||
modelField == UserModel.SearchableFields.EMAIL ||
modelField == UserModel.SearchableFields.FEDERATION_LINK) {
validateValue(value, modelField, op, String.class);
return new JpaUserModelCriteriaBuilder((cb, query, root) ->
cb.equal(root.get(modelField.getName()), value[0])
);
} else if (modelField == UserModel.SearchableFields.ENABLED ||
modelField == UserModel.SearchableFields.EMAIL_VERIFIED) {
validateValue(value, modelField, op, Boolean.class);
return new JpaUserModelCriteriaBuilder((cb, query, root) ->
cb.equal(root.get(modelField.getName()), value[0])
);
} else if (modelField == UserModel.SearchableFields.IDP_AND_USER) {
if (value == null || value.length == 0 || value.length > 2) {
throw new CriterionNotSupportedException(modelField, op,
"Invalid arguments, expected (idp_alias) or (idp_alias, idp_user), got: " + Arrays.toString(value));
}
if (value.length == 1) {
// search by idp only
return new JpaUserModelCriteriaBuilder((cb, query, root) ->
cb.equal(root.join("federatedIdentities", JoinType.LEFT).get("identityProvider"),
value[0]));
} else if (value[0] == null) {
// search by userid only
return new JpaUserModelCriteriaBuilder((cb, query, root) ->
cb.equal(root.join("federatedIdentities", JoinType.LEFT).get("userId"),
value[1]));
} else {
// search using both idp and userid
return new JpaUserModelCriteriaBuilder((cb, query, root) -> {
Join<JpaUserEntity, JpaUserFederatedIdentityEntity> join =
root.join("federatedIdentities", JoinType.LEFT);
return cb.and(cb.equal(join.get("identityProvider"), value[0]),
cb.equal(join.get("userId"),value[1]));
});
}
} else if (modelField == UserModel.SearchableFields.ASSIGNED_GROUP) {
validateValue(value, modelField, op, String.class);
return new JpaUserModelCriteriaBuilder((cb, query, root) ->
cb.equal(root.join("groupIds", JoinType.LEFT), value[0]));
} else if (modelField == UserModel.SearchableFields.ASSIGNED_ROLE) {
validateValue(value, modelField, op, String.class);
return new JpaUserModelCriteriaBuilder((cb, query, root) ->
cb.equal(root.join("roleIds", JoinType.LEFT), value[0]));
} else if (modelField == UserModel.SearchableFields.SERVICE_ACCOUNT_CLIENT) {
validateValue(value, modelField, op, String.class);
return new JpaUserModelCriteriaBuilder((cb, query, root) ->
cb.equal(
cb.function("->>", String.class, root.get("metadata"), cb.literal("fServiceAccountClientLink")),
value[0]));
} else if (modelField == UserModel.SearchableFields.CONSENT_FOR_CLIENT) {
validateValue(value, modelField, op, String.class);
return new JpaUserModelCriteriaBuilder((cb, query, root) ->
cb.equal(root.join("consents", JoinType.LEFT).get("clientId"), value[0]));
} else if (modelField == UserModel.SearchableFields.CONSENT_CLIENT_FEDERATION_LINK) {
validateValue(value, modelField, op, String.class);
return new JpaUserModelCriteriaBuilder((cb, query, root) -> {
String providerId = new StorageId((String) value[0], "").getId() + "%";
return cb.like(root.join("consents", JoinType.LEFT).get("clientId"), providerId);
});
} else if (modelField == UserModel.SearchableFields.CONSENT_WITH_CLIENT_SCOPE) {
validateValue(value, modelField, op, String.class);
return new JpaUserModelCriteriaBuilder((cb, query, root) -> {
Join<JpaUserEntity, JpaUserConsentEntity> join = root.join("consents", JoinType.LEFT);
return cb.isTrue(cb.function("@>",
Boolean.TYPE,
cb.function("->", JsonbType.class, join.get("metadata"), cb.literal("fGrantedClientScopesIds")),
cb.literal(convertToJson(value[0]))));
});
} else if (modelField == UserModel.SearchableFields.ATTRIBUTE) {
validateValue(value, modelField, op, String.class, String.class);
return new JpaUserModelCriteriaBuilder((cb, query, root) -> {
Join<JpaUserEntity, JpaUserAttributeEntity> join = root.join("attributes", JoinType.LEFT);
return cb.and(
cb.equal(join.get("name"), value[0]),
cb.equal(join.get("value"), value[1])
);
});
}
else {
throw new CriterionNotSupportedException(modelField, op);
}
case ILIKE:
if (modelField == UserModel.SearchableFields.USERNAME ||
modelField == UserModel.SearchableFields.FIRST_NAME ||
modelField == UserModel.SearchableFields.LAST_NAME ||
modelField == UserModel.SearchableFields.EMAIL) {
validateValue(value, modelField, op, String.class);
return new JpaUserModelCriteriaBuilder((cb, query, root) ->
cb.like(cb.lower(root.get(modelField.getName())), value[0].toString().toLowerCase()));
} else {
throw new CriterionNotSupportedException(modelField, op);
}
case IN:
if (modelField == UserModel.SearchableFields.ASSIGNED_GROUP) {
final Collection<?> collectionValues = getValuesForInOperator(value, modelField);
if (collectionValues.isEmpty()) return new JpaUserModelCriteriaBuilder((cb, query, root) -> cb.or());
return new JpaUserModelCriteriaBuilder((cb, query, root) -> {
CriteriaBuilder.In<String> in = cb.in(root.join("groupIds", JoinType.LEFT));
collectionValues.forEach(groupId -> {
if (!(groupId instanceof String))
throw new CriterionNotSupportedException(modelField, op, "Invalid type. Expected String, got " + groupId.getClass());
in.value(groupId.toString());
});
return in;
});
} else {
throw new CriterionNotSupportedException(modelField, op);
}
case NOT_EXISTS:
if (modelField == UserModel.SearchableFields.SERVICE_ACCOUNT_CLIENT) {
return new JpaUserModelCriteriaBuilder((cb, query, root) ->
cb.isNull(cb.function("->>", String.class, root.get("metadata"), cb.literal("fServiceAccountClientLink"))));
} else {
throw new CriterionNotSupportedException(modelField, op);
}
default:
throw new CriterionNotSupportedException(modelField, op);
}
}
}

View file

@ -0,0 +1,100 @@
/*
* 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.user.delegate;
import java.util.UUID;
import javax.persistence.EntityManager;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.JoinType;
import javax.persistence.criteria.Root;
import org.keycloak.models.map.common.EntityField;
import org.keycloak.models.map.common.delegate.DelegateProvider;
import org.keycloak.models.map.storage.jpa.JpaDelegateProvider;
import org.keycloak.models.map.storage.jpa.user.entity.JpaUserEntity;
import org.keycloak.models.map.user.MapUserEntity;
import org.keycloak.models.map.user.MapUserEntityFields;
/**
* A {@link DelegateProvider} implementation for {@link JpaUserEntity}.
*
* @author <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/
public class JpaUserDelegateProvider extends JpaDelegateProvider<JpaUserEntity> implements DelegateProvider<MapUserEntity> {
private final EntityManager em;
public JpaUserDelegateProvider(final JpaUserEntity delegate, final EntityManager em) {
super(delegate);
this.em = em;
}
@Override
public MapUserEntity getDelegate(boolean isRead, Enum<? extends EntityField<MapUserEntity>> field, Object... parameters) {
if (getDelegate().isMetadataInitialized()) return getDelegate();
if (isRead) {
if (field instanceof MapUserEntityFields) {
switch ((MapUserEntityFields) field) {
case ID:
case REALM_ID:
case USERNAME:
case FIRST_NAME:
case LAST_NAME:
case EMAIL:
case EMAIL_CONSTRAINT:
case FEDERATION_LINK:
case ENABLED:
case EMAIL_VERIFIED:
case CREATED_TIMESTAMP:
return getDelegate();
case ATTRIBUTES:
this.setDelegateWithAssociation("attributes");
break;
case USER_CONSENTS:
this.setDelegateWithAssociation("consents");
break;
case FEDERATED_IDENTITIES:
this.setDelegateWithAssociation("federatedIdentities");
break;
default:
setDelegate(em.find(JpaUserEntity.class, UUID.fromString(getDelegate().getId())));
}
} else {
throw new IllegalStateException("Not a valid realm field: " + field);
}
} else {
setDelegate(em.find(JpaUserEntity.class, UUID.fromString(getDelegate().getId())));
}
return getDelegate();
}
protected void setDelegateWithAssociation(final String associationName) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<JpaUserEntity> query = cb.createQuery(JpaUserEntity.class);
Root<JpaUserEntity> root = query.from(JpaUserEntity.class);
root.fetch(associationName, JoinType.LEFT);
query.select(root).where(cb.equal(root.get("id"), UUID.fromString(getDelegate().getId())));
setDelegate(em.createQuery(query).getSingleResult());
}
}

View file

@ -0,0 +1,44 @@
/*
* 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.user.entity;
import javax.persistence.Entity;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
import org.keycloak.models.map.storage.jpa.JpaAttributeEntity;
/**
* JPA implementation for user attributes. This entity represents a user attribute and has a many-to-one relationship
* with the user entity.
*
* @author <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/
@Entity
@Table(name = "kc_user_attribute", uniqueConstraints = {
@UniqueConstraint(columnNames = {"fk_root", "name", "value"})
})
public class JpaUserAttributeEntity extends JpaAttributeEntity<JpaUserEntity> {
public JpaUserAttributeEntity() {
}
public JpaUserAttributeEntity(final JpaUserEntity root, final String name, final String value) {
super(root, name, value);
}
}

View file

@ -0,0 +1,161 @@
/*
* 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.user.entity;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import javax.persistence.Basic;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
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.UpdatableEntity;
import org.keycloak.models.map.storage.jpa.JpaChildEntity;
import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType;
import org.keycloak.models.map.user.MapUserConsentEntity;
/**
* JPA {@link MapUserConsentEntity} 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_user_consent",
uniqueConstraints = {
@UniqueConstraint(columnNames = {"clientId"})
})
@TypeDefs({@TypeDef(name = "jsonb", typeClass = JsonbType.class)})
public class JpaUserConsentEntity extends UpdatableEntity.Impl implements MapUserConsentEntity, JpaChildEntity<JpaUserEntity> {
@Id
@Column
@GeneratedValue
private UUID id;
@Column(insertable = false, updatable = false)
@Basic(fetch = FetchType.LAZY)
private String clientId;
@Type(type = "jsonb")
@Column(columnDefinition = "jsonb")
private final JpaUserConsentMetadata metadata;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name="fk_root")
private JpaUserEntity root;
public JpaUserConsentEntity() {
this.metadata = new JpaUserConsentMetadata();
}
public JpaUserConsentEntity(final DeepCloner cloner) {
this.metadata = new JpaUserConsentMetadata(cloner);
}
public Integer getEntityVersion() {
return this.metadata.getEntityVersion();
}
public void setEntityVersion(Integer version) {
this.metadata.setEntityVersion(version);
}
@Override
public JpaUserEntity getParent() {
return this.root;
}
public void setParent(final JpaUserEntity root) {
this.root = root;
}
@Override
public String getClientId() {
return this.metadata.getClientId();
}
@Override
public void setClientId(String clientId) {
this.metadata.setClientId(clientId);
}
@Override
public Set<String> getGrantedClientScopesIds() {
return this.metadata.getGrantedClientScopesIds();
}
@Override
public void addGrantedClientScopesId(String scope) {
this.metadata.addGrantedClientScopesId(scope);
}
@Override
public void setGrantedClientScopesIds(Set<String> scopesIds) {
this.metadata.setGrantedClientScopesIds(scopesIds);
}
@Override
public void removeGrantedClientScopesId(String scopes) {
this.metadata.removeGrantedClientScopesId(scopes);
}
@Override
public Long getCreatedDate() {
return this.metadata.getCreatedDate();
}
@Override
public void setCreatedDate(Long createdDate) {
this.metadata.setCreatedDate(createdDate);
}
@Override
public Long getLastUpdatedDate() {
return this.metadata.getLastUpdatedDate();
}
@Override
public void setLastUpdatedDate(Long lastUpdatedDate) {
this.metadata.setLastUpdatedDate(lastUpdatedDate);
}
@Override
public int hashCode() {
return getClass().hashCode();
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof JpaUserConsentEntity)) return false;
return Objects.equals(id, ((JpaUserConsentEntity) obj).id);
}
}

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.user.entity;
import java.io.Serializable;
import org.keycloak.models.map.common.DeepCloner;
import org.keycloak.models.map.user.MapUserConsentEntityImpl;
/**
* Class that contains all the user consent metadata that is written as JSON into the database.
*
* @author <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/
public class JpaUserConsentMetadata extends MapUserConsentEntityImpl implements Serializable {
public JpaUserConsentMetadata() {
super();
}
public JpaUserConsentMetadata(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,551 @@
/*
* 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.user.entity;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
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.CollectionTable;
import javax.persistence.Column;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
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.storage.jpa.JpaRootVersionedEntity;
import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType;
import org.keycloak.models.map.user.MapUserConsentEntity;
import org.keycloak.models.map.user.MapUserCredentialEntity;
import org.keycloak.models.map.user.MapUserEntity;
import org.keycloak.models.map.user.MapUserFederatedIdentityEntity;
import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_USER;
import static org.keycloak.models.map.storage.jpa.JpaMapStorageProviderFactory.CLONER;
/**
* JPA {@link MapUserEntity} 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_user",
uniqueConstraints = {
@UniqueConstraint(columnNames = {"realmId", "username"}),
@UniqueConstraint(columnNames = {"realmId", "emailConstraint"})
})
@TypeDefs({@TypeDef(name = "jsonb", typeClass = JsonbType.class)})
@SuppressWarnings("ConstantConditions")
public class JpaUserEntity extends MapUserEntity.AbstractUserEntity 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 JpaUserMetadata metadata;
@Column(insertable = false, updatable = false)
@Basic(fetch = FetchType.LAZY)
private Integer entityVersion;
@Column(insertable = false, updatable = false)
@Basic(fetch = FetchType.LAZY)
private String realmId;
@Column(insertable = false, updatable = false)
@Basic(fetch = FetchType.LAZY)
private String username;
@Column(insertable = false, updatable = false)
@Basic(fetch = FetchType.LAZY)
private String firstName;
@Column(insertable = false, updatable = false)
@Basic(fetch = FetchType.LAZY)
private String lastName;
@Column(insertable = false, updatable = false)
@Basic(fetch = FetchType.LAZY)
private String email;
@Column(insertable = false, updatable = false)
@Basic(fetch = FetchType.LAZY)
private String emailConstraint;
@Column(insertable = false, updatable = false)
@Basic(fetch = FetchType.LAZY)
private String federationLink;
@Column(insertable = false, updatable = false)
@Basic(fetch = FetchType.LAZY)
private Boolean enabled;
@Column(insertable = false, updatable = false)
@Basic(fetch = FetchType.LAZY)
private Boolean emailVerified;
@Column(insertable = false, updatable = false)
@Basic(fetch = FetchType.LAZY)
private Long timestamp;
@Column(name = "group_id")
@ElementCollection
@CollectionTable(name = "kc_user_group", joinColumns = @JoinColumn(name = "user_id", nullable = false))
private final Set<String> groupIds = new HashSet<>();
@Column(name = "role_id")
@ElementCollection
@CollectionTable(name = "kc_user_role", joinColumns = @JoinColumn(name = "user_id", nullable = false))
private final Set<String> roleIds = new HashSet<>();
@OneToMany(mappedBy = "root", cascade = CascadeType.PERSIST, orphanRemoval = true)
private final Set<JpaUserAttributeEntity> attributes = new HashSet<>();
@OneToMany(mappedBy = "root", cascade = CascadeType.PERSIST, orphanRemoval = true)
private final Set<JpaUserConsentEntity> consents = new HashSet<>();
@OneToMany(mappedBy = "root", cascade = CascadeType.PERSIST, orphanRemoval = true)
private final Set<JpaUserFederatedIdentityEntity> federatedIdentities = new HashSet<>();
/**
* No-argument constructor, used by hibernate to instantiate entities.
*/
public JpaUserEntity() {
this.metadata = new JpaUserMetadata();
}
public JpaUserEntity(final DeepCloner cloner) {
this.metadata = new JpaUserMetadata(cloner);
}
/**
* Used by hibernate when calling cb.construct from read(QueryParameters) method.
* It is used to select user without metadata(json) field.
*/
public JpaUserEntity(final UUID id, final int version, final Integer entityVersion, final String realmId, final String username,
final String firstName, final String lastName, final String email, final String emailConstraint,
final String federationLink, final Boolean enabled, final Boolean emailVerified, final Long timestamp) {
this.id = id;
this.version = version;
this.entityVersion = entityVersion;
this.realmId = realmId;
this.username = username;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.emailConstraint = emailConstraint;
this.federationLink = federationLink;
this.enabled = enabled;
this.emailVerified = emailVerified;
this.timestamp = timestamp;
this.metadata = null;
}
public boolean isMetadataInitialized() {
return this.metadata != null;
}
@Override
public Integer getEntityVersion() {
if (isMetadataInitialized()) return this.metadata.getEntityVersion();
return this.entityVersion;
}
@Override
public void setEntityVersion(Integer entityVersion) {
this.metadata.setEntityVersion(entityVersion);
}
@Override
public Integer getCurrentSchemaVersion() {
return CURRENT_SCHEMA_VERSION_USER;
}
@Override
public int getVersion() {
return this.version;
}
@Override
public String getId() {
return this.id == null ? null : this.id.toString();
}
@Override
public void setId(String id) {
String validatedId = UuidValidator.validateAndConvert(id);
this.id = UUID.fromString(validatedId);
}
@Override
public String getRealmId() {
if (this.isMetadataInitialized()) return this.metadata.getRealmId();
return this.realmId;
}
@Override
public void setRealmId(String realmId) {
this.metadata.setRealmId(realmId);
}
@Override
public String getUsername() {
if (this.isMetadataInitialized()) return this.metadata.getUsername();
return this.username;
}
@Override
public void setUsername(String username) {
this.metadata.setUsername(username);
}
@Override
public String getFirstName() {
if (this.isMetadataInitialized()) return this.metadata.getFirstName();
return this.firstName;
}
@Override
public void setFirstName(String firstName) {
this.metadata.setFirstName(firstName);
}
@Override
public Long getCreatedTimestamp() {
if (this.isMetadataInitialized()) return this.metadata.getCreatedTimestamp();
return this.timestamp;
}
@Override
public void setCreatedTimestamp(Long createdTimestamp) {
this.metadata.setCreatedTimestamp(createdTimestamp);
}
@Override
public String getLastName() {
if (this.isMetadataInitialized()) return this.metadata.getLastName();
return this.lastName;
}
@Override
public void setLastName(String lastName) {
this.metadata.setLastName(lastName);
}
@Override
public String getEmail() {
if (this.isMetadataInitialized()) return this.metadata.getEmail();
return this.email;
}
@Override
public void setEmail(String email) {
this.metadata.setEmail(email);
}
@Override
public Boolean isEnabled() {
if (this.isMetadataInitialized()) return this.metadata.isEnabled();
return this.enabled;
}
@Override
public void setEnabled(Boolean enabled) {
this.metadata.setEnabled(enabled);
}
@Override
public Boolean isEmailVerified() {
if (this.isMetadataInitialized()) return this.metadata.isEmailVerified();
return this.emailVerified;
}
@Override
public void setEmailVerified(Boolean emailVerified) {
this.metadata.setEmailVerified(emailVerified);
}
@Override
public String getEmailConstraint() {
if (this.isMetadataInitialized()) return this.metadata.getEmailConstraint();
return this.emailConstraint;
}
@Override
public void setEmailConstraint(String emailConstraint) {
this.metadata.setEmailConstraint(emailConstraint);
}
@Override
public String getFederationLink() {
if (this.isMetadataInitialized()) return this.metadata.getFederationLink();
return this.federationLink;
}
@Override
public void setFederationLink(String federationLink) {
this.metadata.setFederationLink(federationLink);
}
@Override
public String getServiceAccountClientLink() {
return this.metadata.getServiceAccountClientLink();
}
@Override
public void setServiceAccountClientLink(String serviceAccountClientLink) {
this.metadata.setServiceAccountClientLink(serviceAccountClientLink);
}
@Override
public Long getNotBefore() {
return this.metadata.getNotBefore();
}
@Override
public void setNotBefore(Long notBefore) {
this.metadata.setNotBefore(notBefore);
}
//groups membership
@Override
public Set<String> getGroupsMembership() {
return this.groupIds;
}
@Override
public void setGroupsMembership(Set<String> groupsMembership) {
this.groupIds.clear();
if (groupsMembership != null) this.groupIds.addAll(groupsMembership);
}
@Override
public void addGroupsMembership(String groupId) {
this.groupIds.add(groupId);
}
@Override
public void removeGroupsMembership(String groupId) {
this.groupIds.remove(groupId);
}
//roles membership
@Override
public Set<String> getRolesMembership() {
return this.roleIds;
}
@Override
public void setRolesMembership(Set<String> rolesMembership) {
this.roleIds.clear();
if (rolesMembership != null) this.roleIds.addAll(rolesMembership);
}
@Override
public void addRolesMembership(String roleId) {
this.roleIds.add(roleId);
}
@Override
public void removeRolesMembership(String roleId) {
this.roleIds.remove(roleId);
}
//user required actions
@Override
public Set<String> getRequiredActions() {
return this.metadata.getRequiredActions();
}
@Override
public void setRequiredActions(Set<String> requiredActions) {
this.metadata.setRequiredActions(requiredActions);
}
@Override
public void addRequiredAction(String requiredAction) {
this.metadata.addRequiredAction(requiredAction);
}
@Override
public void removeRequiredAction(String requiredAction) {
this.metadata.removeRequiredAction(requiredAction);
}
//user attributes
@Override
public Map<String, List<String>> getAttributes() {
Map<String, List<String>> result = new HashMap<>();
for (JpaUserAttributeEntity attribute : this.attributes) {
List<String> values = result.getOrDefault(attribute.getName(), new LinkedList<>());
values.add(attribute.getValue());
result.put(attribute.getName(), values);
}
return result;
}
@Override
public void setAttributes(Map<String, List<String>> attributes) {
this.attributes.clear();
if (attributes != null) {
attributes.forEach(this::setAttribute);
}
}
@Override
public List<String> getAttribute(String name) {
return this.attributes.stream().filter(a -> Objects.equals(a.getName(), name))
.map(JpaUserAttributeEntity::getValue)
.collect(Collectors.toList());
}
@Override
public void setAttribute(String name, List<String> values) {
this.removeAttribute(name);
if (values != null) {
values.forEach(value -> this.attributes.add(new JpaUserAttributeEntity(this, name, value)));
}
}
@Override
public void removeAttribute(String name) {
this.attributes.removeIf(attr -> Objects.equals(attr.getName(), name));
}
//user consents
@Override
public Set<MapUserConsentEntity> getUserConsents() {
return this.consents.stream().map(MapUserConsentEntity.class::cast).collect(Collectors.toSet());
}
@Override
public void setUserConsents(Set<MapUserConsentEntity> userConsents) {
this.consents.clear();
if (userConsents != null) {
userConsents.forEach(this::addUserConsent);
}
}
@Override
public void addUserConsent(MapUserConsentEntity userConsentEntity) {
JpaUserConsentEntity entity = (JpaUserConsentEntity) CLONER.from(userConsentEntity);
entity.setParent(this);
entity.setEntityVersion(this.getEntityVersion());
this.consents.add(entity);
}
@Override
public Boolean removeUserConsent(MapUserConsentEntity userConsentEntity) {
return this.consents.removeIf(uc -> Objects.equals(uc.getClientId(), userConsentEntity.getClientId()));
}
@Override
public Boolean removeUserConsent(String clientId) {
return this.consents.removeIf(uc -> Objects.equals(uc.getClientId(), clientId));
}
//user credentials
@Override
public List<MapUserCredentialEntity> getCredentials() {
return this.metadata.getCredentials();
}
@Override
public void setCredentials(List<MapUserCredentialEntity> credentials) {
this.metadata.setCredentials(credentials);
}
@Override
public void addCredential(MapUserCredentialEntity credentialEntity) {
this.metadata.addCredential(credentialEntity);
}
@Override
public Boolean removeCredential(MapUserCredentialEntity credentialEntity) {
return super.removeCredential(credentialEntity.getId());
}
//user federated identities
@Override
public Set<MapUserFederatedIdentityEntity> getFederatedIdentities() {
return this.federatedIdentities.stream().map(MapUserFederatedIdentityEntity.class::cast).collect(Collectors.toSet());
}
@Override
public void setFederatedIdentities(Set<MapUserFederatedIdentityEntity> federatedIdentities) {
this.federatedIdentities.clear();
if (federatedIdentities != null) {
federatedIdentities.forEach(this::addFederatedIdentity);
}
}
@Override
public void addFederatedIdentity(MapUserFederatedIdentityEntity federatedIdentity) {
JpaUserFederatedIdentityEntity entity = (JpaUserFederatedIdentityEntity) CLONER.from(federatedIdentity);
entity.setParent(this);
entity.setEntityVersion(this.getEntityVersion());
this.federatedIdentities.add(entity);
}
@Override
public Boolean removeFederatedIdentity(MapUserFederatedIdentityEntity federatedIdentity) {
return this.federatedIdentities.removeIf(fi -> Objects.equals(fi.getIdentityProvider(), federatedIdentity.getIdentityProvider()));
}
@Override
public Boolean removeFederatedIdentity(String identityProviderId) {
return this.federatedIdentities.removeIf(fi -> Objects.equals(fi.getIdentityProvider(), identityProviderId));
}
@Override
public int hashCode() {
return getClass().hashCode();
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof JpaUserEntity)) return false;
return Objects.equals(getId(), ((JpaUserEntity) obj).getId());
}
}

View file

@ -0,0 +1,149 @@
/*
* 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.user.entity;
import java.util.Objects;
import java.util.UUID;
import javax.persistence.Basic;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
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.UpdatableEntity;
import org.keycloak.models.map.storage.jpa.JpaChildEntity;
import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType;
import org.keycloak.models.map.user.MapUserFederatedIdentityEntity;
/**
* JPA {@link MapUserFederatedIdentityEntity} 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_user_federated_identity")
@TypeDefs({@TypeDef(name = "jsonb", typeClass = JsonbType.class)})
public class JpaUserFederatedIdentityEntity extends UpdatableEntity.Impl implements MapUserFederatedIdentityEntity, JpaChildEntity<JpaUserEntity> {
@Id
@Column
@GeneratedValue
private UUID id;
@Column(insertable = false, updatable = false)
@Basic(fetch = FetchType.LAZY)
private String identityProvider;
@Column(insertable = false, updatable = false)
@Basic(fetch = FetchType.LAZY)
private String userId;
@Type(type = "jsonb")
@Column(columnDefinition = "jsonb")
private final JpaUserFederatedIdentityMetadata metadata;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name="fk_root")
private JpaUserEntity root;
public JpaUserFederatedIdentityEntity() {
this.metadata = new JpaUserFederatedIdentityMetadata();
}
public JpaUserFederatedIdentityEntity(final DeepCloner cloner) {
this.metadata = new JpaUserFederatedIdentityMetadata(cloner);
}
public Integer getEntityVersion() {
return this.metadata.getEntityVersion();
}
public void setEntityVersion(Integer version) {
this.metadata.setEntityVersion(version);
}
@Override
public JpaUserEntity getParent() {
return this.root;
}
public void setParent(final JpaUserEntity root) {
this.root = root;
}
@Override
public String getToken() {
return this.metadata.getToken();
}
@Override
public void setToken(String token) {
this.metadata.setToken(token);
}
@Override
public String getUserId() {
return this.metadata.getUserId();
}
@Override
public void setUserId(String userId) {
this.metadata.setUserId(userId);
}
@Override
public String getIdentityProvider() {
return this.metadata.getIdentityProvider();
}
@Override
public void setIdentityProvider(String identityProvider) {
this.metadata.setIdentityProvider(identityProvider);
}
@Override
public String getUserName() {
return this.metadata.getUserName();
}
@Override
public void setUserName(String userName) {
this.metadata.setUserName(userName);
}
@Override
public int hashCode() {
return getClass().hashCode();
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof JpaUserFederatedIdentityEntity)) return false;
return Objects.equals(id, ((JpaUserFederatedIdentityEntity) obj).id);
}
}

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.user.entity;
import java.io.Serializable;
import org.keycloak.models.map.common.DeepCloner;
import org.keycloak.models.map.user.MapUserFederatedIdentityEntityImpl;
/**
* Class that contains all the user federated identity metadata that is written as JSON into the database.
*
* @author <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/
public class JpaUserFederatedIdentityMetadata extends MapUserFederatedIdentityEntityImpl implements Serializable {
public JpaUserFederatedIdentityMetadata() {
super();
}
public JpaUserFederatedIdentityMetadata(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,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.user.entity;
import java.io.Serializable;
import org.keycloak.models.map.common.DeepCloner;
import org.keycloak.models.map.user.MapUserEntityImpl;
/**
* Class that contains all the user metadata that is written as JSON into the database.
*
* @author <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/
public class JpaUserMetadata extends MapUserEntityImpl implements Serializable {
public JpaUserMetadata() {
super();
}
public JpaUserMetadata(final DeepCloner cloner) {
super(cloner);
}
private Integer entityVersion;
public Integer getEntityVersion() {
return entityVersion;
}
public void setEntityVersion(Integer entityVersion) {
this.entityVersion = entityVersion;
}
}

View file

@ -29,4 +29,5 @@ limitations under the License.
<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"/>
<include file="META-INF/jpa-users-changelog.xml"/>
</databaseChangeLog>

View file

@ -0,0 +1,22 @@
<?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">
<include file="META-INF/users/jpa-users-changelog-1.xml"/>
</databaseChangeLog>

View file

@ -38,5 +38,10 @@
<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>
<!--users-->
<class>org.keycloak.models.map.storage.jpa.user.entity.JpaUserEntity</class>
<class>org.keycloak.models.map.storage.jpa.user.entity.JpaUserAttributeEntity</class>
<class>org.keycloak.models.map.storage.jpa.user.entity.JpaUserConsentEntity</class>
<class>org.keycloak.models.map.storage.jpa.user.entity.JpaUserFederatedIdentityEntity</class>
</persistence-unit>
</persistence>

View file

@ -0,0 +1,180 @@
<?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="users-1">
<createTable tableName="kc_user">
<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_user">
<ext:column name="entityversion" type="INTEGER" jsonColumn="metadata" jsonProperty="entityVersion"/>
<ext:column name="realmid" type="KC_KEY" jsonColumn="metadata" jsonProperty="fRealmId"/>
<ext:column name="username" type="VARCHAR(255)" jsonColumn="metadata" jsonProperty="fUsername"/>
<ext:column name="firstname" type="VARCHAR(255)" jsonColumn="metadata" jsonProperty="fFirstName"/>
<ext:column name="lastname" type="VARCHAR(255)" jsonColumn="metadata" jsonProperty="fLastName"/>
<ext:column name="email" type="VARCHAR(255)" jsonColumn="metadata" jsonProperty="fEmail"/>
<ext:column name="emailconstraint" type="VARCHAR(255)" jsonColumn="metadata" jsonProperty="fEmailConstraint"/>
<ext:column name="federationlink" type="VARCHAR(255)" jsonColumn="metadata" jsonProperty="fFederationLink"/>
<ext:column name="enabled" type="BOOLEAN" jsonColumn="metadata" jsonProperty="fEnabled"/>
<ext:column name="emailverified" type="BOOLEAN" jsonColumn="metadata" jsonProperty="fEmailVerified"/>
<ext:column name="timestamp" type="BIGINT" jsonColumn="metadata" jsonProperty="fCreatedTimestamp"/>
</ext:addGeneratedColumn>
<createIndex tableName="kc_user" indexName="user_entityVersion">
<column name="entityversion"/>
</createIndex>
<createIndex tableName="kc_user" indexName="user_realmId">
<column name="realmid"/>
</createIndex>
<createIndex tableName="kc_user" indexName="user_username_realmid">
<column name="username"/>
<column name="realmid"/>
</createIndex>
<createIndex tableName="kc_user" indexName="user_email_realmid">
<column name="email"/>
<column name="realmid"/>
</createIndex>
<createIndex tableName="kc_user" indexName="user_email_constraint_realmid">
<column name="emailconstraint"/>
<column name="realmid"/>
</createIndex>
<!-- TODO: create individual indexes for all other searchable columns (firstname, lastname, enabled, etc) ? -->
<ext:createJsonIndex tableName="kc_user" indexName="user_serviceAccountClientLink">
<ext:column jsonColumn="metadata" jsonProperty="fServiceAccountClientLink"/>
</ext:createJsonIndex>
<createTable tableName="kc_user_consent">
<column name="id" type="UUID">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="version" type="INTEGER" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="fk_root" type="UUID">
<constraints foreignKeyName="user_consent_fk_root_fkey" references="kc_user(id)" deleteCascade="true"/>
</column>
<column name="metadata" type="json"/>
</createTable>
<ext:addGeneratedColumn tableName="kc_user_consent">
<ext:column name="clientid" type="KC_KEY" jsonColumn="metadata" jsonProperty="fClientId"/>
</ext:addGeneratedColumn>
<createIndex tableName="kc_user_consent" indexName="user_consent_fk_root">
<column name="fk_root"/>
</createIndex>
<createIndex tableName="kc_user_consent" indexName="user_consent_clientId">
<column name="clientid"/>
</createIndex>
<ext:createJsonIndex tableName="kc_user_consent" indexName="user_consent_grantedClientScopes">
<ext:column jsonColumn="metadata" jsonProperty="fGrantedClientScopesIds"/>
</ext:createJsonIndex>
<createTable tableName="kc_user_federated_identity">
<column name="id" type="UUID">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="version" type="INTEGER" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="fk_root" type="UUID">
<constraints foreignKeyName="user_federated_identity_fk_root_fkey" references="kc_user(id)" deleteCascade="true"/>
</column>
<column name="metadata" type="json"/>
</createTable>
<ext:addGeneratedColumn tableName="kc_user_federated_identity">
<ext:column name="identityprovider" type="VARCHAR(255)" jsonColumn="metadata" jsonProperty="fIdentityProvider"/>
<ext:column name="userid" type="KC_KEY" jsonColumn="metadata" jsonProperty="fUserId"/>
</ext:addGeneratedColumn>
<createIndex tableName="kc_user_federated_identity" indexName="user_federated_identity_fk_root">
<column name="fk_root"/>
</createIndex>
<createIndex tableName="kc_user_federated_identity" indexName="user_federated_identity_idp">
<column name="identityprovider"/>
</createIndex>
<createIndex tableName="kc_user_federated_identity" indexName="user_federated_identity_userId">
<column name="userid"/>
</createIndex>
<createTable tableName="kc_user_group">
<column name="user_id" type="UUID">
<constraints nullable="false" foreignKeyName="user_group_userid_fkey" references="kc_user(id)" deleteCascade="true"/>
</column>
<column name="group_id" type="KC_KEY"/>
</createTable>
<createIndex tableName="kc_user_group" indexName="user_group_userId">
<column name="user_id"/>
</createIndex>
<createIndex tableName="kc_user_group" indexName="user_group_groupId">
<column name="group_id"/>
</createIndex>
<createTable tableName="kc_user_role">
<column name="user_id" type="UUID">
<constraints nullable="false" foreignKeyName="user_role_userid_fkey" references="kc_user(id)" deleteCascade="true"/>
</column>
<column name="role_id" type="KC_KEY"/>
</createTable>
<createIndex tableName="kc_user_role" indexName="user_role_userId">
<column name="user_id"/>
</createIndex>
<createIndex tableName="kc_user_role" indexName="user_role_roleId">
<column name="role_id"/>
</createIndex>
<createTable tableName="kc_user_attribute">
<column name="id" type="UUID">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="fk_root" type="UUID">
<constraints foreignKeyName="user_attr_fk_root_fkey" references="kc_user(id)" deleteCascade="true"/>
</column>
<column name="name" type="VARCHAR(255)"/>
<column name="value" type="TEXT"/>
</createTable>
<createIndex tableName="kc_user_attribute" indexName="user_attr_fk_root">
<column name="fk_root"/>
</createIndex>
<createIndex tableName="kc_user_attribute" indexName="user_attr_name_value">
<column name="name"/>
<column name="VALUE(255)" valueComputed="VALUE(255)"/>
</createIndex>
<modifySql dbms="postgresql,cockroachdb">
<replace replace="VALUE(255)" with="(value::varchar(250))"/>
</modifySql>
</changeSet>
<changeSet author="keycloak" id="users-2" dbms="postgresql">
<!-- this is deferrable and initiallyDeferred as hibernate will first insert new entries and then delete the old by default -->
<!-- this will not work on cockroachdb as deferred indexes are not supported in version 22.1 yet, therefore, only run it on postgresql -->
<!-- see https://go.crdb.dev/issue-v/31632/v21.2 for the current status of the implementation -->
<addUniqueConstraint tableName="kc_user_group" columnNames="user_id, group_id" deferrable="true" initiallyDeferred="true" />
<addUniqueConstraint tableName="kc_user_role" columnNames="user_id, role_id" deferrable="true" initiallyDeferred="true" />
<addUniqueConstraint tableName="kc_user_attribute" columnNames="fk_root, name, value" deferrable="true" initiallyDeferred="true" />
</changeSet>
</databaseChangeLog>

View file

@ -900,6 +900,7 @@
<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>
<keycloak.user.map.storage.provider>jpa</keycloak.user.map.storage.provider>
</systemPropertyVariables>
</configuration>
</plugin>

View file

@ -26,6 +26,7 @@ import org.jboss.arquillian.core.api.Instance;
import org.jboss.arquillian.core.api.InstanceProducer;
import org.jboss.arquillian.core.api.annotation.Inject;
import org.jboss.arquillian.test.spi.TestResult;
import org.keycloak.common.Profile;
import org.keycloak.common.util.reflections.Reflections;
import org.keycloak.testsuite.arquillian.annotation.ModelTest;
import org.keycloak.testsuite.client.KeycloakTestingClient;
@ -49,19 +50,23 @@ public class ModelTestExecutor extends LocalTestExecuter {
super.execute(event);
} else {
TestResult result = new TestResult();
if (annotation.skipForMapStorage() && Profile.isFeatureEnabled(Profile.Feature.MAP_STORAGE)) {
result = TestResult.skipped();
}
else {
try {
// Model test - wrap the call inside the
TestContext ctx = testContext.get();
KeycloakTestingClient testingClient = ctx.getTestingClient();
testingClient.server().runModelTest(testMethod.getDeclaringClass().getName(), testMethod.getName());
try {
// Model test - wrap the call inside the
TestContext ctx = testContext.get();
KeycloakTestingClient testingClient = ctx.getTestingClient();
testingClient.server().runModelTest(testMethod.getDeclaringClass().getName(), testMethod.getName());
result.setStatus(TestResult.Status.PASSED);
} catch (Throwable e) {
result.setStatus(TestResult.Status.FAILED);
result.setThrowable(e);
} finally {
result.setEnd(System.currentTimeMillis());
result.setStatus(TestResult.Status.PASSED);
} catch (Throwable e) {
result.setStatus(TestResult.Status.FAILED);
result.setThrowable(e);
} finally {
result.setEnd(System.currentTimeMillis());
}
}
// Need to use reflection this way...

View file

@ -33,4 +33,6 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Retention(RUNTIME)
@Target({ElementType.METHOD}) // TODO: Maybe ElementClass.TYPE too? That way it will be possible to add the annotation on the the test class and not need to add on all the test methods inside the class
public @interface ModelTest {
boolean skipForMapStorage() default false;
}

View file

@ -33,12 +33,8 @@ public class BadRealmTest extends AbstractKeycloakTest {
}
@Test
@ModelTest
@ModelTest(skipForMapStorage = true) // when map storage is enabled, the id is always converted into a valid UUID.
public void testBadRealmId(KeycloakSession session) {
if (Profile.isFeatureEnabled(Profile.Feature.MAP_STORAGE)) {
// when map storage is enabled, the id is always converted into a valid UUID.
return;
}
RealmManager manager = new RealmManager(session);
try {
manager.createRealm(id + script, name);

View file

@ -196,7 +196,7 @@ public class ConcurrentTransactionsTest extends AbstractTestRealmKeycloakTest {
// KEYCLOAK-3296 , KEYCLOAK-3494
@Test
@ModelTest
@ModelTest(skipForMapStorage = true) // skipped for map storage - to be revisited (GHI #12910)
public void removeUserAttribute(KeycloakSession session) throws Exception {
try {

View file

@ -184,7 +184,7 @@ public class OIDCProtocolMappersTest extends AbstractKeycloakTest {
user.singleAttribute("formatted", "6 Foo Street");
user.singleAttribute("phone", "617-777-6666");
user.singleAttribute("json-attribute", "{\"a\": 1, \"b\": 2, \"c\": [{\"a\": 1, \"b\": 2}], \"d\": {\"a\": 1, \"b\": 2}}");
user.getAttributes().put("json-attribute-multi", Arrays.asList("{\"a\": 1, \"b\": 2, \"c\": [{\"a\": 1, \"b\": 2}], \"d\": {\"a\": 1, \"b\": 2}}", "{\"a\": 1, \"b\": 2, \"c\": [{\"a\": 1, \"b\": 2}], \"d\": {\"a\": 1, \"b\": 2}}"));
user.getAttributes().put("json-attribute-multi", Arrays.asList("{\"a\": 1, \"b\": 2, \"c\": [{\"a\": 1, \"b\": 2}], \"d\": {\"a\": 1, \"b\": 2}}", "{\"a\": 3, \"b\": 4, \"c\": [{\"a\": 1, \"b\": 2}], \"d\": {\"a\": 1, \"b\": 2}}"));
List<String> departments = Arrays.asList("finance", "development");
user.getAttributes().put("departments", departments);

View file

@ -88,16 +88,16 @@ public class JpaMapStorage extends KeycloakModelParameters {
.config("driverDialect", "org.keycloak.models.map.storage.jpa.hibernate.dialect.JsonbPostgreSQL95Dialect");
cf.spi(AuthenticationSessionSpi.PROVIDER_ID).provider(MapRootAuthenticationSessionProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, JpaMapStorageProviderFactory.PROVIDER_ID)
.spi("client").provider(MapClientProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, JpaMapStorageProviderFactory.PROVIDER_ID)
.spi("clientScope").provider(MapClientScopeProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, JpaMapStorageProviderFactory.PROVIDER_ID)
.spi("group").provider(MapGroupProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, JpaMapStorageProviderFactory.PROVIDER_ID)
.spi("realm").provider(MapRealmProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, JpaMapStorageProviderFactory.PROVIDER_ID)
.spi("role").provider(MapRoleProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, JpaMapStorageProviderFactory.PROVIDER_ID)
.spi("client").provider(MapClientProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, JpaMapStorageProviderFactory.PROVIDER_ID)
.spi("clientScope").provider(MapClientScopeProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, JpaMapStorageProviderFactory.PROVIDER_ID)
.spi("group").provider(MapGroupProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, JpaMapStorageProviderFactory.PROVIDER_ID)
.spi("realm").provider(MapRealmProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, JpaMapStorageProviderFactory.PROVIDER_ID)
.spi("role").provider(MapRoleProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, JpaMapStorageProviderFactory.PROVIDER_ID)
.spi(DeploymentStateSpi.NAME).provider(MapDeploymentStateProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
.spi(StoreFactorySpi.NAME).provider(MapAuthorizationStoreFactory.PROVIDER_ID) .config(STORAGE_CONFIG, JpaMapStorageProviderFactory.PROVIDER_ID)
.spi("user").provider(MapUserProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
.spi("user").provider(MapUserProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, JpaMapStorageProviderFactory.PROVIDER_ID)
.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("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, JpaMapStorageProviderFactory.PROVIDER_ID)
.spi("publicKeyStorage").provider(MapPublicKeyStorageProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)