From f6a2b334d1e0652dcaba76cf8fccdc464a3171cd Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Thu, 14 Jul 2022 17:47:51 -0300 Subject: [PATCH] Integrate the JPA map store (#13097) Co-authored-by: Alexander Schwartz --- .github/workflows/ci.yml | 2 +- .../storage/jpa/EventListenerIntegrator.java | 55 +++++++ .../jpa/JpaMapStorageProviderFactory.java | 137 ++++++++---------- .../jpa/KeycloakIntegratorProvider.java | 32 ++++ ...ce.xml => default-map-jpa-persistence.xml} | 19 ++- .../org/keycloak/config/DatabaseOptions.java | 5 +- .../java/org/keycloak/config/MultiOption.java | 1 - .../org/keycloak/config/OptionBuilder.java | 1 - .../org/keycloak/config/StorageOptions.java | 1 + ...ngBuildSteps.java => CacheBuildSteps.java} | 2 +- .../quarkus/deployment/IsJpaStoreEnabled.java | 42 ++++++ .../deployment/IsLegacyStoreEnabled.java | 7 +- .../quarkus/deployment/KeycloakProcessor.java | 87 +++++++---- quarkus/runtime/pom.xml | 10 ++ .../runtime/cli/command/ShowConfig.java | 23 ++- .../runtime/configuration/Configuration.java | 4 +- .../mappers/DatabasePropertyMappers.java | 56 ++++++- .../configuration/mappers/PropertyMapper.java | 2 +- .../mappers/StoragePropertyMappers.java | 25 +++- .../mappers/TransactionPropertyMappers.java | 9 ++ .../QuarkusJpaMapStorageProviderFactory.java | 95 ++++++++++++ .../QuarkusJtaTransactionManagerLookup.java | 9 +- ...dels.map.storage.MapStorageProviderFactory | 18 +++ .../configuration/test/ConfigurationTest.java | 3 +- .../it/junit5/extension/CLITestExtension.java | 9 +- .../it/junit5/extension/WithDatabase.java | 5 + .../it/storage/map/JPAStoreDistTest.java | 40 +++++ ...ommandTest.testBuildHelp.unix.approved.txt | 4 +- ...andTest.testBuildHelp.windows.approved.txt | 5 +- ...Test.testStartDevHelpAll.unix.approved.txt | 4 +- ...t.testStartDevHelpAll.windows.approved.txt | 5 +- 31 files changed, 573 insertions(+), 144 deletions(-) create mode 100644 model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/EventListenerIntegrator.java create mode 100644 model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/KeycloakIntegratorProvider.java rename model/map-jpa/src/main/resources/{META-INF/persistence.xml => default-map-jpa-persistence.xml} (84%) rename quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/{CLusteringBuildSteps.java => CacheBuildSteps.java} (98%) create mode 100644 quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/IsJpaStoreEnabled.java create mode 100644 quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/database/jpa/QuarkusJpaMapStorageProviderFactory.java create mode 100644 quarkus/runtime/src/main/resources/META-INF/services/org.keycloak.models.map.storage.MapStorageProviderFactory create mode 100644 quarkus/tests/integration/src/test/java/org/keycloak/it/storage/map/JPAStoreDistTest.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a740b0f3a5..0ff2afb3748 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -492,7 +492,7 @@ jobs: - name: Run Quarkus Storage Tests run: | - ./mvnw clean install -nsu -B -f quarkus/tests/pom.xml -Ptest-database -Dtest=PostgreSQLDistTest,DatabaseOptionsDistTest | misc/log/trimmer.sh + ./mvnw clean install -nsu -B -f quarkus/tests/pom.xml -Ptest-database -Dtest=PostgreSQLDistTest,DatabaseOptionsDistTest,JPAStoreDistTest | misc/log/trimmer.sh TEST_RESULT=${PIPESTATUS[0]} find . -path '*/target/surefire-reports/*.xml' | zip -q reports-quarkus-tests.zip -@ exit $TEST_RESULT diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/EventListenerIntegrator.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/EventListenerIntegrator.java new file mode 100644 index 00000000000..22788cf4064 --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/EventListenerIntegrator.java @@ -0,0 +1,55 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.models.map.storage.jpa; + +import org.hibernate.boot.Metadata; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.event.service.spi.EventListenerRegistry; +import org.hibernate.event.spi.EventType; +import org.hibernate.integrator.spi.Integrator; +import org.hibernate.service.spi.SessionFactoryServiceRegistry; +import org.keycloak.models.map.storage.jpa.hibernate.listeners.JpaAutoFlushListener; +import org.keycloak.models.map.storage.jpa.hibernate.listeners.JpaEntityVersionListener; +import org.keycloak.models.map.storage.jpa.hibernate.listeners.JpaOptimisticLockingListener; + +public class EventListenerIntegrator implements Integrator { + + @Override + public void integrate(Metadata metadata, SessionFactoryImplementor sessionFactoryImplementor, + SessionFactoryServiceRegistry sessionFactoryServiceRegistry) { + final EventListenerRegistry eventListenerRegistry = + sessionFactoryServiceRegistry.getService( EventListenerRegistry.class ); + + eventListenerRegistry.appendListeners(EventType.PRE_INSERT, JpaOptimisticLockingListener.INSTANCE); + eventListenerRegistry.appendListeners(EventType.PRE_UPDATE, JpaOptimisticLockingListener.INSTANCE); + eventListenerRegistry.appendListeners(EventType.PRE_DELETE, JpaOptimisticLockingListener.INSTANCE); + + eventListenerRegistry.appendListeners(EventType.PRE_INSERT, JpaEntityVersionListener.INSTANCE); + eventListenerRegistry.appendListeners(EventType.PRE_UPDATE, JpaEntityVersionListener.INSTANCE); + eventListenerRegistry.appendListeners(EventType.PRE_DELETE, JpaEntityVersionListener.INSTANCE); + + // replace auto-flush listener + eventListenerRegistry.setListeners(EventType.AUTO_FLUSH, JpaAutoFlushListener.INSTANCE); + } + + @Override + public void disintegrate(SessionFactoryImplementor sessionFactoryImplementor, + SessionFactoryServiceRegistry sessionFactoryServiceRegistry) { + + } +} diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaMapStorageProviderFactory.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaMapStorageProviderFactory.java index dba49e987af..f3e66b6c3ce 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaMapStorageProviderFactory.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaMapStorageProviderFactory.java @@ -22,7 +22,6 @@ import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.DriverManager; import java.sql.SQLException; -import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; @@ -35,16 +34,12 @@ import javax.naming.InitialContext; import javax.naming.NamingException; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; -import javax.persistence.Persistence; import javax.sql.DataSource; -import org.hibernate.boot.Metadata; import org.hibernate.cfg.AvailableSettings; -import org.hibernate.engine.spi.SessionFactoryImplementor; -import org.hibernate.event.service.spi.EventListenerRegistry; -import org.hibernate.event.spi.EventType; -import org.hibernate.jpa.boot.spi.IntegratorProvider; -import org.hibernate.service.spi.SessionFactoryServiceRegistry; +import org.hibernate.jpa.boot.internal.ParsedPersistenceXmlDescriptor; +import org.hibernate.jpa.boot.internal.PersistenceXmlParser; +import org.hibernate.jpa.boot.spi.Bootstrap; import org.jboss.logging.Logger; import org.keycloak.Config; import org.keycloak.authorization.model.PermissionTicket; @@ -59,7 +54,6 @@ import org.keycloak.component.AmphibianProviderFactory; import org.keycloak.events.Event; import org.keycloak.events.admin.AdminEvent; import org.keycloak.models.ActionTokenValueModel; -import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientScopeModel; import org.keycloak.models.GroupModel; @@ -158,6 +152,14 @@ public class JpaMapStorageProviderFactory implements public static final String HIBERNATE_DEFAULT_SCHEMA = "hibernate.default_schema"; + public static Map configureHibernateProperties() { + Map properties = new HashMap<>(); + + properties.put("hibernate.integrator_provider", new KeycloakIntegratorProvider()); + + return properties; + } + private volatile EntityManagerFactory emf; private final Set> validatedModels = ConcurrentHashMap.newKeySet(); private Config.Scope config; @@ -304,78 +306,59 @@ public class JpaMapStorageProviderFactory implements if (emf == null) { synchronized (this) { if (emf == null) { - logger.debugf("Initializing JPA connections %s", StackUtil.getShortStackTrace()); - - Map properties = new HashMap<>(); - - String unitName = config.get("persistenceUnitName", "keycloak-jpa-default"); - - 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(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")); - - properties.put( - "hibernate.integrator_provider", - (IntegratorProvider) () -> Collections.singletonList( - new org.hibernate.integrator.spi.Integrator() { - - @Override - public void integrate(Metadata metadata, SessionFactoryImplementor sessionFactoryImplementor, - SessionFactoryServiceRegistry sessionFactoryServiceRegistry) { - final EventListenerRegistry eventListenerRegistry = - sessionFactoryServiceRegistry.getService( EventListenerRegistry.class ); - - eventListenerRegistry.appendListeners(EventType.PRE_INSERT, JpaOptimisticLockingListener.INSTANCE); - eventListenerRegistry.appendListeners(EventType.PRE_UPDATE, JpaOptimisticLockingListener.INSTANCE); - eventListenerRegistry.appendListeners(EventType.PRE_DELETE, JpaOptimisticLockingListener.INSTANCE); - - eventListenerRegistry.appendListeners(EventType.PRE_INSERT, JpaEntityVersionListener.INSTANCE); - eventListenerRegistry.appendListeners(EventType.PRE_UPDATE, JpaEntityVersionListener.INSTANCE); - eventListenerRegistry.appendListeners(EventType.PRE_DELETE, JpaEntityVersionListener.INSTANCE); - - // replace auto-flush listener - eventListenerRegistry.setListeners(EventType.AUTO_FLUSH, JpaAutoFlushListener.INSTANCE); - } - - @Override - public void disintegrate(SessionFactoryImplementor sessionFactoryImplementor, - SessionFactoryServiceRegistry sessionFactoryServiceRegistry) { - - } - } - ) - ); - - logger.trace("Creating EntityManagerFactory"); - this.emf = Persistence.createEntityManagerFactory(unitName, properties); - logger.trace("EntityManagerFactory created"); + this.emf = createEntityManagerFactory(); } } } } + protected EntityManagerFactory createEntityManagerFactory() { + logger.debugf("Initializing JPA connections %s", StackUtil.getShortStackTrace()); + + Map properties = new HashMap<>(); + 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(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")); + + properties.putAll(configureHibernateProperties()); + + logger.trace("Creating EntityManagerFactory"); + ParsedPersistenceXmlDescriptor descriptor = PersistenceXmlParser.locateIndividualPersistenceUnit( + JpaMapStorageProviderFactory.class.getClassLoader() + .getResource("default-map-jpa-persistence.xml")); + EntityManagerFactory emf = Bootstrap.getEntityManagerFactoryBuilder(descriptor, properties).build(); + logger.trace("EntityManagerFactory created"); + + return emf; + } + + protected EntityManagerFactory getEntityManagerFactory() { + return emf; + } + public void validateAndUpdateSchema(KeycloakSession session, Class modelType) { /* For authz - there is validation run 5 times. For each authz model class separately. @@ -414,7 +397,7 @@ public class JpaMapStorageProviderFactory implements } } - private Connection getConnection() { + protected Connection getConnection() { try { String dataSourceLookup = config.get("dataSource"); if (dataSourceLookup != null) { diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/KeycloakIntegratorProvider.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/KeycloakIntegratorProvider.java new file mode 100644 index 00000000000..25fa7743546 --- /dev/null +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/KeycloakIntegratorProvider.java @@ -0,0 +1,32 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.models.map.storage.jpa; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.hibernate.integrator.spi.Integrator; +import org.hibernate.jpa.boot.spi.IntegratorProvider; + +public class KeycloakIntegratorProvider implements IntegratorProvider { + + @Override + public List getIntegrators() { + return new ArrayList<>(Collections.singletonList(new EventListenerIntegrator())); + } +} diff --git a/model/map-jpa/src/main/resources/META-INF/persistence.xml b/model/map-jpa/src/main/resources/default-map-jpa-persistence.xml similarity index 84% rename from model/map-jpa/src/main/resources/META-INF/persistence.xml rename to model/map-jpa/src/main/resources/default-map-jpa-persistence.xml index fa3167d157b..7e2cb2a0101 100644 --- a/model/map-jpa/src/main/resources/META-INF/persistence.xml +++ b/model/map-jpa/src/main/resources/default-map-jpa-persistence.xml @@ -1,6 +1,23 @@ + + - + org.keycloak.models.map.storage.jpa.authSession.entity.JpaRootAuthenticationSessionEntity org.keycloak.models.map.storage.jpa.authSession.entity.JpaAuthenticationSessionEntity diff --git a/quarkus/config-api/src/main/java/org/keycloak/config/DatabaseOptions.java b/quarkus/config-api/src/main/java/org/keycloak/config/DatabaseOptions.java index b97143e2409..d7fed4f179b 100644 --- a/quarkus/config-api/src/main/java/org/keycloak/config/DatabaseOptions.java +++ b/quarkus/config-api/src/main/java/org/keycloak/config/DatabaseOptions.java @@ -19,9 +19,10 @@ public class DatabaseOptions { .defaultValue(Database.getDriver("dev-file", true).get()) .build(); - public static final Option DB = new OptionBuilder<>("db", Database.Vendor.class) + public static final Option DB = new OptionBuilder<>("db", String.class) .category(OptionCategory.DATABASE) - .description("The database vendor. Possible values are: " + String.join(", ", Database.getAliases())) + .description(String.format("The database vendor. Possible values are: %s.", String.join(", ", Database.getAliases()))) + .defaultValue("dev-file") .expectedStringValues(Database.getAliases()) .buildTime(true) .build(); diff --git a/quarkus/config-api/src/main/java/org/keycloak/config/MultiOption.java b/quarkus/config-api/src/main/java/org/keycloak/config/MultiOption.java index 52e1117cc69..353b03899c1 100644 --- a/quarkus/config-api/src/main/java/org/keycloak/config/MultiOption.java +++ b/quarkus/config-api/src/main/java/org/keycloak/config/MultiOption.java @@ -2,7 +2,6 @@ package org.keycloak.config; import java.util.List; import java.util.Optional; -import java.util.Set; public class MultiOption extends Option { diff --git a/quarkus/config-api/src/main/java/org/keycloak/config/OptionBuilder.java b/quarkus/config-api/src/main/java/org/keycloak/config/OptionBuilder.java index 4a8ee081236..a00f504a009 100644 --- a/quarkus/config-api/src/main/java/org/keycloak/config/OptionBuilder.java +++ b/quarkus/config-api/src/main/java/org/keycloak/config/OptionBuilder.java @@ -4,7 +4,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Optional; -import java.util.Set; import java.util.stream.Collectors; public class OptionBuilder { diff --git a/quarkus/config-api/src/main/java/org/keycloak/config/StorageOptions.java b/quarkus/config-api/src/main/java/org/keycloak/config/StorageOptions.java index 9c3c4456c97..8091aeb4985 100644 --- a/quarkus/config-api/src/main/java/org/keycloak/config/StorageOptions.java +++ b/quarkus/config-api/src/main/java/org/keycloak/config/StorageOptions.java @@ -26,6 +26,7 @@ public class StorageOptions { public enum StorageType { + jpa("jpa"), chm("concurrenthashmap"); private final String provider; diff --git a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/CLusteringBuildSteps.java b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/CacheBuildSteps.java similarity index 98% rename from quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/CLusteringBuildSteps.java rename to quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/CacheBuildSteps.java index dc7c35db099..1d341cac139 100644 --- a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/CLusteringBuildSteps.java +++ b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/CacheBuildSteps.java @@ -38,7 +38,7 @@ import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.ShutdownContextBuildItem; -public class CLusteringBuildSteps { +public class CacheBuildSteps { @Consume(KeycloakSessionFactoryPreInitBuildItem.class) @Record(ExecutionTime.RUNTIME_INIT) diff --git a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/IsJpaStoreEnabled.java b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/IsJpaStoreEnabled.java new file mode 100644 index 00000000000..8bcf8928c61 --- /dev/null +++ b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/IsJpaStoreEnabled.java @@ -0,0 +1,42 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.quarkus.deployment; + +import static org.keycloak.config.StorageOptions.STORAGE; +import static org.keycloak.quarkus.runtime.configuration.Configuration.getOptionalValue; +import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX; + +import java.util.Optional; +import java.util.function.BooleanSupplier; +import org.keycloak.config.StorageOptions; + +public class IsJpaStoreEnabled implements BooleanSupplier { + + @Override + public boolean getAsBoolean() { + Optional storage = getOptionalValue(NS_KEYCLOAK_PREFIX.concat(STORAGE.getKey())); + + if (storage.isEmpty()) { + // legacy store + return true; + } + + return StorageOptions.StorageType.jpa.name().equals(storage.orElse(null)); + } + +} diff --git a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/IsLegacyStoreEnabled.java b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/IsLegacyStoreEnabled.java index f1acf7e1d73..bfed5a2e6ef 100644 --- a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/IsLegacyStoreEnabled.java +++ b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/IsLegacyStoreEnabled.java @@ -18,19 +18,16 @@ package org.keycloak.quarkus.deployment; import static org.keycloak.config.StorageOptions.STORAGE; -import static org.keycloak.quarkus.runtime.configuration.Configuration.getOptionalBooleanValue; +import static org.keycloak.quarkus.runtime.configuration.Configuration.getOptionalValue; import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX; import java.util.function.BooleanSupplier; -import org.keycloak.config.StorageOptions; -import org.keycloak.quarkus.runtime.Environment; -import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider; public class IsLegacyStoreEnabled implements BooleanSupplier { @Override public boolean getAsBoolean() { - return getOptionalBooleanValue(NS_KEYCLOAK_PREFIX.concat(STORAGE.getKey())).isEmpty(); + return getOptionalValue(NS_KEYCLOAK_PREFIX.concat(STORAGE.getKey())).isEmpty(); } } diff --git a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java index 6e7991a2917..92747dc9c07 100644 --- a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java +++ b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java @@ -63,7 +63,6 @@ import io.quarkus.deployment.builditem.GeneratedResourceBuildItem; import io.quarkus.deployment.builditem.HotDeploymentWatchedFileBuildItem; import io.quarkus.deployment.builditem.IndexDependencyBuildItem; import io.quarkus.deployment.builditem.LaunchModeBuildItem; -import io.quarkus.deployment.builditem.RuntimeConfigSetupCompleteBuildItem; import io.quarkus.deployment.builditem.StaticInitConfigSourceProviderBuildItem; import io.quarkus.hibernate.orm.deployment.AdditionalJpaModelBuildItem; import io.quarkus.hibernate.orm.deployment.HibernateOrmConfig; @@ -89,8 +88,11 @@ import org.jboss.logging.Logger; import org.jboss.resteasy.plugins.server.servlet.ResteasyContextParameters; import org.jboss.resteasy.spi.ResteasyDeployment; import org.keycloak.Config; +import org.keycloak.config.StorageOptions; import org.keycloak.connections.jpa.JpaConnectionProvider; import org.keycloak.connections.jpa.JpaConnectionSpi; +import org.keycloak.models.map.storage.MapStorageSpi; +import org.keycloak.models.map.storage.jpa.JpaMapStorageProviderFactory; import org.keycloak.quarkus.runtime.QuarkusProfile; import org.keycloak.quarkus.runtime.configuration.PersistedConfigSource; import org.keycloak.quarkus.runtime.configuration.QuarkusPropertiesConfigSource; @@ -129,6 +131,7 @@ import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.vertx.http.deployment.FilterBuildItem; import org.keycloak.quarkus.runtime.storage.database.jpa.NamedJpaConnectionProviderFactory; +import org.keycloak.quarkus.runtime.storage.database.jpa.QuarkusJpaMapStorageProviderFactory; import org.keycloak.quarkus.runtime.themes.FlatClasspathThemeResourceProviderFactory; import org.keycloak.representations.provider.ScriptProviderDescriptor; import org.keycloak.representations.provider.ScriptProviderMetadata; @@ -166,7 +169,8 @@ class KeycloakProcessor { RequestHostnameProviderFactory.class, FilesPlainTextVaultProviderFactory.class, BlacklistPasswordPolicyProviderFactory.class, - ClasspathThemeResourceProviderFactory.class); + ClasspathThemeResourceProviderFactory.class, + JpaMapStorageProviderFactory.class); static { DEPLOYEABLE_SCRIPT_PROVIDERS.put(AUTHENTICATORS, KeycloakProcessor::registerScriptAuthenticator); @@ -203,7 +207,7 @@ class KeycloakProcessor { * @param config * @param descriptors */ - @BuildStep(onlyIf = IsLegacyStoreEnabled.class) + @BuildStep(onlyIf = {IsJpaStoreEnabled.class}) @Record(ExecutionTime.RUNTIME_INIT) void configurePersistenceUnits(HibernateOrmConfig config, List descriptors, @@ -229,10 +233,20 @@ class KeycloakProcessor { } } - @BuildStep(onlyIf = IsLegacyStoreEnabled.class) + @BuildStep(onlyIf = IsJpaStoreEnabled.class) void produceDefaultPersistenceUnit(BuildProducer producer) { - ParsedPersistenceXmlDescriptor descriptor = PersistenceXmlParser.locateIndividualPersistenceUnit( - Thread.currentThread().getContextClassLoader().getResource("default-persistence.xml")); + String storage = Configuration.getRawValue( + MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX.concat(StorageOptions.STORAGE.getKey())); + ParsedPersistenceXmlDescriptor descriptor; + + if (storage == null) { + descriptor = PersistenceXmlParser.locateIndividualPersistenceUnit( + Thread.currentThread().getContextClassLoader().getResource("default-persistence.xml")); + } else { + descriptor = PersistenceXmlParser.locateIndividualPersistenceUnit( + Thread.currentThread().getContextClassLoader().getResource("default-map-jpa-persistence.xml")); + descriptor.getProperties().putAll(QuarkusJpaMapStorageProviderFactory.configureHibernateProperties()); + } producer.produce(new PersistenceXmlDescriptorBuildItem(descriptor)); } @@ -395,26 +409,10 @@ class KeycloakProcessor { void persistBuildTimeProperties(BuildProducer resources) { Properties properties = new Properties(); + putPersistedProperty(properties, "kc.db"); + for (String name : getPropertyNames()) { - PropertyMapper mapper = PropertyMappers.getMapper(name); - ConfigValue value = null; - - if (mapper == null) { - if (name.startsWith(NS_QUARKUS)) { - value = Configuration.getConfigValue(name); - - if (!QuarkusPropertiesConfigSource.isSameSource(value)) { - continue; - } - } - } else if (mapper.isBuildTime()) { - name = mapper.getFrom(); - value = Configuration.getConfigValue(name); - } - - if (value != null && value.getValue() != null) { - properties.put(name, value.getValue()); - } + putPersistedProperty(properties, name); } for (File jar : getProviderFiles().values()) { @@ -438,6 +436,34 @@ class KeycloakProcessor { } } + private void putPersistedProperty(Properties properties, String name) { + PropertyMapper mapper = PropertyMappers.getMapper(name); + ConfigValue value = null; + + if (mapper == null) { + if (name.startsWith(NS_QUARKUS)) { + value = Configuration.getConfigValue(name); + + if (!QuarkusPropertiesConfigSource.isSameSource(value)) { + return; + } + } + } else if (mapper.isBuildTime()) { + name = mapper.getFrom(); + value = Configuration.getConfigValue(name); + } + + if (value != null && value.getValue() != null) { + String rawValue = value.getRawValue(); + + if (rawValue == null) { + rawValue = value.getValue(); + } + + properties.put(name, rawValue); + } + } + /** * This will cause quarkus tu include specified modules in the jandex index. For example keycloak-services is needed as it includes * most of the JAX-RS resources, which are required to register Resteasy builtin providers. See {@link ResteasyDeployment#isRegisterBuiltin()}. @@ -451,6 +477,11 @@ class KeycloakProcessor { indexDependencyBuildItemBuildProducer.produce(new IndexDependencyBuildItem("org.keycloak", "keycloak-services")); } + @BuildStep(onlyIf = IsJpaStoreEnabled.class, onlyIfNot = IsLegacyStoreEnabled.class) + void indexJpaStore(BuildProducer indexDependencyBuildItemBuildProducer) { + indexDependencyBuildItemBuildProducer.produce(new IndexDependencyBuildItem("org.keycloak", "keycloak-model-map-jpa")); + } + @BuildStep void initializeFilter(BuildProducer filters, LaunchModeBuildItem launchModeBuildItem) { QuarkusRequestFilter filter = new QuarkusRequestFilter(); @@ -523,10 +554,12 @@ class KeycloakProcessor { for (Spi spi : pm.loadSpis()) { Map, Map> providers = new HashMap<>(); - String provider = Config.getProvider(spi.getName()); List loadedFactories = new ArrayList<>(); + String provider = Config.getProvider(spi.getName()); - if (provider == null) { + // TODO: remove the condition for MapStorageSpi once JPA store is ready and we can set a default provider + // while still allowing multiple implementations at runtime + if (provider == null || spi instanceof MapStorageSpi) { loadedFactories.addAll(pm.load(spi)); } else { ProviderFactory factory = pm.load(spi, provider); diff --git a/quarkus/runtime/pom.xml b/quarkus/runtime/pom.xml index 5e439638b52..9c26ddacb3e 100644 --- a/quarkus/runtime/pom.xml +++ b/quarkus/runtime/pom.xml @@ -310,6 +310,16 @@ + + org.keycloak + keycloak-model-map-jpa + + + * + * + + + diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/ShowConfig.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/ShowConfig.java index 288162ec5e8..99e10bfefa4 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/ShowConfig.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/cli/command/ShowConfig.java @@ -21,6 +21,7 @@ import static org.keycloak.quarkus.runtime.Environment.getCurrentOrPersistedProf import static org.keycloak.quarkus.runtime.Environment.setProfile; import static org.keycloak.quarkus.runtime.configuration.Configuration.getConfigValue; import static org.keycloak.quarkus.runtime.configuration.Configuration.getPropertyNames; +import static org.keycloak.quarkus.runtime.configuration.Configuration.getRuntimeProperty; import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers.formatValue; import java.util.HashSet; @@ -34,8 +35,11 @@ import java.util.stream.Collectors; import java.util.stream.StreamSupport; import org.keycloak.quarkus.runtime.Environment; +import org.keycloak.quarkus.runtime.configuration.Configuration; import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider; import org.keycloak.quarkus.runtime.configuration.PersistedConfigSource; +import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper; +import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers; import io.quarkus.runtime.Quarkus; import io.smallrye.config.ConfigValue; @@ -124,11 +128,6 @@ public final class ShowConfig extends AbstractCommand implements Runnable { private void printProperty(String property) { ConfigValue configValue = getConfigValue(property); - if (configValue.getValue() == null) { - configValue = getConfigValue(property); - } - - if (configValue.getValue() == null) { return; } @@ -137,7 +136,19 @@ public final class ShowConfig extends AbstractCommand implements Runnable { return; } - spec.commandLine().getOut().printf("\t%s = %s (%s)%n", configValue.getName(), formatValue(configValue.getName(), configValue.getValue()), configValue.getConfigSourceName()); + String value = configValue.getRawValue(); + + if (value == null) { + value = configValue.getValue(); + } + + PropertyMapper mapper = PropertyMappers.getMapper(property); + + if (mapper != null && mapper.isRunTime()) { + value = getRuntimeProperty(property).orElse(value); + } + + spec.commandLine().getOut().printf("\t%s = %s (%s)%n", configValue.getName(), formatValue(configValue.getName(), value), configValue.getConfigSourceName()); } private static String groupProperties(String property) { diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/Configuration.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/Configuration.java index 2f20f7f3d2d..246e5a6cac2 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/Configuration.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/Configuration.java @@ -157,10 +157,10 @@ public final class Configuration { } private static String getValue(ConfigSource configSource, String name) { - String value = configSource.getValue(name); + String value = configSource.getValue("%".concat(getProfileOrDefault("prod").concat(".").concat(name))); if (value == null) { - value = configSource.getValue("%".concat(getProfileOrDefault("prod").concat(".").concat(name))); + value = configSource.getValue(name); } return value; diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/DatabasePropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/DatabasePropertyMappers.java index 8d312248813..ee95cda1b42 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/DatabasePropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/DatabasePropertyMappers.java @@ -4,12 +4,17 @@ import io.quarkus.datasource.common.runtime.DatabaseKind; import io.smallrye.config.ConfigSourceInterceptorContext; import io.smallrye.config.ConfigValue; import org.keycloak.config.DatabaseOptions; +import org.keycloak.config.StorageOptions; import org.keycloak.config.database.Database; +import org.keycloak.quarkus.runtime.configuration.Configuration; import java.util.Optional; import static java.util.Optional.of; +import static org.keycloak.config.StorageOptions.STORAGE; import static org.keycloak.quarkus.runtime.Messages.invalidDatabaseVendor; +import static org.keycloak.quarkus.runtime.configuration.Configuration.getRawValue; +import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX; import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption; import static org.keycloak.quarkus.runtime.integration.QuarkusPlatform.addInitializationException; @@ -30,6 +35,7 @@ final class DatabasePropertyMappers { .transformer(DatabasePropertyMappers::getXaOrNonXaDriver) .build(), fromOption(DatabaseOptions.DB) + .transformer(DatabasePropertyMappers::resolveDatabaseVendor) .to("quarkus.datasource.db-kind") .transformer(DatabasePropertyMappers::toDatabaseKind) .paramLabel("vendor") @@ -92,6 +98,9 @@ final class DatabasePropertyMappers { Optional url = Database.getDefaultUrl(value.get()); if (url.isPresent()) { + if (isJpaStore()) { + return Database.getDefaultUrl(Database.Vendor.POSTGRES.name().toLowerCase()); + } return url; } @@ -99,13 +108,17 @@ final class DatabasePropertyMappers { } private static Optional getXaOrNonXaDriver(Optional value, ConfigSourceInterceptorContext context) { + if (isJpaStore()) { + return Database.getDriver(Database.Vendor.POSTGRES.name().toLowerCase(), false); + } + ConfigValue xaEnabledConfigValue = context.proceed("kc.transaction-xa-enabled"); - ConfigValue jtaEnabledConfiguration = context.proceed("kc.transaction-jta-enabled"); + ConfigValue jtaEnabledConfiguration = context.proceed("kc.transaction-jta-enabled"); boolean isXaEnabled = xaEnabledConfigValue == null || Boolean.parseBoolean(xaEnabledConfigValue.getValue()); boolean isJtaEnabled = jtaEnabledConfiguration == null || Boolean.parseBoolean(jtaEnabledConfiguration.getValue()); - if(!isJtaEnabled) { + if (!isJtaEnabled) { isXaEnabled = false; } @@ -119,6 +132,10 @@ final class DatabasePropertyMappers { } private static Optional toDatabaseKind(Optional db, ConfigSourceInterceptorContext context) { + if (isJpaStore()) { + return Database.getDatabaseKind(Database.Vendor.POSTGRES.name().toLowerCase()); + } + Optional databaseKind = Database.getDatabaseKind(db.get()); if (databaseKind.isPresent()) { @@ -130,6 +147,18 @@ final class DatabasePropertyMappers { return of("h2"); } + private static Optional resolveDatabaseVendor(Optional db, ConfigSourceInterceptorContext context) { + if (isJpaStore()) { + return of(Database.Vendor.POSTGRES.name().toLowerCase()); + } + + if (db.isEmpty()) { + return of("dev-file"); + } + + return db; + } + private static Optional resolveUsername(Optional value, ConfigSourceInterceptorContext context) { if (isDevModeDatabase(context)) { return of("sa"); @@ -147,11 +176,19 @@ final class DatabasePropertyMappers { } private static boolean isDevModeDatabase(ConfigSourceInterceptorContext context) { - String db = context.proceed("kc.db").getValue(); + if (isJpaStore()) { + return false; + } + + String db = Configuration.getConfig().getConfigValue("kc.db").getValue(); return Database.getDatabaseKind(db).get().equals(DatabaseKind.H2); } private static Optional transformDialect(Optional db, ConfigSourceInterceptorContext context) { + if (isJpaStore()) { + return of("org.keycloak.models.map.storage.jpa.hibernate.dialect.JsonbPostgreSQL95Dialect"); + } + Optional databaseKind = Database.getDatabaseKind(db.get()); if (databaseKind.isEmpty()) { @@ -166,4 +203,17 @@ final class DatabasePropertyMappers { return Database.getDialect("dev-file"); } + + private static String getDefaultVendor() { + if (isJpaStore()) { + return Database.Vendor.POSTGRES.name().toLowerCase(); + } + + return "dev-file"; + } + + private static boolean isJpaStore() { + String storage = getRawValue(NS_KEYCLOAK_PREFIX.concat(STORAGE.getKey())); + return storage != null && StorageOptions.StorageType.jpa.name().equals(storage); + } } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMapper.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMapper.java index febec73ca46..080b7e4b032 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMapper.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/PropertyMapper.java @@ -210,7 +210,7 @@ public class PropertyMapper { return null; } - return ConfigValue.builder().withName(to).withValue(mappedValue.get()).build(); + return ConfigValue.builder().withName(to).withValue(mappedValue.get()).withRawValue(value.orElse(null)).build(); } public static class Builder { diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/StoragePropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/StoragePropertyMappers.java index fbce9aa7ba9..61e64b09943 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/StoragePropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/StoragePropertyMappers.java @@ -22,6 +22,7 @@ import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper. import java.util.Optional; import org.keycloak.config.StorageOptions; +import org.keycloak.config.StorageOptions.StorageType; import io.smallrye.config.ConfigSourceInterceptorContext; @@ -153,7 +154,7 @@ final class StoragePropertyMappers { fromOption(StorageOptions.STORAGE_USER_SESSION_STORE) .to("kc.spi-user-sessions-map-storage-provider") .mapFrom("storage") - .transformer(StoragePropertyMappers::resolveMapStorageProvider) + .transformer(StoragePropertyMappers::resolveUserSessionProvider) .paramLabel("type") .build(), fromOption(StorageOptions.STORAGE_LOGIN_FAILURE) @@ -332,8 +333,8 @@ final class StoragePropertyMappers { private static Optional resolveMapStorageProvider(Optional value, ConfigSourceInterceptorContext context) { try { if (value.isPresent()) { - return of(value.map(StorageOptions.StorageType::valueOf).map(StorageOptions.StorageType::getProvider) - .orElse(StorageOptions.StorageType.chm.getProvider())); + return of(value.map(StorageType::valueOf).map(StorageType::getProvider) + .orElse(StorageType.chm.getProvider())); } } catch (IllegalArgumentException iae) { throw new IllegalArgumentException("Invalid storage provider: " + value.orElse(null), iae); @@ -345,4 +346,22 @@ final class StoragePropertyMappers { private static Optional isCacheAreaEnabledForStorage(Optional storage, ConfigSourceInterceptorContext context) { return of(storage.isEmpty() ? Boolean.TRUE.toString() : Boolean.FALSE.toString()); } + + private static Optional resolveUserSessionProvider(Optional storage, ConfigSourceInterceptorContext context) { + try { + if (storage.isPresent()) { + Optional type = storage.map(StorageType::valueOf); + + if (StorageType.jpa.equals(type.get())) { + return of(StorageType.chm.getProvider()); + } + + return of(type.map(StorageType::getProvider).orElse(StorageType.chm.getProvider())); + } + } catch (IllegalArgumentException iae) { + throw new IllegalArgumentException("Invalid storage provider: " + storage.orElse(null), iae); + } + + return storage; + } } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/TransactionPropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/TransactionPropertyMappers.java index 4831b2be675..95f26b3d5e3 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/TransactionPropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/TransactionPropertyMappers.java @@ -2,9 +2,13 @@ package org.keycloak.quarkus.runtime.configuration.mappers; import io.smallrye.config.ConfigSourceInterceptorContext; import io.smallrye.config.ConfigValue; + +import org.keycloak.config.StorageOptions; import org.keycloak.config.TransactionOptions; import static java.util.Optional.of; +import static org.keycloak.config.StorageOptions.STORAGE; +import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX; import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption; import java.util.Optional; @@ -32,6 +36,11 @@ public class TransactionPropertyMappers { private static Optional getQuarkusTransactionsValue(Optional txValue, ConfigSourceInterceptorContext context) { boolean isXaEnabled = Boolean.parseBoolean(txValue.get()); boolean isJtaEnabled = getBooleanValue("kc.transaction-jta-enabled", context, true); + ConfigValue storage = context.proceed(NS_KEYCLOAK_PREFIX.concat(STORAGE.getKey())); + + if (storage != null && StorageOptions.StorageType.jpa.name().equals(storage.getValue())) { + isJtaEnabled = false; + } if (!isJtaEnabled) { return of("disabled"); diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/database/jpa/QuarkusJpaMapStorageProviderFactory.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/database/jpa/QuarkusJpaMapStorageProviderFactory.java new file mode 100644 index 00000000000..cd01ef95b44 --- /dev/null +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/database/jpa/QuarkusJpaMapStorageProviderFactory.java @@ -0,0 +1,95 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.quarkus.runtime.storage.database.jpa; + +import static org.keycloak.config.StorageOptions.STORAGE; +import static org.keycloak.quarkus.runtime.configuration.Configuration.getOptionalValue; +import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX; + +import java.lang.annotation.Annotation; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Optional; +import javax.enterprise.inject.Instance; +import javax.persistence.EntityManagerFactory; +import org.hibernate.internal.SessionFactoryImpl; +import org.keycloak.config.StorageOptions; +import org.keycloak.models.map.storage.jpa.JpaMapStorageProviderFactory; + +import io.quarkus.arc.Arc; +import io.quarkus.hibernate.orm.PersistenceUnit; + +public class QuarkusJpaMapStorageProviderFactory extends JpaMapStorageProviderFactory { + + @Override + public String getId() { + return StorageOptions.StorageType.jpa.getProvider(); + } + + @Override + protected EntityManagerFactory createEntityManagerFactory() { + Instance instance = Arc.container().select(EntityManagerFactory.class); + + if (instance.isResolvable()) { + return instance.get(); + } + + return getEntityManagerFactory("keycloak-default").orElseThrow(() -> new IllegalStateException("Failed to resolve the default entity manager factory")); + } + + @Override + protected Connection getConnection() { + SessionFactoryImpl entityManagerFactory = getEntityManagerFactory().unwrap(SessionFactoryImpl.class); + + try { + return entityManagerFactory.getJdbcServices().getBootstrapJdbcConnectionAccess().obtainConnection(); + } catch (SQLException cause) { + throw new RuntimeException("Failed to obtain JDBC connection", cause); + } + } + + protected Optional getEntityManagerFactory(String unitName) { + Instance instance = Arc.container().select(EntityManagerFactory.class, new PersistenceUnit() { + + @Override + public Class annotationType() { + return PersistenceUnit.class; + } + + @Override + public String value() { + return unitName; + } + }); + + if (instance.isResolvable()) { + return Optional.of(instance.get()); + } + + return Optional.empty(); + } + + @Override + public boolean isSupported() { + // allow running multiple storages at runtime if jpa is enabled, otherwise disables it + // only enable jpa store if explicitly said so + // this is because jpa store is missing some provider implementations that require the usage of another storage at runtime like chm + // once we have jpa-based provider impls for all areas we can remove this and be able to run jpa store as the single store + return StorageOptions.StorageType.jpa.name().equals(getOptionalValue(NS_KEYCLOAK_PREFIX.concat(STORAGE.getKey())).orElse(null)); + } +} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/transaction/QuarkusJtaTransactionManagerLookup.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/transaction/QuarkusJtaTransactionManagerLookup.java index 96daac6834b..e0c04f9a09c 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/transaction/QuarkusJtaTransactionManagerLookup.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/transaction/QuarkusJtaTransactionManagerLookup.java @@ -22,10 +22,12 @@ import javax.transaction.TransactionManager; import org.jboss.logging.Logger; import org.keycloak.Config; +import org.keycloak.common.Profile; import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.provider.EnvironmentDependentProviderFactory; import org.keycloak.transaction.JtaTransactionManagerLookup; -public class QuarkusJtaTransactionManagerLookup implements JtaTransactionManagerLookup { +public class QuarkusJtaTransactionManagerLookup implements JtaTransactionManagerLookup, EnvironmentDependentProviderFactory { private static final Logger logger = Logger.getLogger(QuarkusJtaTransactionManagerLookup.class); @@ -65,4 +67,9 @@ public class QuarkusJtaTransactionManagerLookup implements JtaTransactionManager public int order() { return 100; } + + @Override + public boolean isSupported() { + return !Profile.isFeatureEnabled(Profile.Feature.MAP_STORAGE); + } } diff --git a/quarkus/runtime/src/main/resources/META-INF/services/org.keycloak.models.map.storage.MapStorageProviderFactory b/quarkus/runtime/src/main/resources/META-INF/services/org.keycloak.models.map.storage.MapStorageProviderFactory new file mode 100644 index 00000000000..6b36fe6e5b6 --- /dev/null +++ b/quarkus/runtime/src/main/resources/META-INF/services/org.keycloak.models.map.storage.MapStorageProviderFactory @@ -0,0 +1,18 @@ +# +# Copyright 2022 Red Hat, Inc. and/or its affiliates +# and other contributors as indicated by the @author tags. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +org.keycloak.quarkus.runtime.storage.database.jpa.QuarkusJpaMapStorageProviderFactory \ No newline at end of file diff --git a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/ConfigurationTest.java b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/ConfigurationTest.java index de4be18767c..cd611f6dceb 100644 --- a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/ConfigurationTest.java +++ b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/ConfigurationTest.java @@ -484,7 +484,7 @@ public class ConfigurationTest { @Test public void testOptionValueWithEqualSign() { - System.setProperty(CLI_ARGS, "--db-password=my_secret="); + System.setProperty(CLI_ARGS, "--db=postgres" + ARG_SEPARATOR + "--db-password=my_secret="); SmallRyeConfig config = createConfig(); assertEquals("my_secret=", config.getConfigValue("kc.db-password").getValue()); } @@ -505,6 +505,7 @@ public class ConfigurationTest { private SmallRyeConfig createConfig() { KeycloakConfigSourceProvider.reload(); + ConfigProviderResolver.setInstance(null); return ConfigUtils.configBuilder(true, LaunchMode.NORMAL).build(); } } diff --git a/quarkus/tests/integration/src/main/java/org/keycloak/it/junit5/extension/CLITestExtension.java b/quarkus/tests/integration/src/main/java/org/keycloak/it/junit5/extension/CLITestExtension.java index 261535618d1..4f2bdb65a1c 100644 --- a/quarkus/tests/integration/src/main/java/org/keycloak/it/junit5/extension/CLITestExtension.java +++ b/quarkus/tests/integration/src/main/java/org/keycloak/it/junit5/extension/CLITestExtension.java @@ -255,7 +255,14 @@ public class CLITestExtension extends QuarkusMainTestExtension { databaseContainer.start(); - dist.setProperty("db", database.alias()); + if (database.buildOptions().length == 0) { + dist.setProperty("db", database.alias()); + } else { + for (String option : database.buildOptions()) { + dist.setProperty(option.substring(0, option.indexOf('=')), option.substring(option.indexOf('=') + 1)); + } + } + dist.setProperty("db-username", databaseContainer.getUsername()); dist.setProperty("db-password", databaseContainer.getPassword()); dist.setProperty("db-url", databaseContainer.getJdbcUrl()); diff --git a/quarkus/tests/integration/src/main/java/org/keycloak/it/junit5/extension/WithDatabase.java b/quarkus/tests/integration/src/main/java/org/keycloak/it/junit5/extension/WithDatabase.java index 8ba4c76334e..8bd39075341 100644 --- a/quarkus/tests/integration/src/main/java/org/keycloak/it/junit5/extension/WithDatabase.java +++ b/quarkus/tests/integration/src/main/java/org/keycloak/it/junit5/extension/WithDatabase.java @@ -35,4 +35,9 @@ public @interface WithDatabase { * The database name as per database aliases. */ String alias(); + + /** + * The build options that should be used to build the server prior to starting. + */ + String[] buildOptions() default {}; } diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/storage/map/JPAStoreDistTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/storage/map/JPAStoreDistTest.java new file mode 100644 index 00000000000..0495d0956b4 --- /dev/null +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/storage/map/JPAStoreDistTest.java @@ -0,0 +1,40 @@ +/* + * Copyright 2022 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.it.storage.map; + +import org.junit.jupiter.api.Test; +import org.keycloak.it.junit5.extension.CLIResult; +import org.keycloak.it.junit5.extension.DistributionTest; +import org.keycloak.it.junit5.extension.WithDatabase; + +import io.quarkus.test.junit.main.Launch; +import io.quarkus.test.junit.main.LaunchResult; + +@DistributionTest(removeBuildOptionsAfterBuild = true) +@WithDatabase(alias = "postgres", buildOptions={"storage=jpa"}) +public class JPAStoreDistTest { + + @Test + @Launch({ "start", "--http-enabled=true", "--hostname-strict=false" }) + void testSuccessful(LaunchResult result) { + CLIResult cliResult = (CLIResult) result; + cliResult.assertMessage("Experimental feature enabled: map_storage"); + cliResult.assertMessage("[org.keycloak.models.map.storage.jpa.liquibase.updater.MapJpaLiquibaseUpdaterProvider] (main) Initializing database schema. Using changelog META-INF/jpa-realms-changelog.xml"); + cliResult.assertStarted(); + } +} diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/approvals/cli/help/HelpCommandTest.testBuildHelp.unix.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/approvals/cli/help/HelpCommandTest.testBuildHelp.unix.approved.txt index e5ab13c6b3b..51844f33fe9 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/approvals/cli/help/HelpCommandTest.testBuildHelp.unix.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/approvals/cli/help/HelpCommandTest.testBuildHelp.unix.approved.txt @@ -37,12 +37,12 @@ Cluster: Storage (Experimental): ---storage Experimental: Sets a storage mechanism. Possible values are: chm. +--storage Experimental: Sets a storage mechanism. Possible values are: jpa, chm. Database: --db The database vendor. Possible values are: dev-file, dev-mem, mariadb, mssql, - mysql, oracle, postgres + mysql, oracle, postgres. Default: dev-file. Transaction: diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/approvals/cli/help/HelpCommandTest.testBuildHelp.windows.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/approvals/cli/help/HelpCommandTest.testBuildHelp.windows.approved.txt index e9a86cf07ab..3236eb2542f 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/approvals/cli/help/HelpCommandTest.testBuildHelp.windows.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/approvals/cli/help/HelpCommandTest.testBuildHelp.windows.approved.txt @@ -37,13 +37,12 @@ Cluster: Storage (Experimental): ---storage Experimental: Sets a storage mechanism. Possible values are: legacy, chm. Default: - legacy. +--storage Experimental: Sets a storage mechanism. Possible values are: jpa, chm. Database: --db The database vendor. Possible values are: dev-file, dev-mem, mariadb, mssql, - mysql, oracle, postgres + mysql, oracle, postgres Default: dev-file. Transaction: diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/approvals/cli/help/HelpCommandTest.testStartDevHelpAll.unix.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/approvals/cli/help/HelpCommandTest.testStartDevHelpAll.unix.approved.txt index fb2affb3010..4ac4f7fad50 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/approvals/cli/help/HelpCommandTest.testStartDevHelpAll.unix.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/approvals/cli/help/HelpCommandTest.testStartDevHelpAll.unix.approved.txt @@ -29,12 +29,12 @@ Cluster: Storage (Experimental): ---storage Experimental: Sets a storage mechanism. Possible values are: chm. +--storage Experimental: Sets a storage mechanism. Possible values are: jpa, chm. Database: --db The database vendor. Possible values are: dev-file, dev-mem, mariadb, mssql, - mysql, oracle, postgres + mysql, oracle, postgres. Default: dev-file. --db-password The password of the database user. --db-pool-initial-size diff --git a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/approvals/cli/help/HelpCommandTest.testStartDevHelpAll.windows.approved.txt b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/approvals/cli/help/HelpCommandTest.testStartDevHelpAll.windows.approved.txt index 053a935fd35..10848bd2996 100644 --- a/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/approvals/cli/help/HelpCommandTest.testStartDevHelpAll.windows.approved.txt +++ b/quarkus/tests/integration/src/test/resources/org/keycloak/it/cli/approvals/cli/help/HelpCommandTest.testStartDevHelpAll.windows.approved.txt @@ -29,13 +29,12 @@ Cluster: Storage (Experimental): ---storage Experimental: Sets a storage mechanism. Possible values are: legacy, chm. Default: - legacy. +--storage Experimental: Sets a storage mechanism. Possible values are: jpa, chm. Database: --db The database vendor. Possible values are: dev-file, dev-mem, mariadb, mssql, - mysql, oracle, postgres + mysql, oracle, postgres. Default: dev-file. --db-password The password of the database user. --db-pool-initial-size