From 0c7a8c868459e02d59df731fa8eb4fa666c9600b Mon Sep 17 00:00:00 2001 From: Stefan Guilhen Date: Wed, 6 Apr 2022 10:47:03 -0300 Subject: [PATCH] Login Failures Map JPA implementation Closes #9664 --- .../models/map/storage/jpa/Constants.java | 1 + .../storage/jpa/JpaMapStorageProvider.java | 8 + .../jpa/JpaMapStorageProviderFactory.java | 8 +- .../hibernate/jsonb/JpaEntityMigration.java | 23 +- .../JpaUserLoginFailureMigration.java | 35 +++ ...serLoginFailureMapKeycloakTransaction.java | 72 ++++++ ...aUserLoginFailureModelCriteriaBuilder.java | 65 ++++++ .../JpaUserLoginFailureDelegateProvider.java | 65 ++++++ .../entity/JpaUserLoginFailureEntity.java | 216 ++++++++++++++++++ .../entity/JpaUserLoginFailureMetadata.java | 48 ++++ .../jpa-user-login-failures-changelog.xml | 23 ++ .../main/resources/META-INF/persistence.xml | 2 + .../jpa-user-login-failures-changelog-1.xml | 51 +++++ 13 files changed, 607 insertions(+), 10 deletions(-) create mode 100644 model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/migration/JpaUserLoginFailureMigration.java create mode 100644 model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/loginFailure/JpaUserLoginFailureMapKeycloakTransaction.java create mode 100644 model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/loginFailure/JpaUserLoginFailureModelCriteriaBuilder.java create mode 100644 model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/loginFailure/delegate/JpaUserLoginFailureDelegateProvider.java create mode 100644 model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/loginFailure/entity/JpaUserLoginFailureEntity.java create mode 100644 model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/loginFailure/entity/JpaUserLoginFailureMetadata.java create mode 100644 model/map-jpa/src/main/resources/META-INF/jpa-user-login-failures-changelog.xml create mode 100644 model/map-jpa/src/main/resources/META-INF/user-login-failures/jpa-user-login-failures-changelog-1.xml diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/Constants.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/Constants.java index 8dcf0dfc3e..499d9e98a8 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/Constants.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/Constants.java @@ -23,4 +23,5 @@ public interface Constants { public static final Integer CURRENT_SCHEMA_VERSION_GROUP = 1; 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_USER_LOGIN_FAILURE = 1; } diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaMapStorageProvider.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaMapStorageProvider.java index 367a06b1d8..3ac6aad121 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaMapStorageProvider.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaMapStorageProvider.java @@ -17,7 +17,10 @@ package org.keycloak.models.map.storage.jpa; import javax.persistence.EntityManager; + +import org.jboss.logging.Logger; import org.keycloak.models.KeycloakSession; +import org.keycloak.models.UserLoginFailureModel; import org.keycloak.models.map.common.AbstractEntity; import org.keycloak.models.map.storage.MapKeycloakTransaction; import org.keycloak.models.map.storage.MapStorage; @@ -26,6 +29,8 @@ import org.keycloak.models.map.storage.MapStorageProviderFactory.Flag; public class JpaMapStorageProvider implements MapStorageProvider { + private static final Logger logger = Logger.getLogger(JpaMapStorageProvider.class); + private final String SESSION_TX_PREFIX = "jpa-map-tx-"; private final JpaMapStorageProviderFactory factory; @@ -46,6 +51,9 @@ public class JpaMapStorageProvider implements MapStorageProvider { @Override @SuppressWarnings("unchecked") public MapStorage getStorage(Class modelType, Flag... flags) { + if (modelType == UserLoginFailureModel.class) { + logger.warn("Enabling JPA storage for user login failures will result in testsuite failures until GHI #11230 is resolved"); + } factory.validateAndUpdateSchema(session, modelType); return new MapStorage() { @Override diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaMapStorageProviderFactory.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaMapStorageProviderFactory.java index 0a36f4f485..197997c3b0 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaMapStorageProviderFactory.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaMapStorageProviderFactory.java @@ -56,6 +56,7 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleModel; +import org.keycloak.models.UserLoginFailureModel; import org.keycloak.models.dblock.DBLockProvider; import org.keycloak.models.map.client.MapProtocolMapperEntity; import org.keycloak.models.map.client.MapProtocolMapperEntityImpl; @@ -94,6 +95,8 @@ import org.keycloak.models.map.storage.jpa.group.JpaGroupMapKeycloakTransaction; import org.keycloak.models.map.storage.jpa.group.entity.JpaGroupEntity; import org.keycloak.models.map.storage.jpa.hibernate.listeners.JpaEntityVersionListener; import org.keycloak.models.map.storage.jpa.hibernate.listeners.JpaOptimisticLockingListener; +import org.keycloak.models.map.storage.jpa.loginFailure.JpaUserLoginFailureMapKeycloakTransaction; +import org.keycloak.models.map.storage.jpa.loginFailure.entity.JpaUserLoginFailureEntity; import org.keycloak.models.map.storage.jpa.realm.JpaRealmMapKeycloakTransaction; import org.keycloak.models.map.storage.jpa.realm.entity.JpaComponentEntity; import org.keycloak.models.map.storage.jpa.realm.entity.JpaRealmEntity; @@ -129,7 +132,7 @@ public class JpaMapStorageProviderFactory implements .constructor(JpaClientScopeEntity.class, JpaClientScopeEntity::new) //group .constructor(JpaGroupEntity.class, JpaGroupEntity::new) - // realm + //realm .constructor(JpaRealmEntity.class, JpaRealmEntity::new) .constructor(JpaComponentEntity.class, JpaComponentEntity::new) .constructor(MapAuthenticationExecutionEntity.class, MapAuthenticationExecutionEntityImpl::new) @@ -144,6 +147,8 @@ public class JpaMapStorageProviderFactory implements .constructor(MapWebAuthnPolicyEntity.class, MapWebAuthnPolicyEntityImpl::new) //role .constructor(JpaRoleEntity.class, JpaRoleEntity::new) + //user login-failure + .constructor(JpaUserLoginFailureEntity.class, JpaUserLoginFailureEntity::new) .build(); private static final Map, Function> MODEL_TO_TX = new HashMap<>(); @@ -154,6 +159,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(UserLoginFailureModel.class, JpaUserLoginFailureMapKeycloakTransaction::new); } public MapKeycloakTransaction createTransaction(Class modelType, EntityManager em) { diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/JpaEntityMigration.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/JpaEntityMigration.java index afcdd6c814..07acca3ae0 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/JpaEntityMigration.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/JpaEntityMigration.java @@ -16,12 +16,13 @@ */ package org.keycloak.models.map.storage.jpa.hibernate.jsonb; -import com.fasterxml.jackson.databind.node.ObjectNode; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.BiFunction; import java.util.function.Function; + +import com.fasterxml.jackson.databind.node.ObjectNode; import org.keycloak.models.map.storage.jpa.authSession.entity.JpaAuthenticationSessionMetadata; import org.keycloak.models.map.storage.jpa.authSession.entity.JpaRootAuthenticationSessionMetadata; import org.keycloak.models.map.storage.jpa.client.entity.JpaClientMetadata; @@ -35,6 +36,8 @@ import org.keycloak.models.map.storage.jpa.hibernate.jsonb.migration.JpaGroupMig import org.keycloak.models.map.storage.jpa.hibernate.jsonb.migration.JpaRealmMigration; 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.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; @@ -45,19 +48,21 @@ 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_USER_LOGIN_FAILURE; public class JpaEntityMigration { static final Map, BiFunction> MIGRATIONS = new HashMap<>(); static { - 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(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(JpaUserLoginFailureMetadata.class, (tree, entityVersion) -> migrateTreeTo(entityVersion, CURRENT_SCHEMA_VERSION_USER_LOGIN_FAILURE,tree, JpaUserLoginFailureMigration.MIGRATORS)); } private static ObjectNode migrateTreeTo(int entityVersion, Integer supportedVersion, ObjectNode node, List> migrators) { diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/migration/JpaUserLoginFailureMigration.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/migration/JpaUserLoginFailureMigration.java new file mode 100644 index 0000000000..839432cd08 --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/migration/JpaUserLoginFailureMigration.java @@ -0,0 +1,35 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.models.map.storage.jpa.hibernate.jsonb.migration; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; + +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * Migration functions for user login failures. + * + * @author Stefan Guilhen + */ +public class JpaUserLoginFailureMigration { + + public static final List> MIGRATORS = Arrays.asList( + o -> o // no migration yet + ); +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/loginFailure/JpaUserLoginFailureMapKeycloakTransaction.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/loginFailure/JpaUserLoginFailureMapKeycloakTransaction.java new file mode 100644 index 0000000000..72835353af --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/loginFailure/JpaUserLoginFailureMapKeycloakTransaction.java @@ -0,0 +1,72 @@ +/* + * 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.loginFailure; + +import javax.persistence.EntityManager; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.Root; +import javax.persistence.criteria.Selection; + +import org.keycloak.models.UserLoginFailureModel; +import org.keycloak.models.map.loginFailure.MapUserLoginFailureEntity; +import org.keycloak.models.map.loginFailure.MapUserLoginFailureEntityDelegate; +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.loginFailure.delegate.JpaUserLoginFailureDelegateProvider; +import org.keycloak.models.map.storage.jpa.loginFailure.entity.JpaUserLoginFailureEntity; + +import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_USER_LOGIN_FAILURE; + +/** + * A {@link JpaMapKeycloakTransaction} implementation for user login failure entities. + * + * @author Stefan Guilhen + */ +public class JpaUserLoginFailureMapKeycloakTransaction extends JpaMapKeycloakTransaction { + + @SuppressWarnings("unchecked") + public JpaUserLoginFailureMapKeycloakTransaction(EntityManager em) { + super(JpaUserLoginFailureEntity.class, em); + } + + @Override + public Selection selectCbConstruct(CriteriaBuilder cb, Root root) { + return cb.construct(JpaUserLoginFailureEntity.class, + root.get("id"), + root.get("version"), + root.get("entityVersion"), + root.get("realmId"), + root.get("userId") + ); + } + + @Override + public void setEntityVersion(JpaRootEntity entity) { + entity.setEntityVersion(CURRENT_SCHEMA_VERSION_USER_LOGIN_FAILURE); + } + + @Override + public JpaModelCriteriaBuilder createJpaModelCriteriaBuilder() { + return new JpaUserLoginFailureModelCriteriaBuilder(); + } + + @Override + protected MapUserLoginFailureEntity mapToEntityDelegate(JpaUserLoginFailureEntity original) { + return new MapUserLoginFailureEntityDelegate(new JpaUserLoginFailureDelegateProvider(original, em)); + } +} \ No newline at end of file diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/loginFailure/JpaUserLoginFailureModelCriteriaBuilder.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/loginFailure/JpaUserLoginFailureModelCriteriaBuilder.java new file mode 100644 index 0000000000..fd82ed530d --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/loginFailure/JpaUserLoginFailureModelCriteriaBuilder.java @@ -0,0 +1,65 @@ +/* + * 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.loginFailure; + +import java.util.function.BiFunction; + +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; + +import org.keycloak.models.UserLoginFailureModel; +import org.keycloak.models.map.storage.CriterionNotSupportedException; +import org.keycloak.models.map.storage.jpa.JpaModelCriteriaBuilder; +import org.keycloak.models.map.storage.jpa.loginFailure.entity.JpaUserLoginFailureEntity; +import org.keycloak.storage.SearchableModelField; + +/** + * A {@link JpaModelCriteriaBuilder} implementation for user login failures. + * + * @author Stefan Guilhen + */ +public class JpaUserLoginFailureModelCriteriaBuilder extends JpaModelCriteriaBuilder { + + public JpaUserLoginFailureModelCriteriaBuilder() { + super(JpaUserLoginFailureModelCriteriaBuilder::new); + } + + private JpaUserLoginFailureModelCriteriaBuilder(BiFunction, Predicate> predicateFunc) { + super(JpaUserLoginFailureModelCriteriaBuilder::new, predicateFunc); + } + + @Override + public JpaUserLoginFailureModelCriteriaBuilder compare(SearchableModelField modelField, Operator op, Object... value) { + switch (op) { + case EQ: + if (modelField.equals(UserLoginFailureModel.SearchableFields.REALM_ID) || + modelField.equals(UserLoginFailureModel.SearchableFields.USER_ID)) { + + validateValue(value, modelField, op, String.class); + + return new JpaUserLoginFailureModelCriteriaBuilder((cb, root) -> + cb.equal(root.get(modelField.getName()), value[0]) + ); + } else { + throw new CriterionNotSupportedException(modelField, op); + } + default: + throw new CriterionNotSupportedException(modelField, op); + } + } +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/loginFailure/delegate/JpaUserLoginFailureDelegateProvider.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/loginFailure/delegate/JpaUserLoginFailureDelegateProvider.java new file mode 100644 index 0000000000..de19a31e44 --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/loginFailure/delegate/JpaUserLoginFailureDelegateProvider.java @@ -0,0 +1,65 @@ +/* + * 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.loginFailure.delegate; + +import java.util.UUID; + +import javax.persistence.EntityManager; + +import org.keycloak.models.map.common.EntityField; +import org.keycloak.models.map.common.delegate.DelegateProvider; +import org.keycloak.models.map.loginFailure.MapUserLoginFailureEntity; +import org.keycloak.models.map.loginFailure.MapUserLoginFailureEntityFields; +import org.keycloak.models.map.storage.jpa.JpaDelegateProvider; +import org.keycloak.models.map.storage.jpa.loginFailure.entity.JpaUserLoginFailureEntity; + +/** + * A {@link DelegateProvider} implementation for {@link JpaUserLoginFailureEntity}. + * + * @author Stefan Guilhen + */ +public class JpaUserLoginFailureDelegateProvider extends JpaDelegateProvider implements DelegateProvider { + + private final EntityManager em; + + public JpaUserLoginFailureDelegateProvider(JpaUserLoginFailureEntity delegate, EntityManager em) { + super(delegate); + this.em = em; + } + + @Override + public MapUserLoginFailureEntity getDelegate(boolean isRead, Enum> field, Object... parameters) { + if (getDelegate().isMetadataInitialized()) return getDelegate(); + if (isRead) { + if (field instanceof MapUserLoginFailureEntityFields) { + switch ((MapUserLoginFailureEntityFields) field) { + case ID: + case REALM_ID: + case USER_ID: + return getDelegate(); + default: + setDelegate(em.find(JpaUserLoginFailureEntity.class, UUID.fromString(getDelegate().getId()))); + } + } else { + throw new IllegalStateException("Not a valid user login failure field: " + field); + } + } else { + setDelegate(em.find(JpaUserLoginFailureEntity.class, UUID.fromString(getDelegate().getId()))); + } + return getDelegate(); + } +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/loginFailure/entity/JpaUserLoginFailureEntity.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/loginFailure/entity/JpaUserLoginFailureEntity.java new file mode 100644 index 0000000000..13c144d378 --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/loginFailure/entity/JpaUserLoginFailureEntity.java @@ -0,0 +1,216 @@ +/* + * 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.loginFailure.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.Id; +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.loginFailure.MapUserLoginFailureEntity; +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_USER_LOGIN_FAILURE; + +/** + * JPA {@link MapUserLoginFailureEntity} implementation. Some fields are annotated with {@code @Column(insertable = false, updatable = false)} + * to indicate that they are automatically generated from json fields. As such, these fields are non-insertable and non-updatable. + * + * @author Stefan Guilhen + */ +@Entity +@Table(name = "kc_user_login_failure", + uniqueConstraints = { + @UniqueConstraint( + columnNames = {"realmId", "userId"} + ) +}) +@TypeDefs({@TypeDef(name = "jsonb", typeClass = JsonbType.class)}) +public class JpaUserLoginFailureEntity extends MapUserLoginFailureEntity.AbstractUserLoginFailureEntity 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 JpaUserLoginFailureMetadata 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 userId; + + /** + * No-argument constructor, used by hibernate to instantiate entities. + */ + public JpaUserLoginFailureEntity() { + this.metadata = new JpaUserLoginFailureMetadata(); + } + + public JpaUserLoginFailureEntity(DeepCloner cloner) { + this.metadata = new JpaUserLoginFailureMetadata(cloner); + } + + /** + * Used by hibernate when calling cb.construct from read(QueryParameters) method. + * It is used to select user login failure without metadata(json) field. + */ + public JpaUserLoginFailureEntity(UUID id, int version, Integer entityVersion, String realmId, String userId) { + this.id = id; + this.version = version; + this.entityVersion = entityVersion; + this.realmId = realmId; + this.userId = userId; + this.metadata = null; + } + + public boolean isMetadataInitialized() { + return this.metadata != null; + } + + @Override + public Integer getEntityVersion() { + if (isMetadataInitialized()) return metadata.getEntityVersion(); + return this.entityVersion; + } + + @Override + public void setEntityVersion(Integer entityVersion) { + this.metadata.setEntityVersion(entityVersion); + } + + @Override + public Integer getCurrentSchemaVersion() { + return CURRENT_SCHEMA_VERSION_USER_LOGIN_FAILURE; + } + + @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 (isMetadataInitialized()) return this.metadata.getRealmId(); + return this.realmId; + } + + @Override + public void setRealmId(String realmId) { + this.metadata.setRealmId(realmId); + } + + @Override + public String getUserId() { + if (isMetadataInitialized()) return this.metadata.getUserId(); + return this.userId; + } + + @Override + public void setUserId(String userId) { + this.metadata.setUserId(userId); + } + + @Override + public Long getFailedLoginNotBefore() { + return this.metadata.getFailedLoginNotBefore(); + } + + @Override + public void setFailedLoginNotBefore(Long failedLoginNotBefore) { + this.metadata.setFailedLoginNotBefore(failedLoginNotBefore); + } + + @Override + public Integer getNumFailures() { + return this.metadata.getNumFailures(); + } + + @Override + public void setNumFailures(Integer numFailures) { + this.metadata.setNumFailures(numFailures); + } + + @Override + public Long getLastFailure() { + return this.metadata.getLastFailure(); + } + + @Override + public void setLastFailure(Long lastFailure) { + this.metadata.setLastFailure(lastFailure); + } + + @Override + public String getLastIPFailure() { + return this.metadata.getLastIPFailure(); + } + + @Override + public void setLastIPFailure(String lastIPFailure) { + this.metadata.setLastIPFailure(lastIPFailure); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof JpaUserLoginFailureEntity)) return false; + return Objects.equals(getId(), ((JpaUserLoginFailureEntity) obj).getId()); + } +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/loginFailure/entity/JpaUserLoginFailureMetadata.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/loginFailure/entity/JpaUserLoginFailureMetadata.java new file mode 100644 index 0000000000..4c4abd618f --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/loginFailure/entity/JpaUserLoginFailureMetadata.java @@ -0,0 +1,48 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.keycloak.models.map.storage.jpa.loginFailure.entity; + +import java.io.Serializable; + +import org.keycloak.models.map.common.DeepCloner; +import org.keycloak.models.map.loginFailure.MapUserLoginFailureEntityImpl; + +/** + * Class that contains all the user login failure metadata that is written as JSON into the database. + * + * @author Stefan Guilhen + */ +public class JpaUserLoginFailureMetadata extends MapUserLoginFailureEntityImpl implements Serializable { + + public JpaUserLoginFailureMetadata(DeepCloner cloner) { + super(cloner); + } + + public JpaUserLoginFailureMetadata() { + super(); + } + + private Integer entityVersion; + + public Integer getEntityVersion() { + return entityVersion; + } + + public void setEntityVersion(Integer entityVersion) { + this.entityVersion = entityVersion; + } +} diff --git a/model/map-jpa/src/main/resources/META-INF/jpa-user-login-failures-changelog.xml b/model/map-jpa/src/main/resources/META-INF/jpa-user-login-failures-changelog.xml new file mode 100644 index 0000000000..2e8ea1d9b4 --- /dev/null +++ b/model/map-jpa/src/main/resources/META-INF/jpa-user-login-failures-changelog.xml @@ -0,0 +1,23 @@ + + + + + + + + diff --git a/model/map-jpa/src/main/resources/META-INF/persistence.xml b/model/map-jpa/src/main/resources/META-INF/persistence.xml index 063b089722..b9173c2003 100644 --- a/model/map-jpa/src/main/resources/META-INF/persistence.xml +++ b/model/map-jpa/src/main/resources/META-INF/persistence.xml @@ -20,5 +20,7 @@ org.keycloak.models.map.storage.jpa.role.entity.JpaRoleEntity org.keycloak.models.map.storage.jpa.role.entity.JpaRoleAttributeEntity + + org.keycloak.models.map.storage.jpa.loginFailure.entity.JpaUserLoginFailureEntity diff --git a/model/map-jpa/src/main/resources/META-INF/user-login-failures/jpa-user-login-failures-changelog-1.xml b/model/map-jpa/src/main/resources/META-INF/user-login-failures/jpa-user-login-failures-changelog-1.xml new file mode 100644 index 0000000000..e103e4c3ec --- /dev/null +++ b/model/map-jpa/src/main/resources/META-INF/user-login-failures/jpa-user-login-failures-changelog-1.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +