From c6312e3308fab84fe4c4b7d9ab966ce68d3da889 Mon Sep 17 00:00:00 2001 From: vramik Date: Sat, 17 Jul 2021 19:57:52 +0200 Subject: [PATCH] KEYCLOAK-18717 KEYCLOAK-18716 KEYCLOAK-18715 KEYCLOAK-18713 KEYCLOAK-18712 KEYCLOAK-18711 JPA clients no-downtime store --- dependencies/server-all/pom.xml | 4 + .../DefaultJpaConnectionProviderFactory.java | 3 +- .../LiquibaseJpaUpdaterProviderFactory.java | 4 +- model/map-jpa/pom.xml | 69 ++ .../JpaClientMapKeycloakTransaction.java | 161 +++++ .../jpa/client/JpaClientMapStorage.java | 40 ++ .../client/JpaClientMapStorageProvider.java | 43 ++ .../JpaClientMapStorageProviderFactory.java | 235 +++++++ .../client/JpaClientModelCriteriaBuilder.java | 154 +++++ .../delegate/JpaClientDelegateProvider.java | 73 +++ .../entity/JpaClientAttributeEntity.java | 103 +++ .../jpa/client/entity/JpaClientEntity.java | 620 ++++++++++++++++++ .../jpa/client/entity/JpaClientMetadata.java | 34 + .../dialect/JsonbPostgreSQL95Dialect.java | 30 + .../hibernate/jsonb/JpaClientMigration.java | 41 ++ .../jpa/hibernate/jsonb/JsonbType.java | 228 +++++++ .../jpa/updater/MapJpaUpdaterProvider.java | 71 ++ .../updater/MapJpaUpdaterProviderFactory.java | 23 + .../storage/jpa/updater/MapJpaUpdaterSpi.java | 48 ++ .../MapJpaLiquibaseUpdaterProvider.java | 183 ++++++ ...MapJpaLiquibaseUpdaterProviderFactory.java | 53 ++ .../META-INF/jpa-clients-changelog-1.xml | 53 ++ .../META-INF/jpa-clients-changelog.xml | 23 + .../main/resources/META-INF/persistence.xml | 7 + ...dels.map.storage.MapStorageProviderFactory | 18 + ...e.jpa.updater.MapJpaUpdaterProviderFactory | 18 + .../services/org.keycloak.provider.Spi | 18 + .../models/map/client/MapClientProvider.java | 2 +- .../models/map/common/Serialization.java | 10 +- model/pom.xml | 1 + pom.xml | 5 + .../java/org/keycloak/models/ClientModel.java | 2 +- .../resources/META-INF/keycloak-server.json | 10 +- .../base/src/test/resources/log4j.properties | 2 +- testsuite/utils/pom.xml | 2 +- .../resources/META-INF/keycloak-server.json | 50 +- 36 files changed, 2420 insertions(+), 21 deletions(-) create mode 100644 model/map-jpa/pom.xml create mode 100644 model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientMapKeycloakTransaction.java create mode 100644 model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientMapStorage.java create mode 100644 model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientMapStorageProvider.java create mode 100644 model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientMapStorageProviderFactory.java create mode 100644 model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientModelCriteriaBuilder.java create mode 100644 model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/delegate/JpaClientDelegateProvider.java create mode 100644 model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/entity/JpaClientAttributeEntity.java create mode 100644 model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/entity/JpaClientEntity.java create mode 100644 model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/entity/JpaClientMetadata.java create mode 100644 model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/dialect/JsonbPostgreSQL95Dialect.java create mode 100644 model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/JpaClientMigration.java create mode 100644 model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/JsonbType.java create mode 100644 model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/updater/MapJpaUpdaterProvider.java create mode 100644 model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/updater/MapJpaUpdaterProviderFactory.java create mode 100644 model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/updater/MapJpaUpdaterSpi.java create mode 100644 model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/updater/liquibase/MapJpaLiquibaseUpdaterProvider.java create mode 100644 model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/updater/liquibase/MapJpaLiquibaseUpdaterProviderFactory.java create mode 100644 model/map-jpa/src/main/resources/META-INF/jpa-clients-changelog-1.xml create mode 100644 model/map-jpa/src/main/resources/META-INF/jpa-clients-changelog.xml create mode 100644 model/map-jpa/src/main/resources/META-INF/persistence.xml create mode 100644 model/map-jpa/src/main/resources/META-INF/services/org.keycloak.models.map.storage.MapStorageProviderFactory create mode 100644 model/map-jpa/src/main/resources/META-INF/services/org.keycloak.models.map.storage.jpa.updater.MapJpaUpdaterProviderFactory create mode 100644 model/map-jpa/src/main/resources/META-INF/services/org.keycloak.provider.Spi diff --git a/dependencies/server-all/pom.xml b/dependencies/server-all/pom.xml index 088d48f1d1..cfc82ee8e1 100755 --- a/dependencies/server-all/pom.xml +++ b/dependencies/server-all/pom.xml @@ -44,6 +44,10 @@ org.keycloak keycloak-model-map + + org.keycloak + keycloak-model-map-jpa + org.keycloak keycloak-model-infinispan diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java index 7c27121a12..1f63166b67 100755 --- a/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java +++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java @@ -29,6 +29,7 @@ import org.keycloak.ServerStartupError; import org.keycloak.common.util.StackUtil; import org.keycloak.common.util.StringPropertyReplacer; import org.keycloak.connections.jpa.updater.JpaUpdaterProvider; +import org.keycloak.connections.jpa.updater.liquibase.LiquibaseJpaUpdaterProviderFactory; import org.keycloak.connections.jpa.util.JpaUtils; import org.keycloak.migration.MigrationModelManager; import org.keycloak.models.KeycloakSession; @@ -338,7 +339,7 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide } void migration(MigrationStrategy strategy, boolean initializeEmpty, String schema, File databaseUpdateFile, Connection connection, KeycloakSession session) { - JpaUpdaterProvider updater = session.getProvider(JpaUpdaterProvider.class); + JpaUpdaterProvider updater = session.getProvider(JpaUpdaterProvider.class, LiquibaseJpaUpdaterProviderFactory.PROVIDER_ID); JpaUpdaterProvider.Status status = updater.validate(connection, schema); if (status == JpaUpdaterProvider.Status.VALID) { diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProviderFactory.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProviderFactory.java index 26eac9a646..6385232e17 100755 --- a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProviderFactory.java +++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProviderFactory.java @@ -28,6 +28,8 @@ import org.keycloak.models.KeycloakSessionFactory; */ public class LiquibaseJpaUpdaterProviderFactory implements JpaUpdaterProviderFactory { + public static final String PROVIDER_ID = "liquibase"; + @Override public JpaUpdaterProvider create(KeycloakSession session) { return new LiquibaseJpaUpdaterProvider(session); @@ -48,7 +50,7 @@ public class LiquibaseJpaUpdaterProviderFactory implements JpaUpdaterProviderFac @Override public String getId() { - return "liquibase"; + return PROVIDER_ID; } } diff --git a/model/map-jpa/pom.xml b/model/map-jpa/pom.xml new file mode 100644 index 0000000000..a03ffbb98d --- /dev/null +++ b/model/map-jpa/pom.xml @@ -0,0 +1,69 @@ + + + + + + keycloak-model-pom + org.keycloak + 16.0.0-SNAPSHOT + + 4.0.0 + + keycloak-model-map-jpa + Keycloak Model Map JPA + + + + org.keycloak + keycloak-model-map + + + jakarta.persistence + jakarta.persistence-api + + + org.hibernate + hibernate-core + + + org.keycloak + keycloak-model-jpa + + + + + + org.hibernate.orm.tooling + hibernate-enhance-maven-plugin + ${hibernate.core.version} + + + + true + true + + + enhance + + + + + + + diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientMapKeycloakTransaction.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientMapKeycloakTransaction.java new file mode 100644 index 0000000000..07a338d9ba --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientMapKeycloakTransaction.java @@ -0,0 +1,161 @@ +/* + * Copyright 2021 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.client; + +import java.util.LinkedList; +import java.util.List; +import java.util.UUID; +import java.util.stream.Stream; +import javax.persistence.EntityManager; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaDelete; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Order; +import javax.persistence.criteria.Root; +import org.keycloak.connections.jpa.JpaKeycloakTransaction; +import org.keycloak.models.ClientModel; +import static org.keycloak.models.jpa.PaginationUtils.paginateQuery; +import org.keycloak.models.map.client.MapClientEntity; +import org.keycloak.models.map.client.MapClientEntityDelegate; +import org.keycloak.models.map.common.StringKeyConvertor.UUIDKey; +import org.keycloak.models.map.storage.jpa.client.delegate.JpaClientDelegateProvider; +import org.keycloak.models.map.storage.jpa.client.entity.JpaClientEntity; +import static org.keycloak.models.map.storage.jpa.client.JpaClientMapStorage.SUPPORTED_VERSION; +import static org.keycloak.models.map.storage.jpa.client.JpaClientMapStorageProviderFactory.CLONER; +import org.keycloak.models.map.storage.MapKeycloakTransaction; +import org.keycloak.models.map.storage.QueryParameters; +import static org.keycloak.utils.StreamsUtil.closing; + +public class JpaClientMapKeycloakTransaction extends JpaKeycloakTransaction implements MapKeycloakTransaction { + + public JpaClientMapKeycloakTransaction(EntityManager em) { + super(em); + } + + @Override + public MapClientEntity create(MapClientEntity mapEntity) { + JpaClientEntity jpaEntity = (JpaClientEntity) CLONER.from(mapEntity); + if (mapEntity.getId() == null) { + jpaEntity.setId(UUIDKey.INSTANCE.yieldNewUniqueKey().toString()); + } + jpaEntity.setEntityVersion(SUPPORTED_VERSION); + em.persist(jpaEntity); + return jpaEntity; + } + + @Override + public MapClientEntity read(String key) { + if (key == null) return null; + UUID uuid = UUIDKey.INSTANCE.fromStringSafe(key); + if (uuid == null) return null; + + return em.find(JpaClientEntity.class, uuid); + } + + @Override + public Stream read(QueryParameters queryParameters) { + JpaClientModelCriteriaBuilder mcb = queryParameters.getModelCriteriaBuilder() + .flashToModelCriteriaBuilder(new JpaClientModelCriteriaBuilder()); + + CriteriaBuilder cb = em.getCriteriaBuilder(); + CriteriaQuery query = cb.createQuery(JpaClientEntity.class); + Root root = query.from(JpaClientEntity.class); + query.select(cb.construct(JpaClientEntity.class, + root.get("id"), + root.get("entityVersion"), + root.get("realmId"), + root.get("clientId"), + root.get("protocol"), + root.get("enabled") + )); + + //ordering + if (!queryParameters.getOrderBy().isEmpty()) { + List orderByList = new LinkedList<>(); + for (QueryParameters.OrderBy order : queryParameters.getOrderBy()) { + switch (order.getOrder()) { + case ASCENDING: + orderByList.add(cb.asc(root.get(order.getModelField().getName()))); + break; + case DESCENDING: + orderByList.add(cb.desc(root.get(order.getModelField().getName()))); + break; + default: + throw new UnsupportedOperationException("Unknown ordering."); + } + } + query.orderBy(orderByList); + } + + if (mcb.getPredicateFunc() != null) query.where(mcb.getPredicateFunc().apply(cb, root)); + + return closing( + paginateQuery(em.createQuery(query), queryParameters.getOffset(), queryParameters.getLimit()) + .getResultStream()) + .map(c -> new MapClientEntityDelegate(new JpaClientDelegateProvider(c, em))); + } + + @Override + public long getCount(QueryParameters queryParameters) { + JpaClientModelCriteriaBuilder mcb = queryParameters.getModelCriteriaBuilder() + .flashToModelCriteriaBuilder(new JpaClientModelCriteriaBuilder()); + + CriteriaBuilder cb = em.getCriteriaBuilder(); + + CriteriaQuery countQuery = cb.createQuery(Long.class); + Root root = countQuery.from(JpaClientEntity.class); + countQuery.select(cb.count(root)); + + if (mcb.getPredicateFunc() != null) countQuery.where(mcb.getPredicateFunc().apply(cb, root)); + + return em.createQuery(countQuery).getSingleResult(); + } + + @Override + public boolean delete(String key) { + if (key == null) return false; + UUID uuid = UUIDKey.INSTANCE.fromStringSafe(key); + if (uuid == null) return false; + em.remove(em.getReference(JpaClientEntity.class, uuid)); + return true; + } + + @Override + public long delete(QueryParameters queryParameters) { + JpaClientModelCriteriaBuilder mcb = queryParameters.getModelCriteriaBuilder() + .flashToModelCriteriaBuilder(new JpaClientModelCriteriaBuilder()); + + CriteriaBuilder cb = em.getCriteriaBuilder(); + + CriteriaDelete deleteQuery = cb.createCriteriaDelete(JpaClientEntity.class); + + Root root = deleteQuery.from(JpaClientEntity.class); + + if (mcb.getPredicateFunc() != null) deleteQuery.where(mcb.getPredicateFunc().apply(cb, root)); + +// TODO find out if the flush and clear are needed here or not, since delete(QueryParameters) +// is not used yet from the code it's difficult to investigate its potential purpose here +// according to https://thorben-janssen.com/5-common-hibernate-mistakes-that-cause-dozens-of-unexpected-queries/#Remove_Child_Entities_With_a_Bulk_Operation +// it seems it is necessary unless it is sure that any of removed entities wasn't fetched +// Once KEYCLOAK-19697 is done we could test our scenarios and see if we need the flush and clear +// em.flush(); +// em.clear(); + + return em.createQuery(deleteQuery).executeUpdate(); + } + +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientMapStorage.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientMapStorage.java new file mode 100644 index 0000000000..ecc1ba7373 --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientMapStorage.java @@ -0,0 +1,40 @@ +/* + * Copyright 2021 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.client; + +import javax.persistence.EntityManager; +import org.keycloak.models.ClientModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.map.client.MapClientEntity; +import org.keycloak.models.map.storage.MapKeycloakTransaction; +import org.keycloak.models.map.storage.MapStorage; + +public class JpaClientMapStorage implements MapStorage { + + public static final Integer SUPPORTED_VERSION = 1; + private final EntityManager em; + + public JpaClientMapStorage(EntityManager em) { + this.em = em; + } + + @Override + public MapKeycloakTransaction createTransaction(KeycloakSession session) { + return new JpaClientMapKeycloakTransaction(em); + } + +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientMapStorageProvider.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientMapStorageProvider.java new file mode 100644 index 0000000000..014ed9c940 --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientMapStorageProvider.java @@ -0,0 +1,43 @@ +/* + * Copyright 2021 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.client; + +import javax.persistence.EntityManager; +import org.keycloak.models.ClientModel; +import org.keycloak.models.map.client.MapClientEntity; +import org.keycloak.models.map.storage.MapStorage; +import org.keycloak.models.map.storage.MapStorageProvider; +import org.keycloak.models.map.storage.MapStorageProviderFactory.Flag; + +public class JpaClientMapStorageProvider implements MapStorageProvider { + + private final EntityManager em; + + public JpaClientMapStorageProvider(EntityManager em) { + this.em = em; + } + + @Override + public void close() { + em.close(); + } + + @Override + public MapStorage getStorage(Class modelType, Flag... flags) { + return new JpaClientMapStorage(em); + } +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientMapStorageProviderFactory.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientMapStorageProviderFactory.java new file mode 100644 index 0000000000..269a0b988f --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientMapStorageProviderFactory.java @@ -0,0 +1,235 @@ +/* + * Copyright 2021 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.client; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.persistence.EntityManagerFactory; +import javax.sql.DataSource; +import org.hibernate.cfg.AvailableSettings; +import org.jboss.logging.Logger; +import org.keycloak.Config; +import org.keycloak.common.Profile; +import org.keycloak.common.util.StackUtil; +import org.keycloak.common.util.StringPropertyReplacer; +import org.keycloak.component.AmphibianProviderFactory; +import org.keycloak.connections.jpa.util.JpaUtils; +import org.keycloak.models.ClientModel; +import org.keycloak.models.map.storage.jpa.client.entity.JpaClientEntity; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.dblock.DBLockManager; +import org.keycloak.models.dblock.DBLockProvider; +import org.keycloak.models.map.client.MapProtocolMapperEntity; +import org.keycloak.models.map.client.MapProtocolMapperEntityImpl; +import org.keycloak.models.map.common.DeepCloner; +import org.keycloak.models.map.storage.MapStorageProvider; +import org.keycloak.models.map.storage.MapStorageProviderFactory; +import org.keycloak.models.map.storage.jpa.updater.MapJpaUpdaterProvider; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.provider.EnvironmentDependentProviderFactory; + +public class JpaClientMapStorageProviderFactory implements + AmphibianProviderFactory, + MapStorageProviderFactory, + EnvironmentDependentProviderFactory { + + final static DeepCloner CLONER = new DeepCloner.Builder() + .constructor(JpaClientEntity.class, JpaClientEntity::new) + .constructor(MapProtocolMapperEntity.class, MapProtocolMapperEntityImpl::new) + .build(); + + public static final String PROVIDER_ID = "jpa-client-map-storage"; + + private volatile EntityManagerFactory emf; + + private static final Logger logger = Logger.getLogger(JpaClientMapStorageProviderFactory.class); + + private Config.Scope config; + + @Override + public MapStorageProvider create(KeycloakSession session) { + lazyInit(session); + + return new JpaClientMapStorageProvider(emf.createEntityManager()); + } + + @Override + public void init(Config.Scope config) { + this.config = config; + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + } + + @Override + public String getId() { + return PROVIDER_ID; + } + + @Override + public String getHelpText() { + return "JPA Client Map Storage"; + } + + @Override + public boolean isSupported() { + return Profile.isFeatureEnabled(Profile.Feature.MAP_STORAGE); + } + + @Override + public void close() { + if (emf != null) { + emf.close(); + } + } + + private void lazyInit(KeycloakSession session) { + if (emf == null) { + synchronized (this) { + if (emf == null) { + logger.debugf("Initializing JPA connections%s", StackUtil.getShortStackTrace()); + + Map properties = new HashMap<>(); + + String unitName = "keycloak-client-store"; + + String dataSource = config.get("dataSource"); + if (dataSource != null) { + properties.put(AvailableSettings.JPA_NON_JTA_DATASOURCE, dataSource); + } else { + properties.put(AvailableSettings.JPA_JDBC_URL, config.get("url")); + properties.put(AvailableSettings.JPA_JDBC_DRIVER, config.get("driver")); + + String user = config.get("user"); + if (user != null) { + properties.put(AvailableSettings.JPA_JDBC_USER, user); + } + String password = config.get("password"); + if (password != null) { + properties.put(AvailableSettings.JPA_JDBC_PASSWORD, password); + } + } + + String schema = config.get("schema"); + if (schema != null) { + properties.put(JpaUtils.HIBERNATE_DEFAULT_SCHEMA, schema); + } + + properties.put("hibernate.show_sql", config.getBoolean("showSql", false)); + properties.put("hibernate.format_sql", config.getBoolean("formatSql", true)); + properties.put("hibernate.dialect", config.get("driverDialect")); + + Integer isolation = config.getInt("isolation"); + if (isolation != null) { + if (isolation < Connection.TRANSACTION_REPEATABLE_READ) { + logger.warn("Concurrent requests may not be reliable with transaction level lower than TRANSACTION_REPEATABLE_READ."); + } + properties.put(AvailableSettings.ISOLATION, String.valueOf(isolation)); + } else { + // default value is TRANSACTION_READ_COMMITTED + logger.warn("Concurrent requests may not be reliable with transaction level lower than TRANSACTION_REPEATABLE_READ."); + } + + + Connection connection = getConnection(); + try { + printOperationalInfo(connection); + + customChanges(connection, schema, session, session.getProvider(MapJpaUpdaterProvider.class)); + + logger.trace("Creating EntityManagerFactory"); + Collection classLoaders = new ArrayList<>(); + if (properties.containsKey(AvailableSettings.CLASSLOADERS)) { + classLoaders.addAll((Collection) properties.get(AvailableSettings.CLASSLOADERS)); + } + classLoaders.add(getClass().getClassLoader()); + properties.put(AvailableSettings.CLASSLOADERS, classLoaders); + this.emf = JpaUtils.createEntityManagerFactory(session, unitName, properties, false); + logger.trace("EntityManagerFactory created"); + + } finally { + // Close after creating EntityManagerFactory to prevent in-mem databases from closing + if (connection != null) { + try { + connection.close(); + } catch (SQLException e) { + logger.warn("Can't close connection", e); + } + } + } + } + } + } + } + + private void customChanges(Connection connection, String schema, KeycloakSession session, MapJpaUpdaterProvider updater) { + KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession lockSession) -> { + DBLockManager dbLockManager = new DBLockManager(lockSession); + DBLockProvider dbLock2 = dbLockManager.getDBLock(); + dbLock2.waitForLock(DBLockProvider.Namespace.DATABASE); + try { + updater.update(ClientModel.class, connection, schema); + } finally { + dbLock2.releaseLock(); + } + }); + } + + private void printOperationalInfo(Connection connection) { + try { + HashMap operationalInfo = new LinkedHashMap<>(); + DatabaseMetaData md = connection.getMetaData(); + operationalInfo.put("databaseUrl", md.getURL()); + operationalInfo.put("databaseUser", md.getUserName()); + operationalInfo.put("databaseProduct", md.getDatabaseProductName() + " " + md.getDatabaseProductVersion()); + operationalInfo.put("databaseDriver", md.getDriverName() + " " + md.getDriverVersion()); + + logger.infof("Database info: %s", operationalInfo.toString()); + } catch (SQLException e) { + logger.warn("Unable to prepare operational info due database exception: " + e.getMessage()); + } + } + + private Connection getConnection() { + try { + String dataSourceLookup = config.get("dataSource"); + if (dataSourceLookup != null) { + DataSource dataSource = (DataSource) new InitialContext().lookup(dataSourceLookup); + return dataSource.getConnection(); + } else { + Class.forName(config.get("driver")); + return DriverManager.getConnection( + StringPropertyReplacer.replaceProperties(config.get("url"), System.getProperties()), + config.get("user"), + config.get("password")); + } + } catch (ClassNotFoundException | SQLException | NamingException e) { + throw new RuntimeException("Failed to connect to database", e); + } + } +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientModelCriteriaBuilder.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientModelCriteriaBuilder.java new file mode 100644 index 0000000000..0a364a5fb8 --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientModelCriteriaBuilder.java @@ -0,0 +1,154 @@ +/* + * Copyright 2021 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.client; + +import com.fasterxml.jackson.core.JsonProcessingException; +import org.keycloak.models.map.storage.jpa.client.entity.JpaClientEntity; +import org.keycloak.models.map.storage.jpa.client.entity.JpaClientAttributeEntity; +import java.util.Arrays; +import java.util.function.BiFunction; +import java.util.stream.Stream; +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.Join; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; +import org.keycloak.models.ClientModel; +import org.keycloak.models.ClientModel.SearchableFields; +import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType; +import org.keycloak.models.map.storage.CriterionNotSupportedException; +import org.keycloak.models.map.storage.ModelCriteriaBuilder; +import org.keycloak.storage.SearchableModelField; + +public class JpaClientModelCriteriaBuilder implements ModelCriteriaBuilder { + + private BiFunction, Predicate> predicateFunc = null; + + public JpaClientModelCriteriaBuilder() { + } + + private JpaClientModelCriteriaBuilder(BiFunction, Predicate> predicateFunc) { + this.predicateFunc = predicateFunc; + } + + private void validateValue(Object[] value, SearchableModelField field, Operator op, Class... expectedTypes) { + if (value == null || expectedTypes == null || value.length != expectedTypes.length) { + throw new CriterionNotSupportedException(field, op, "Invalid argument: " + Arrays.toString(value)); + } + for (int i = 0; i < expectedTypes.length; i++) { + if (! expectedTypes[i].isInstance(value[i])) { + throw new CriterionNotSupportedException(field, op, "Expected types: " + Arrays.toString(expectedTypes) + + " but got: " + Arrays.toString(value)); + } + } + } + + private String convertToJson(Object input) { + try { + return JsonbType.MAPPER.writeValueAsString(input); + } catch (JsonProcessingException ex) { + throw new RuntimeException("Unable to write value as String.", ex); + } + } + + @Override + public JpaClientModelCriteriaBuilder compare(SearchableModelField modelField, Operator op, Object... value) { + switch (op) { + case EQ: + if (modelField.equals(SearchableFields.REALM_ID) || + modelField.equals(SearchableFields.CLIENT_ID)) { + + validateValue(value, modelField, op, String.class); + + return new JpaClientModelCriteriaBuilder((cb, root) -> + cb.equal(root.get(modelField.getName()), value[0]) + ); + } else if (modelField.equals(SearchableFields.ENABLED)) { + validateValue(value, modelField, op, Boolean.class); + + return new JpaClientModelCriteriaBuilder((cb, root) -> + cb.equal(root.get(modelField.getName()), value[0]) + ); + } else if (modelField.equals(SearchableFields.SCOPE_MAPPING_ROLE)) { + validateValue(value, modelField, op, String.class); + + return new JpaClientModelCriteriaBuilder((cb, root) -> + cb.isTrue(cb.function("@>", + Boolean.TYPE, + cb.function("->", JsonbType.class, root.get("metadata"), cb.literal("fScopeMappings")), + cb.literal(convertToJson(value[0])))) + ); + } else if (modelField.equals(SearchableFields.ALWAYS_DISPLAY_IN_CONSOLE)) { + validateValue(value, modelField, op, Boolean.class); + + return new JpaClientModelCriteriaBuilder((cb, root) -> + cb.equal( + cb.function("->", JsonbType.class, root.get("metadata"), cb.literal("fAlwaysDisplayInConsole")), + cb.literal(convertToJson(value[0]))) + ); + } else if (modelField.equals(SearchableFields.ATTRIBUTE)) { + validateValue(value, modelField, op, String.class, String.class); + + return new JpaClientModelCriteriaBuilder((cb, root) -> { + Join join = root.join("attributes"); + 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.equals(SearchableFields.CLIENT_ID)) { + validateValue(value, modelField, op, String.class); + + return new JpaClientModelCriteriaBuilder((cb, root) -> + cb.like(cb.lower(root.get(modelField.getName())), value[0].toString().toLowerCase()) + ); + } + default: + throw new CriterionNotSupportedException(modelField, op); + } + } + + @Override + public JpaClientModelCriteriaBuilder and(JpaClientModelCriteriaBuilder... builders) { + return new JpaClientModelCriteriaBuilder( + (cb, root) -> cb.and(Stream.of(builders).map(b -> b.getPredicateFunc().apply(cb, root)).toArray(Predicate[]::new)) + ); + } + + @Override + public JpaClientModelCriteriaBuilder or(JpaClientModelCriteriaBuilder... builders) { + return new JpaClientModelCriteriaBuilder( + (cb, root) -> cb.or(Stream.of(builders).map(b -> b.getPredicateFunc().apply(cb, root)).toArray(Predicate[]::new)) + ); + } + + @Override + public JpaClientModelCriteriaBuilder not(JpaClientModelCriteriaBuilder builder) { + return new JpaClientModelCriteriaBuilder( + (cb, root) -> cb.not(builder.getPredicateFunc().apply(cb, root)) + ); + } + + BiFunction, Predicate> getPredicateFunc() { + return predicateFunc; + } + +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/delegate/JpaClientDelegateProvider.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/delegate/JpaClientDelegateProvider.java new file mode 100644 index 0000000000..aa1f656d54 --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/delegate/JpaClientDelegateProvider.java @@ -0,0 +1,73 @@ +/* + * Copyright 2021 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.client.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.client.MapClientEntity; +import org.keycloak.models.map.client.MapClientEntityFields; +import org.keycloak.models.map.common.delegate.DelegateProvider; +import org.keycloak.models.map.storage.jpa.client.entity.JpaClientEntity; + +public class JpaClientDelegateProvider implements DelegateProvider { + + private JpaClientEntity delegate; + private final EntityManager em; + + public JpaClientDelegateProvider(JpaClientEntity deledate, EntityManager em) { + this.delegate = deledate; + this.em = em; + } + + @Override + public JpaClientEntity getDelegate(boolean isRead, Object field, Object... parameters) { + if (delegate.isMetadataInitialized()) return delegate; + if (isRead) { + if (field instanceof MapClientEntityFields) { + switch ((MapClientEntityFields) field) { + case ID: + case REALM_ID: + case CLIENT_ID: + case PROTOCOL: + case ENABLED: + return delegate; + + case ATTRIBUTES: + CriteriaBuilder cb = em.getCriteriaBuilder(); + CriteriaQuery query = cb.createQuery(JpaClientEntity.class); + Root root = query.from(JpaClientEntity.class); + root.fetch("attributes", JoinType.INNER); + query.select(root).where(cb.equal(root.get("id"), UUID.fromString(delegate.getId()))); + + delegate = em.createQuery(query).getSingleResult(); + break; + + default: + delegate = em.find(JpaClientEntity.class, UUID.fromString(delegate.getId())); + } + } else throw new IllegalStateException("Not a valid client field: " + field); + } else { + delegate = em.find(JpaClientEntity.class, UUID.fromString(delegate.getId())); + } + return delegate; + } + +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/entity/JpaClientAttributeEntity.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/entity/JpaClientAttributeEntity.java new file mode 100644 index 0000000000..715fcbcfa7 --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/entity/JpaClientAttributeEntity.java @@ -0,0 +1,103 @@ +/* + * Copyright 2021 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.client.entity; + +import java.io.Serializable; +import java.util.Objects; +import java.util.UUID; +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.Nationalized; + +@Entity +@Table(name = "client_attribute") +public class JpaClientAttributeEntity implements Serializable { + + @Id + @Column + @GeneratedValue + private UUID id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name="fk_client") + private JpaClientEntity client; + + @Column + private String name; + + @Nationalized + @Column + private String value; + + public JpaClientAttributeEntity() { + } + + public JpaClientAttributeEntity(JpaClientEntity client, String name, String value) { + this.client = client; + this.name = name; + this.value = value; + } + + public UUID getId() { + return id; + } + + public JpaClientEntity getClient() { + return client; + } + + public void setClient(JpaClientEntity client) { + this.client = client; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof JpaClientAttributeEntity)) return false; + JpaClientAttributeEntity that = (JpaClientAttributeEntity) obj; + return Objects.equals(getClient(), that.getClient()) && + Objects.equals(getName(), that.getName()) && + Objects.equals(getValue(), that.getValue()); + } +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/entity/JpaClientEntity.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/entity/JpaClientEntity.java new file mode 100644 index 0000000000..40cb8cccc4 --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/entity/JpaClientEntity.java @@ -0,0 +1,620 @@ +/* + * Copyright 2021 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.client.entity; + +import java.io.Serializable; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +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.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.OneToMany; +import javax.persistence.Table; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; +import org.hibernate.annotations.TypeDefs; +import org.keycloak.models.map.client.MapClientEntity.AbstractClientEntity; +import org.keycloak.models.map.client.MapProtocolMapperEntity; +import static org.keycloak.models.map.storage.jpa.client.JpaClientMapStorage.SUPPORTED_VERSION; +import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType; + +@Entity +@Table(name = "client") +@TypeDefs({@TypeDef(name = "jsonb", typeClass = JsonbType.class)}) +public class JpaClientEntity extends AbstractClientEntity implements Serializable { + + @Id + @Column + private UUID id; + + @Type(type = "jsonb") + @Column(columnDefinition = "jsonb") + private final JpaClientMetadata 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 clientId; + + @Column(insertable = false, updatable = false) + @Basic(fetch = FetchType.LAZY) + private String protocol; + + @Column(insertable = false, updatable = false) + @Basic(fetch = FetchType.LAZY) + private Boolean enabled; + + @OneToMany(mappedBy = "client", cascade = CascadeType.PERSIST, orphanRemoval = true) + private final Set attributes = new HashSet<>(); + + /** + * No-argument constructor, used by hibernate to instantiate entities. + */ + public JpaClientEntity() { + this.metadata = new JpaClientMetadata(); + } + + /** + * Used by hibernate when calling cb.construct from read(QueryParameters) method. + * It is used to select client without metadata(json) field. + */ + public JpaClientEntity(UUID id, Integer entityVersion, String realmId, String clientId, + String protocol, Boolean enabled) { + this.id = id; + this.entityVersion = entityVersion; + this.realmId = realmId; + this.clientId = clientId; + this.protocol = protocol; + this.enabled = enabled; + this.metadata = null; + } + + public boolean isMetadataInitialized() { + return metadata != null; + } + + /** + * In case of any update on entity, we want to update the entityVerion + * to current one. + */ + private void checkEntityVersionForUpdate() { + Integer ev = getEntityVersion(); + if (ev != null && ev < SUPPORTED_VERSION) { + setEntityVersion(SUPPORTED_VERSION); + } + } + + public Integer getEntityVersion() { + if (isMetadataInitialized()) return metadata.getEntityVersion(); + return entityVersion; + } + + public void setEntityVersion(Integer entityVersion) { + metadata.setEntityVersion(entityVersion); + } + + @Override + public String getId() { + return id == null ? null : id.toString(); + } + + @Override + public void setId(String id) { + this.id = id == null ? null : UUID.fromString(id); + } + + @Override + public String getRealmId() { + if (isMetadataInitialized()) return metadata.getRealmId(); + return realmId; + } + + @Override + public void setRealmId(String realmId) { + checkEntityVersionForUpdate(); + metadata.setRealmId(realmId); + } + + @Override + public String getClientId() { + if (isMetadataInitialized()) return metadata.getClientId(); + return clientId; + } + + @Override + public void setClientId(String clientId) { + checkEntityVersionForUpdate(); + metadata.setClientId(clientId); + } + + @Override + public void setEnabled(Boolean enabled) { + checkEntityVersionForUpdate(); + metadata.setEnabled(enabled); + } + + @Override + public Boolean isEnabled() { + if (isMetadataInitialized()) return metadata.isEnabled(); + return enabled; + } + + @Override + public Map getClientScopes() { + return metadata.getClientScopes(); + } + + @Override + public void setClientScope(String id, Boolean defaultScope) { + checkEntityVersionForUpdate(); + metadata.setClientScope(id, defaultScope); + } + + @Override + public void removeClientScope(String id) { + checkEntityVersionForUpdate(); + metadata.removeClientScope(id); + } + + @Override + public MapProtocolMapperEntity getProtocolMapper(String id) { + return metadata.getProtocolMapper(id); + } + + @Override + public Map getProtocolMappers() { + return metadata.getProtocolMappers(); + } + + @Override + public void removeProtocolMapper(String id) { + checkEntityVersionForUpdate(); + metadata.removeProtocolMapper(id); + } + + @Override + public void setProtocolMapper(String id, MapProtocolMapperEntity mapping) { + checkEntityVersionForUpdate(); + metadata.setProtocolMapper(id, mapping); + } + + @Override + public void addRedirectUri(String redirectUri) { + checkEntityVersionForUpdate(); + metadata.addRedirectUri(redirectUri); + } + + @Override + public Set getRedirectUris() { + return metadata.getRedirectUris(); + } + + @Override + public void removeRedirectUri(String redirectUri) { + checkEntityVersionForUpdate(); + metadata.removeRedirectUri(redirectUri); + } + + @Override + public void setRedirectUris(Set redirectUris) { + checkEntityVersionForUpdate(); + metadata.setRedirectUris(redirectUris); + } + + @Override + public void addScopeMapping(String id) { + checkEntityVersionForUpdate(); + metadata.addScopeMapping(id); + } + + @Override + public void removeScopeMapping(String id) { + checkEntityVersionForUpdate(); + metadata.removeScopeMapping(id); + } + + @Override + public Collection getScopeMappings() { + return metadata.getScopeMappings(); + } + + @Override + public void addWebOrigin(String webOrigin) { + checkEntityVersionForUpdate(); + metadata.addWebOrigin(webOrigin); + } + + @Override + public Set getWebOrigins() { + return metadata.getWebOrigins(); + } + + @Override + public void removeWebOrigin(String webOrigin) { + checkEntityVersionForUpdate(); + metadata.removeWebOrigin(webOrigin); + } + + @Override + public void setWebOrigins(Set webOrigins) { + checkEntityVersionForUpdate(); + metadata.setWebOrigins(webOrigins); + } + + @Override + public String getAuthenticationFlowBindingOverride(String binding) { + return metadata.getAuthenticationFlowBindingOverride(binding); + } + + @Override + public Map getAuthenticationFlowBindingOverrides() { + return metadata.getAuthenticationFlowBindingOverrides(); + } + + @Override + public void removeAuthenticationFlowBindingOverride(String binding) { + checkEntityVersionForUpdate(); + metadata.removeAuthenticationFlowBindingOverride(binding); + } + + @Override + public void setAuthenticationFlowBindingOverride(String binding, String flowId) { + checkEntityVersionForUpdate(); + metadata.setAuthenticationFlowBindingOverride(binding, flowId); + } + + @Override + public String getBaseUrl() { + return metadata.getBaseUrl(); + } + + @Override + public void setBaseUrl(String baseUrl) { + checkEntityVersionForUpdate(); + metadata.setBaseUrl(baseUrl); + } + + @Override + public String getClientAuthenticatorType() { + return metadata.getClientAuthenticatorType(); + } + + @Override + public void setClientAuthenticatorType(String clientAuthenticatorType) { + checkEntityVersionForUpdate(); + metadata.setClientAuthenticatorType(clientAuthenticatorType); + } + + @Override + public String getDescription() { + return metadata.getDescription(); + } + + @Override + public void setDescription(String description) { + checkEntityVersionForUpdate(); + metadata.setDescription(description); + } + + @Override + public String getManagementUrl() { + return metadata.getManagementUrl(); + } + + @Override + public void setManagementUrl(String managementUrl) { + checkEntityVersionForUpdate(); + metadata.setManagementUrl(managementUrl); + } + + @Override + public String getName() { + return metadata.getName(); + } + + @Override + public void setName(String name) { + checkEntityVersionForUpdate(); + metadata.setName(name); + } + + @Override + public Integer getNodeReRegistrationTimeout() { + return metadata.getNodeReRegistrationTimeout(); + } + + @Override + public void setNodeReRegistrationTimeout(Integer nodeReRegistrationTimeout) { + checkEntityVersionForUpdate(); + metadata.setNodeReRegistrationTimeout(nodeReRegistrationTimeout); + } + + @Override + public Integer getNotBefore() { + return metadata.getNotBefore(); + } + + @Override + public void setNotBefore(Integer notBefore) { + checkEntityVersionForUpdate(); + metadata.setNotBefore(notBefore); + } + + @Override + public String getProtocol() { + if (isMetadataInitialized()) return metadata.getProtocol(); + return protocol; + } + + @Override + public void setProtocol(String protocol) { + checkEntityVersionForUpdate(); + metadata.setProtocol(protocol); + } + + @Override + public String getRegistrationToken() { + return metadata.getRegistrationToken(); + } + + @Override + public void setRegistrationToken(String registrationToken) { + checkEntityVersionForUpdate(); + metadata.setRegistrationToken(registrationToken); + } + + @Override + public String getRootUrl() { + return metadata.getRootUrl(); + } + + @Override + public void setRootUrl(String rootUrl) { + checkEntityVersionForUpdate(); + metadata.setRootUrl(rootUrl); + } + + @Override + public Set getScope() { + return metadata.getScope(); + } + + @Override + public void setScope(Set scope) { + checkEntityVersionForUpdate(); + metadata.setScope(scope); + } + + @Override + public String getSecret() { + return metadata.getSecret(); + } + + @Override + public void setSecret(String secret) { + checkEntityVersionForUpdate(); + metadata.setSecret(secret); + } + + @Override + public Boolean isAlwaysDisplayInConsole() { + return metadata.isAlwaysDisplayInConsole(); + } + + @Override + public void setAlwaysDisplayInConsole(Boolean alwaysDisplayInConsole) { + checkEntityVersionForUpdate(); + metadata.setAlwaysDisplayInConsole(alwaysDisplayInConsole); + } + + @Override + public Boolean isBearerOnly() { + return metadata.isBearerOnly(); + } + + @Override + public void setBearerOnly(Boolean bearerOnly) { + checkEntityVersionForUpdate(); + metadata.setBearerOnly(bearerOnly); + } + + @Override + public Boolean isConsentRequired() { + return metadata.isConsentRequired(); + } + + @Override + public void setConsentRequired(Boolean consentRequired) { + checkEntityVersionForUpdate(); + metadata.setConsentRequired(consentRequired); + } + + @Override + public Boolean isDirectAccessGrantsEnabled() { + return metadata.isDirectAccessGrantsEnabled(); + } + + @Override + public void setDirectAccessGrantsEnabled(Boolean directAccessGrantsEnabled) { + checkEntityVersionForUpdate(); + metadata.setDirectAccessGrantsEnabled(directAccessGrantsEnabled); + } + + @Override + public Boolean isFrontchannelLogout() { + return metadata.isFrontchannelLogout(); + } + + @Override + public void setFrontchannelLogout(Boolean frontchannelLogout) { + checkEntityVersionForUpdate(); + metadata.setFrontchannelLogout(frontchannelLogout); + } + + @Override + public Boolean isFullScopeAllowed() { + return metadata.isFullScopeAllowed(); + } + + @Override + public void setFullScopeAllowed(Boolean fullScopeAllowed) { + checkEntityVersionForUpdate(); + metadata.setFullScopeAllowed(fullScopeAllowed); + } + + @Override + public Boolean isImplicitFlowEnabled() { + return metadata.isImplicitFlowEnabled(); + } + + @Override + public void setImplicitFlowEnabled(Boolean implicitFlowEnabled) { + checkEntityVersionForUpdate(); + metadata.setImplicitFlowEnabled(implicitFlowEnabled); + } + + @Override + public Boolean isPublicClient() { + return metadata.isPublicClient(); + } + + @Override + public void setPublicClient(Boolean publicClient) { + checkEntityVersionForUpdate(); + metadata.setPublicClient(publicClient); + } + + @Override + public Boolean isServiceAccountsEnabled() { + return metadata.isServiceAccountsEnabled(); + } + + @Override + public void setServiceAccountsEnabled(Boolean serviceAccountsEnabled) { + checkEntityVersionForUpdate(); + metadata.setServiceAccountsEnabled(serviceAccountsEnabled); + } + + @Override + public Boolean isStandardFlowEnabled() { + return metadata.isStandardFlowEnabled(); + } + + @Override + public void setStandardFlowEnabled(Boolean standardFlowEnabled) { + checkEntityVersionForUpdate(); + metadata.setStandardFlowEnabled(standardFlowEnabled); + } + + @Override + public Boolean isSurrogateAuthRequired() { + return metadata.isSurrogateAuthRequired(); + } + + @Override + public void setSurrogateAuthRequired(Boolean surrogateAuthRequired) { + checkEntityVersionForUpdate(); + metadata.setSurrogateAuthRequired(surrogateAuthRequired); + } + + @Override + public void removeAttribute(String name) { + checkEntityVersionForUpdate(); + for (Iterator iterator = attributes.iterator(); iterator.hasNext();) { + JpaClientAttributeEntity attr = iterator.next(); + if (Objects.equals(attr.getName(), name)) { + iterator.remove(); + attr.setClient(null); + } + } + } + + @Override + public void setAttribute(String name, List values) { + checkEntityVersionForUpdate(); + removeAttribute(name); + for (String value : values) { + JpaClientAttributeEntity attribute = new JpaClientAttributeEntity(this, name, value); + attributes.add(attribute); + } + } + + @Override + public List getAttribute(String name) { + return attributes.stream() + .filter(a -> Objects.equals(a.getName(), name)) + .map(JpaClientAttributeEntity::getValue) + .collect(Collectors.toList()); + } + + @Override + public Map> getAttributes() { + Map> result = new HashMap<>(); + for (JpaClientAttributeEntity attribute : attributes) { + List values = result.getOrDefault(attribute.getName(), new LinkedList<>()); + values.add(attribute.getValue()); + result.put(attribute.getName(), values); + } + return result; + } + + @Override + public void setAttributes(Map> attributes) { + checkEntityVersionForUpdate(); + for (Iterator iterator = this.attributes.iterator(); iterator.hasNext();) { + JpaClientAttributeEntity attr = iterator.next(); + iterator.remove(); + attr.setClient(null); + } + if (attributes != null) { + for (Map.Entry> attrEntry : attributes.entrySet()) { + setAttribute(attrEntry.getKey(), attrEntry.getValue()); + } + } + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof JpaClientEntity)) return false; + return Objects.equals(getId(), ((JpaClientEntity) obj).getId()); + } +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/entity/JpaClientMetadata.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/entity/JpaClientMetadata.java new file mode 100644 index 0000000000..e4c6492c59 --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/entity/JpaClientMetadata.java @@ -0,0 +1,34 @@ +/* + * Copyright 2021 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.client.entity; + +import java.io.Serializable; +import org.keycloak.models.map.client.MapClientEntityImpl; + +public class JpaClientMetadata extends MapClientEntityImpl implements Serializable { + + private Integer entityVersion; + + public Integer getEntityVersion() { + return entityVersion; + } + + public void setEntityVersion(Integer entityVersion) { + this.entityVersion = entityVersion; + } + +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/dialect/JsonbPostgreSQL95Dialect.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/dialect/JsonbPostgreSQL95Dialect.java new file mode 100644 index 0000000000..ea85c215fa --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/dialect/JsonbPostgreSQL95Dialect.java @@ -0,0 +1,30 @@ +/* + * Copyright 2021 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.dialect; + +import org.hibernate.dialect.PostgreSQL95Dialect; +import org.hibernate.dialect.function.SQLFunctionTemplate; +import org.hibernate.type.StandardBasicTypes; +import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType; + +public class JsonbPostgreSQL95Dialect extends PostgreSQL95Dialect { + public JsonbPostgreSQL95Dialect() { + super(); + registerFunction("->", new SQLFunctionTemplate(JsonbType.INSTANCE, "?1->?2")); + registerFunction("@>", new SQLFunctionTemplate(StandardBasicTypes.BOOLEAN, "?1@>?2::jsonb")); + } +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/JpaClientMigration.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/JpaClientMigration.java new file mode 100644 index 0000000000..d9bcdc3226 --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/JpaClientMigration.java @@ -0,0 +1,41 @@ +/* + * Copyright 2021 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; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; + +public class JpaClientMigration { + + private static final List> MIGRATORS = Arrays.asList( + o -> o // no migration yet + ); + + public static ObjectNode migrateTreeTo(int currentVersion, int targetVersion, ObjectNode node) { + while (currentVersion < targetVersion) { + Function migrator = MIGRATORS.get(currentVersion); + if (migrator != null) { + node = migrator.apply(node); + } + currentVersion++; + } + return node; + } + +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/JsonbType.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/JsonbType.java new file mode 100644 index 0000000000..872b17eb67 --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/JsonbType.java @@ -0,0 +1,228 @@ +/* + * Copyright 2021 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; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.node.ObjectNode; +import java.io.IOException; +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import org.hibernate.HibernateException; +import org.hibernate.type.AbstractSingleColumnStandardBasicType; +import org.hibernate.type.descriptor.ValueBinder; +import org.hibernate.type.descriptor.ValueExtractor; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.AbstractTypeDescriptor; +import org.hibernate.type.descriptor.java.JavaTypeDescriptor; +import org.hibernate.type.descriptor.java.MutableMutabilityPlan; +import org.hibernate.type.descriptor.sql.BasicBinder; +import org.hibernate.type.descriptor.sql.BasicExtractor; +import org.hibernate.type.descriptor.sql.SqlTypeDescriptor; +import org.keycloak.models.map.client.MapClientEntity; +import org.keycloak.models.map.client.MapProtocolMapperEntity; +import org.keycloak.models.map.client.MapProtocolMapperEntityImpl; +import org.keycloak.models.map.common.DeepCloner; +import org.keycloak.models.map.common.Serialization.IgnoreUpdatedMixIn; +import org.keycloak.models.map.common.Serialization.IgnoredTypeMixIn; +import org.keycloak.models.map.common.UpdatableEntity; +import org.keycloak.models.map.storage.jpa.client.entity.JpaClientMetadata; +import org.keycloak.models.map.storage.jpa.client.JpaClientMapStorage; + +public class JsonbType extends AbstractSingleColumnStandardBasicType { + + public static final JsonbType INSTANCE = new JsonbType(); + public static final ObjectMapper MAPPER = new ObjectMapper() + .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false) + .enable(SerializationFeature.INDENT_OUTPUT) + .setSerializationInclusion(JsonInclude.Include.NON_NULL) + .setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE) + .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY) + .activateDefaultTyping(new LaissezFaireSubTypeValidator(), ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT, JsonTypeInfo.As.PROPERTY) + .registerModule(new SimpleModule().addAbstractTypeMapping(MapProtocolMapperEntity.class, MapProtocolMapperEntityImpl.class)) + .addMixIn(UpdatableEntity.class, IgnoreUpdatedMixIn.class) + .addMixIn(DeepCloner.class, IgnoredTypeMixIn.class) + .addMixIn(MapClientEntity.class, IgnoredClientFieldsMixIn.class); + + abstract class IgnoredClientFieldsMixIn { + @JsonIgnore public abstract String getId(); + @JsonIgnore public abstract Map> getAttributes(); + } + + public JsonbType() { + super(JsonbSqlTypeDescriptor.INSTANCE, JsonbJavaTypeDescriptor.INSTANCE); + } + + @Override + public String getName() { + return "jsonb"; + } + + private static class JsonbSqlTypeDescriptor implements SqlTypeDescriptor { + + private static final JsonbSqlTypeDescriptor INSTANCE = new JsonbSqlTypeDescriptor(); + + @Override + public int getSqlType() { + return Types.OTHER; + } + + @Override + public boolean canBeRemapped() { + return true; + } + + @Override + public ValueBinder getBinder(JavaTypeDescriptor javaTypeDescriptor) { + return new BasicBinder(javaTypeDescriptor, this) { + @Override + protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException { + st.setObject(index, javaTypeDescriptor.unwrap(value, JsonNode.class, options), getSqlType()); + } + + @Override + protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) throws SQLException { + st.setObject(name, javaTypeDescriptor.unwrap(value, JsonNode.class, options), getSqlType()); + } + }; + } + + @Override + public ValueExtractor getExtractor(JavaTypeDescriptor javaTypeDescriptor) { + return new BasicExtractor(javaTypeDescriptor, this) { + @Override + protected X doExtract(ResultSet rs, String name, WrapperOptions options) throws SQLException { + return javaTypeDescriptor.wrap(extractJson(rs, name), options); + } + + @Override + protected X doExtract(CallableStatement statement, int index, WrapperOptions options) throws SQLException { + return javaTypeDescriptor.wrap(extractJson(statement, index), options); + } + + @Override + protected X doExtract(CallableStatement statement, String name, WrapperOptions options) throws SQLException { + return javaTypeDescriptor.wrap(extractJson(statement, name), options); + } + }; + } + + private Object extractJson(ResultSet rs, String name) throws SQLException { + return rs.getObject(name); + } + + private Object extractJson(CallableStatement statement, int index) throws SQLException { + return statement.getObject(index); + } + + private Object extractJson(CallableStatement statement, String name) throws SQLException { + return statement.getObject(name); + } + } + + private static class JsonbJavaTypeDescriptor extends AbstractTypeDescriptor { + + private static final JsonbJavaTypeDescriptor INSTANCE = new JsonbJavaTypeDescriptor(); + + public JsonbJavaTypeDescriptor() { + super(Object.class, new MutableMutabilityPlan() { + @Override + protected Object deepCopyNotNull(Object value) { + try { + return MAPPER.readValue(MAPPER.writerFor(value.getClass()).writeValueAsBytes(value), value.getClass()); + } catch (IOException e) { + throw new HibernateException("unable to deep copy object", e); + } + } + }); + } + + @Override + public Object fromString(String json) { + try { + ObjectNode tree = MAPPER.readValue(json, ObjectNode.class); + JsonNode ev = tree.get("entityVersion"); + if (ev == null || ! ev.isInt()) throw new IllegalArgumentException("unable to read entity version from " + json); + + int entityVersion = ev.asInt(); + + if (entityVersion > JpaClientMapStorage.SUPPORTED_VERSION + 1) throw new IllegalArgumentException("Incompatible entity version: " + entityVersion + ", supportedVersion: " + JpaClientMapStorage.SUPPORTED_VERSION); + + if (entityVersion < JpaClientMapStorage.SUPPORTED_VERSION) { + tree = JpaClientMigration.migrateTreeTo(entityVersion, JpaClientMapStorage.SUPPORTED_VERSION, tree); + } + return MAPPER.treeToValue(tree, JpaClientMetadata.class); + } catch (IOException e) { + throw new HibernateException("unable to read", e); + } + } + + @Override + public X unwrap(Object value, Class type, WrapperOptions options) { + if (value == null) return null; + + String stringValue = (value instanceof String) ? (String) value : toString(value); + try { + return (X) MAPPER.readTree(stringValue); + } catch (IOException e) { + throw new HibernateException("unable to read", e); + } + } + + @Override + public Object wrap(X value, WrapperOptions options) { + if (value == null) return null; + + return fromString(value.toString()); + } + + @Override + public String toString(Object value) { + try { + return MAPPER.writeValueAsString(value); + } catch (IOException e) { + throw new HibernateException("unable to tranform value: " + value + " as String.", e); + } + } + + @Override + public boolean areEqual(Object one, Object another) { + if (one == another) return true; + if (one == null || another == null) return Objects.equals(one, another); + try { + return MAPPER.readTree(toString(one)).equals( + MAPPER.readTree(toString(another))); + } catch (IOException e) { + throw new HibernateException("unable to perform areEqual", e); + } + } + } +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/updater/MapJpaUpdaterProvider.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/updater/MapJpaUpdaterProvider.java new file mode 100644 index 0000000000..4921a6401f --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/updater/MapJpaUpdaterProvider.java @@ -0,0 +1,71 @@ +/* + * Copyright 2021 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.updater; + +import org.keycloak.provider.Provider; + +import java.io.File; +import java.sql.Connection; + +public interface MapJpaUpdaterProvider extends Provider { + + /** + * Status of database up-to-dateness + */ + enum Status { + /** + * Database is valid and up to date + */ + VALID, + /** + * No database exists. + */ + EMPTY, + /** + * Database needs to be updated + */ + OUTDATED + } + + /** + * Updates the Keycloak database for the given model type + * @param modelType Model type + * @param connection DB connection + * @param defaultSchema DB connection + */ + void update(Class modelType, Connection connection, String defaultSchema); + + /** + * Checks whether Keycloak database for the given model type is up to date with the most recent changesets + * @param modelType Model type + * @param connection DB connection + * @param defaultSchema DB schema to use + * @return + */ + Status validate(Class modelType, Connection connection, String defaultSchema); + + /** + * Exports the SQL update script for the given model type into the given File. + * @param modelType Model type + * @param connection DB connection + * @param defaultSchema DB schema to use + * @param file File to write to + */ + void export(Class modelType, Connection connection, String defaultSchema, File file); + +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/updater/MapJpaUpdaterProviderFactory.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/updater/MapJpaUpdaterProviderFactory.java new file mode 100644 index 0000000000..3280fdc469 --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/updater/MapJpaUpdaterProviderFactory.java @@ -0,0 +1,23 @@ +/* + * Copyright 2021 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.updater; + +import org.keycloak.provider.ProviderFactory; + +public interface MapJpaUpdaterProviderFactory extends ProviderFactory { +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/updater/MapJpaUpdaterSpi.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/updater/MapJpaUpdaterSpi.java new file mode 100644 index 0000000000..e325d8f8c2 --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/updater/MapJpaUpdaterSpi.java @@ -0,0 +1,48 @@ +/* + * Copyright 2021 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.updater; + +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.Spi; + +public class MapJpaUpdaterSpi implements Spi { + + public final static String NAME = "mapJpaUpdater"; + + @Override + public boolean isInternal() { + return true; + } + + @Override + public String getName() { + return NAME; + } + + @Override + public Class getProviderClass() { + return MapJpaUpdaterProvider.class; + } + + @Override + public Class getProviderFactoryClass() { + return MapJpaUpdaterProviderFactory.class; + } + +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/updater/liquibase/MapJpaLiquibaseUpdaterProvider.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/updater/liquibase/MapJpaLiquibaseUpdaterProvider.java new file mode 100644 index 0000000000..743a39456d --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/updater/liquibase/MapJpaLiquibaseUpdaterProvider.java @@ -0,0 +1,183 @@ +/* + * Copyright 2021 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.updater.liquibase; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Writer; +import java.lang.reflect.Method; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; +import liquibase.Contexts; +import liquibase.LabelExpression; +import liquibase.Liquibase; +import liquibase.changelog.ChangeSet; +import liquibase.changelog.RanChangeSet; +import liquibase.exception.LiquibaseException; +import org.jboss.logging.Logger; +import org.keycloak.common.util.reflections.Reflections; +import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionProvider; +import org.keycloak.connections.jpa.updater.liquibase.ThreadLocalSessionContext; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.map.storage.ModelEntityUtil; +import org.keycloak.models.map.storage.jpa.updater.MapJpaUpdaterProvider; +import org.keycloak.models.map.storage.jpa.updater.MapJpaUpdaterProvider.Status; + +public class MapJpaLiquibaseUpdaterProvider implements MapJpaUpdaterProvider { + + private static final Logger logger = Logger.getLogger(MapJpaLiquibaseUpdaterProvider.class); + + private final KeycloakSession session; + + public MapJpaLiquibaseUpdaterProvider(KeycloakSession session) { + this.session = session; + } + + @Override + public void update(Class modelType, Connection connection, String defaultSchema) { + update(modelType, connection, null, defaultSchema); + } + + @Override + public void export(Class modelType, Connection connection, String defaultSchema, File file) { + update(modelType, connection, file, defaultSchema); + } + + private void update(Class modelType, Connection connection, File file, String defaultSchema) { + logger.debug("Starting database update"); + + // Need ThreadLocal as liquibase doesn't seem to have API to inject custom objects into tasks + ThreadLocalSessionContext.setCurrentSession(session); + + Writer exportWriter = null; + try { + Liquibase liquibase = getLiquibase(modelType, connection, defaultSchema); + if (file != null) { + exportWriter = new FileWriter(file); + } + + updateChangeSet(liquibase, connection); + + } catch (LiquibaseException | IOException | SQLException e) { + logger.error("Error has occurred while updating the database", e); + throw new RuntimeException("Failed to update database", e); + } finally { + ThreadLocalSessionContext.removeCurrentSession(); + if (exportWriter != null) { + try { + exportWriter.close(); + } catch (IOException ioe) { + // ignore + } + } + } + } + + protected void updateChangeSet(Liquibase liquibase, Connection connection) throws LiquibaseException, SQLException { + String changelog = liquibase.getChangeLogFile(); + List changeSets = getLiquibaseUnrunChangeSets(liquibase); + if (!changeSets.isEmpty()) { + List ranChangeSets = liquibase.getDatabase().getRanChangeSetList(); + if (ranChangeSets.isEmpty()) { + logger.infov("Initializing database schema. Using changelog {0}", changelog); + } else { + if (logger.isDebugEnabled()) { + logger.debugv("Updating database from {0} to {1}. Using changelog {2}", ranChangeSets.get(ranChangeSets.size() - 1).getId(), changeSets.get(changeSets.size() - 1).getId(), changelog); + } else { + logger.infov("Updating database. Using changelog {0}", changelog); + } + } + + liquibase.update((Contexts) null); + + logger.debugv("Completed database update for changelog {0}", changelog); + } else { + logger.debugv("Database is up to date for changelog {0}", changelog); + } + + } + + @Override + public Status validate(Class modelType, Connection connection, String defaultSchema) { + logger.debug("Validating if database is updated"); + ThreadLocalSessionContext.setCurrentSession(session); + + try { + Liquibase liquibase = getLiquibase(modelType, connection, defaultSchema); + + Status status = validateChangeSet(liquibase, liquibase.getChangeLogFile()); + if (status != Status.VALID) { + return status; + } + + } catch (LiquibaseException e) { + throw new RuntimeException("Failed to validate database", e); + } + + return Status.VALID; + } + + protected Status validateChangeSet(Liquibase liquibase, String changelog) throws LiquibaseException { + final Status result; + List changeSets = getLiquibaseUnrunChangeSets(liquibase); + + if (!changeSets.isEmpty()) { + if (changeSets.size() == liquibase.getDatabaseChangeLog().getChangeSets().size()) { + result = Status.EMPTY; + } else { + logger.debugf("Validation failed. Database is not up-to-date for changelog %s", changelog); + result = Status.OUTDATED; + } + } else { + logger.debugf("Validation passed. Database is up-to-date for changelog %s", changelog); + result = Status.VALID; + } + + return result; + } + + @SuppressWarnings("unchecked") + private List getLiquibaseUnrunChangeSets(Liquibase liquibase) { + // TODO tracked as: https://issues.jboss.org/browse/KEYCLOAK-3730 + // TODO: When https://liquibase.jira.com/browse/CORE-2919 is resolved, replace the following two lines with: + // List changeSets = liquibase.listUnrunChangeSets((Contexts) null, new LabelExpression(), false); + Method listUnrunChangeSets = Reflections.findDeclaredMethod(Liquibase.class, "listUnrunChangeSets", Contexts.class, LabelExpression.class, boolean.class); + return Reflections.invokeMethod(true, listUnrunChangeSets, List.class, liquibase, (Contexts) null, new LabelExpression(), false); + } + + private Liquibase getLiquibase(Class modelType, Connection connection, String defaultSchema) throws LiquibaseException { + LiquibaseConnectionProvider liquibaseProvider = session.getProvider(LiquibaseConnectionProvider.class); + String changelog = "META-INF/jpa-" + ModelEntityUtil.getModelName(modelType) + "-changelog.xml"; + if (changelog == null) { + throw new IllegalStateException("Cannot find changlelog for modelClass " + modelType.getName()); + } + + return liquibaseProvider.getLiquibaseForCustomUpdate(connection, defaultSchema, changelog, this.getClass().getClassLoader(), "databasechangelog"); + } + + @Override + public void close() { + } + + public static String getTable(String table, String defaultSchema) { + return defaultSchema != null ? defaultSchema + "." + table : table; + } + +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/updater/liquibase/MapJpaLiquibaseUpdaterProviderFactory.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/updater/liquibase/MapJpaLiquibaseUpdaterProviderFactory.java new file mode 100644 index 0000000000..ca79063c3a --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/updater/liquibase/MapJpaLiquibaseUpdaterProviderFactory.java @@ -0,0 +1,53 @@ +/* + * Copyright 2021 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.updater.liquibase; + +import org.keycloak.Config; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.map.storage.jpa.updater.MapJpaUpdaterProvider; +import org.keycloak.models.map.storage.jpa.updater.MapJpaUpdaterProviderFactory; + +public class MapJpaLiquibaseUpdaterProviderFactory implements MapJpaUpdaterProviderFactory { + + public static final String PROVIDER_ID = "map-liquibase-updater"; + + @Override + public MapJpaUpdaterProvider create(KeycloakSession session) { + return new MapJpaLiquibaseUpdaterProvider(session); + } + + @Override + public void init(Config.Scope config) { + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public void close() { + } + + @Override + public String getId() { + return PROVIDER_ID; + } + +} diff --git a/model/map-jpa/src/main/resources/META-INF/jpa-clients-changelog-1.xml b/model/map-jpa/src/main/resources/META-INF/jpa-clients-changelog-1.xml new file mode 100644 index 0000000000..bcdd0299e8 --- /dev/null +++ b/model/map-jpa/src/main/resources/META-INF/jpa-clients-changelog-1.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + create table client ( + id uuid primary key not null, + entityVersion integer generated always as ((metadata->>'entityVersion')::int) stored, + realmId varchar(36) generated always as (metadata->>'fRealmId') stored, + clientId varchar(255) generated always as (metadata->>'fClientId') stored, + protocol varchar(36) generated always as (metadata->>'fProtocol') stored, + enabled boolean generated always as ((metadata->>'fEnabled')::boolean) stored, + metadata jsonb + ); + + create index client_entityVersion on client(entityVersion); + create index client_realmId_clientId on client(realmId, clientId); + create index client_scopeMappings on client using gin ((metadata->'fScopeMappings') jsonb_path_ops); + + create table client_attribute ( + id uuid primary key not null, + fk_client uuid references client(id) on delete cascade, + name varchar(255), + value text + ); + + create index client_attr_fk_client on client_attribute(fk_client); + create index client_attr_name_value on client_attribute(name, (value::varchar(250))); + + + + + diff --git a/model/map-jpa/src/main/resources/META-INF/jpa-clients-changelog.xml b/model/map-jpa/src/main/resources/META-INF/jpa-clients-changelog.xml new file mode 100644 index 0000000000..ef754bdcd7 --- /dev/null +++ b/model/map-jpa/src/main/resources/META-INF/jpa-clients-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 new file mode 100644 index 0000000000..00f33353a3 --- /dev/null +++ b/model/map-jpa/src/main/resources/META-INF/persistence.xml @@ -0,0 +1,7 @@ + + + + org.keycloak.models.map.storage.jpa.client.entity.JpaClientEntity + org.keycloak.models.map.storage.jpa.client.entity.JpaClientAttributeEntity + + diff --git a/model/map-jpa/src/main/resources/META-INF/services/org.keycloak.models.map.storage.MapStorageProviderFactory b/model/map-jpa/src/main/resources/META-INF/services/org.keycloak.models.map.storage.MapStorageProviderFactory new file mode 100644 index 0000000000..e717f726df --- /dev/null +++ b/model/map-jpa/src/main/resources/META-INF/services/org.keycloak.models.map.storage.MapStorageProviderFactory @@ -0,0 +1,18 @@ +# +# Copyright 2021 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. +# + +org.keycloak.models.map.storage.jpa.client.JpaClientMapStorageProviderFactory diff --git a/model/map-jpa/src/main/resources/META-INF/services/org.keycloak.models.map.storage.jpa.updater.MapJpaUpdaterProviderFactory b/model/map-jpa/src/main/resources/META-INF/services/org.keycloak.models.map.storage.jpa.updater.MapJpaUpdaterProviderFactory new file mode 100644 index 0000000000..84cb2376bd --- /dev/null +++ b/model/map-jpa/src/main/resources/META-INF/services/org.keycloak.models.map.storage.jpa.updater.MapJpaUpdaterProviderFactory @@ -0,0 +1,18 @@ +# +# Copyright 2021 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. +# + +org.keycloak.models.map.storage.jpa.updater.liquibase.MapJpaLiquibaseUpdaterProviderFactory diff --git a/model/map-jpa/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/model/map-jpa/src/main/resources/META-INF/services/org.keycloak.provider.Spi new file mode 100644 index 0000000000..a6ae8657b7 --- /dev/null +++ b/model/map-jpa/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -0,0 +1,18 @@ +# +# Copyright 2021 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. +# + +org.keycloak.models.map.storage.jpa.updater.MapJpaUpdaterSpi diff --git a/model/map/src/main/java/org/keycloak/models/map/client/MapClientProvider.java b/model/map/src/main/java/org/keycloak/models/map/client/MapClientProvider.java index c73a77d20a..a88bfbd9e6 100644 --- a/model/map/src/main/java/org/keycloak/models/map/client/MapClientProvider.java +++ b/model/map/src/main/java/org/keycloak/models/map/client/MapClientProvider.java @@ -363,7 +363,7 @@ public class MapClientProvider implements ClientProvider { @Override public void close() { - + } } diff --git a/model/map/src/main/java/org/keycloak/models/map/common/Serialization.java b/model/map/src/main/java/org/keycloak/models/map/common/Serialization.java index c6781f906a..a10a25a2b2 100644 --- a/model/map/src/main/java/org/keycloak/models/map/common/Serialization.java +++ b/model/map/src/main/java/org/keycloak/models/map/common/Serialization.java @@ -16,14 +16,11 @@ */ package org.keycloak.models.map.common; -import org.keycloak.common.util.reflections.Reflections; import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreType; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.annotation.JsonTypeInfo.As; -import com.fasterxml.jackson.annotation.JsonTypeInfo.Id; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; @@ -35,10 +32,7 @@ import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.type.TypeFactory; import com.fasterxml.jackson.datatype.jdk8.StreamSerializer; import java.io.IOException; -import java.lang.reflect.Field; import java.util.concurrent.ConcurrentHashMap; -import java.util.logging.Level; -import java.util.logging.Logger; import java.util.stream.Stream; /** @@ -62,9 +56,9 @@ public class Serialization { public static final ConcurrentHashMap, ObjectWriter> WRITERS = new ConcurrentHashMap<>(); @JsonIgnoreType - class IgnoredTypeMixIn {} + public class IgnoredTypeMixIn {} - abstract class IgnoreUpdatedMixIn { + public abstract class IgnoreUpdatedMixIn { @JsonIgnore public abstract boolean isUpdated(); } diff --git a/model/pom.xml b/model/pom.xml index e2af09a965..da734c2b77 100755 --- a/model/pom.xml +++ b/model/pom.xml @@ -32,6 +32,7 @@ jpa + map-jpa infinispan map build-processor diff --git a/pom.xml b/pom.xml index f204c9e24c..f83990dc68 100644 --- a/pom.xml +++ b/pom.xml @@ -1283,6 +1283,11 @@ keycloak-model-map ${project.version} + + org.keycloak + keycloak-model-map-jpa + ${project.version} + org.keycloak keycloak-model-infinispan diff --git a/server-spi/src/main/java/org/keycloak/models/ClientModel.java b/server-spi/src/main/java/org/keycloak/models/ClientModel.java index 772e43bcef..ed39e764d4 100755 --- a/server-spi/src/main/java/org/keycloak/models/ClientModel.java +++ b/server-spi/src/main/java/org/keycloak/models/ClientModel.java @@ -50,7 +50,7 @@ public interface ClientModel extends ClientScopeModel, RoleContainerModel, Prot public static final SearchableModelField ALWAYS_DISPLAY_IN_CONSOLE = new SearchableModelField<>("alwaysDisplayInConsole", Boolean.class); /** - * Search for attribute value. The parameters is a pair {@code (attribute_name, values...)} where {@code attribute_name} + * Search for attribute value. The parameters is a pair {@code (attribute_name, value)} where {@code attribute_name} * is always checked for equality, and the value is checked per the operator. */ public static final SearchableModelField ATTRIBUTE = new SearchableModelField<>("attribute", String[].class); diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json index 9f4ec5d403..a0cbef575e 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json @@ -69,7 +69,15 @@ "provider": "${keycloak.client.provider:jpa}", "map": { "storage": { - "provider": "${keycloak.client.map.storage.provider:concurrenthashmap}" + "provider": "${keycloak.client.map.storage.provider:concurrenthashmap}", + "jpa-client-map-storage": { + "url": "${keycloak.client.map.storage.connectionsJpa.url:}", + "driver": "org.postgresql.Driver", + "driverDialect": "org.keycloak.models.map.jpa.hibernate.dialect.JsonbPostgreSQL95Dialect", + "user": "${keycloak.client.map.storage.connectionsJpa.user:}", + "password": "${keycloak.client.map.storage.connectionsJpa.password:}", + "showSql": "${keycloak.client.map.storage.connectionsJpa,showSql:false}" + } } } }, diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/log4j.properties b/testsuite/integration-arquillian/tests/base/src/test/resources/log4j.properties index e200365114..5e90666afd 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/log4j.properties +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/log4j.properties @@ -86,7 +86,7 @@ log4j.logger.org.keycloak.services.clientregistration.policy=debug ## Enable SQL debugging # Enable logs the SQL statements -#log4j.logger.org.hibernate.SQL=debug +#log4j.logger.org.hibernate.SQL=debug # Enable logs the JDBC parameters passed to a query #log4j.logger.org.hibernate.type=trace diff --git a/testsuite/utils/pom.xml b/testsuite/utils/pom.xml index 8d0094d981..9812c62475 100755 --- a/testsuite/utils/pom.xml +++ b/testsuite/utils/pom.xml @@ -293,7 +293,7 @@ keycloak.group.providermap keycloak.role.providermap keycloak.user.providermap - keycloak.serverInfo.providermap + keycloak.deploymentState.providermap keycloak.authSession.providermap keycloak.userSession.providermap keycloak.loginFailure.providermap diff --git a/testsuite/utils/src/main/resources/META-INF/keycloak-server.json b/testsuite/utils/src/main/resources/META-INF/keycloak-server.json index dcd3201bf4..b3bed71642 100755 --- a/testsuite/utils/src/main/resources/META-INF/keycloak-server.json +++ b/testsuite/utils/src/main/resources/META-INF/keycloak-server.json @@ -26,23 +26,56 @@ }, "realm": { - "provider": "${keycloak.realm.provider:jpa}" + "provider": "${keycloak.realm.provider:jpa}", + "map": { + "storage": { + "provider": "${keycloak.realm.map.storage.provider:concurrenthashmap}" + } + } }, "client": { - "provider": "${keycloak.client.provider:jpa}" + "provider": "${keycloak.client.provider:jpa}", + "map": { + "storage": { + "provider": "${keycloak.client.map.storage.provider:concurrenthashmap}", + "jpa-client-map-storage": { + "url": "${keycloak.client.map.storage.connectionsJpa.url:}", + "driver": "org.postgresql.Driver", + "driverDialect": "org.keycloak.models.map.jpa.hibernate.dialect.JsonbPostgreSQL95Dialect", + "user": "${keycloak.client.map.storage.connectionsJpa.user:}", + "password": "${keycloak.client.map.storage.connectionsJpa.password:}", + "showSql": "${keycloak.client.map.storage.connectionsJpa,showSql:false}" + } + } + } }, "clientScope": { - "provider": "${keycloak.clientScope.provider:jpa}" + "provider": "${keycloak.clientScope.provider:jpa}", + "map": { + "storage": { + "provider": "${keycloak.clientScope.map.storage.provider:concurrenthashmap}" + } + } }, "group": { - "provider": "${keycloak.group.provider:jpa}" + "provider": "${keycloak.group.provider:jpa}", + "map": { + "storage": { + "provider": "${keycloak.group.map.storage.provider:concurrenthashmap}" + } + } }, "role": { - "provider": "${keycloak.role.provider:jpa}" + "provider": "${keycloak.role.provider:jpa}", + "map": { + "storage": { + "provider": "${keycloak.role.map.storage.provider:concurrenthashmap}" + } + } }, "authenticationSessions": { @@ -70,7 +103,12 @@ }, "user": { - "provider": "${keycloak.user.provider:jpa}" + "provider": "${keycloak.user.provider:jpa}", + "map": { + "storage": { + "provider": "${keycloak.user.map.storage.provider:concurrenthashmap}" + } + } }, "userFederatedStorage": {