Integrate the JPA map store (#13097)

Co-authored-by: Alexander Schwartz <alexander.schwartz@gmx.net>
This commit is contained in:
Pedro Igor
2022-07-14 17:47:51 -03:00
committed by GitHub
parent e4fda78f5f
commit f6a2b334d1
31 changed files with 573 additions and 144 deletions

View File

@@ -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

View File

@@ -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) {
}
}

View File

@@ -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<String, Object> configureHibernateProperties() {
Map<String, Object> properties = new HashMap<>();
properties.put("hibernate.integrator_provider", new KeycloakIntegratorProvider());
return properties;
}
private volatile EntityManagerFactory emf;
private final Set<Class<?>> 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<String, Object> 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<String, Object> 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) {

View File

@@ -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<Integrator> getIntegrators() {
return new ArrayList<>(Collections.singletonList(new EventListenerIntegrator()));
}
}

View File

@@ -1,6 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2022 Red Hat, Inc. and/or its affiliates
~ and other contributors as indicated by the @author tags.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<persistence version="2.1" xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
<persistence-unit name="keycloak-jpa-default">
<persistence-unit name="keycloak-default">
<!--auth-sessions-->
<class>org.keycloak.models.map.storage.jpa.authSession.entity.JpaRootAuthenticationSessionEntity</class>
<class>org.keycloak.models.map.storage.jpa.authSession.entity.JpaAuthenticationSessionEntity</class>

View File

@@ -19,9 +19,10 @@ public class DatabaseOptions {
.defaultValue(Database.getDriver("dev-file", true).get())
.build();
public static final Option<Database.Vendor> DB = new OptionBuilder<>("db", Database.Vendor.class)
public static final Option<String> 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();

View File

@@ -2,7 +2,6 @@ package org.keycloak.config;
import java.util.List;
import java.util.Optional;
import java.util.Set;
public class MultiOption<T> extends Option<T> {

View File

@@ -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<T> {

View File

@@ -26,6 +26,7 @@ public class StorageOptions {
public enum StorageType {
jpa("jpa"),
chm("concurrenthashmap");
private final String provider;

View File

@@ -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)

View File

@@ -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<String> storage = getOptionalValue(NS_KEYCLOAK_PREFIX.concat(STORAGE.getKey()));
if (storage.isEmpty()) {
// legacy store
return true;
}
return StorageOptions.StorageType.jpa.name().equals(storage.orElse(null));
}
}

View File

@@ -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();
}
}

View File

@@ -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<PersistenceXmlDescriptorBuildItem> descriptors,
@@ -229,10 +233,20 @@ class KeycloakProcessor {
}
}
@BuildStep(onlyIf = IsLegacyStoreEnabled.class)
@BuildStep(onlyIf = IsJpaStoreEnabled.class)
void produceDefaultPersistenceUnit(BuildProducer<PersistenceXmlDescriptorBuildItem> 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<GeneratedResourceBuildItem> 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<IndexDependencyBuildItem> indexDependencyBuildItemBuildProducer) {
indexDependencyBuildItemBuildProducer.produce(new IndexDependencyBuildItem("org.keycloak", "keycloak-model-map-jpa"));
}
@BuildStep
void initializeFilter(BuildProducer<FilterBuildItem> filters, LaunchModeBuildItem launchModeBuildItem) {
QuarkusRequestFilter filter = new QuarkusRequestFilter();
@@ -523,10 +554,12 @@ class KeycloakProcessor {
for (Spi spi : pm.loadSpis()) {
Map<Class<? extends Provider>, Map<String, ProviderFactory>> providers = new HashMap<>();
String provider = Config.getProvider(spi.getName());
List<ProviderFactory> 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);

View File

@@ -310,6 +310,16 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-map-jpa</artifactId>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Keycloak Dependencies-->
<dependency>

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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<String> 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<String> getXaOrNonXaDriver(Optional<String> 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<String> toDatabaseKind(Optional<String> db, ConfigSourceInterceptorContext context) {
if (isJpaStore()) {
return Database.getDatabaseKind(Database.Vendor.POSTGRES.name().toLowerCase());
}
Optional<String> databaseKind = Database.getDatabaseKind(db.get());
if (databaseKind.isPresent()) {
@@ -130,6 +147,18 @@ final class DatabasePropertyMappers {
return of("h2");
}
private static Optional<String> resolveDatabaseVendor(Optional<String> db, ConfigSourceInterceptorContext context) {
if (isJpaStore()) {
return of(Database.Vendor.POSTGRES.name().toLowerCase());
}
if (db.isEmpty()) {
return of("dev-file");
}
return db;
}
private static Optional<String> resolveUsername(Optional<String> 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<String> transformDialect(Optional<String> db, ConfigSourceInterceptorContext context) {
if (isJpaStore()) {
return of("org.keycloak.models.map.storage.jpa.hibernate.dialect.JsonbPostgreSQL95Dialect");
}
Optional<String> 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);
}
}

View File

@@ -210,7 +210,7 @@ public class PropertyMapper<T> {
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<T> {

View File

@@ -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<String> resolveMapStorageProvider(Optional<String> 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<String> isCacheAreaEnabledForStorage(Optional<String> storage, ConfigSourceInterceptorContext context) {
return of(storage.isEmpty() ? Boolean.TRUE.toString() : Boolean.FALSE.toString());
}
private static Optional<String> resolveUserSessionProvider(Optional<String> storage, ConfigSourceInterceptorContext context) {
try {
if (storage.isPresent()) {
Optional<StorageType> 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;
}
}

View File

@@ -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<String> getQuarkusTransactionsValue(Optional<String> 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");

View File

@@ -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<EntityManagerFactory> 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<EntityManagerFactory> getEntityManagerFactory(String unitName) {
Instance<EntityManagerFactory> instance = Arc.container().select(EntityManagerFactory.class, new PersistenceUnit() {
@Override
public Class<? extends Annotation> 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));
}
}

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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();
}
}

View File

@@ -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());

View File

@@ -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 {};
}

View File

@@ -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();
}
}

View File

@@ -37,12 +37,12 @@ Cluster:
Storage (Experimental):
--storage <type> Experimental: Sets a storage mechanism. Possible values are: chm.
--storage <type> Experimental: Sets a storage mechanism. Possible values are: jpa, chm.
Database:
--db <vendor> The database vendor. Possible values are: dev-file, dev-mem, mariadb, mssql,
mysql, oracle, postgres
mysql, oracle, postgres. Default: dev-file.
Transaction:

View File

@@ -37,13 +37,12 @@ Cluster:
Storage (Experimental):
--storage <type> Experimental: Sets a storage mechanism. Possible values are: legacy, chm. Default:
legacy.
--storage <type> Experimental: Sets a storage mechanism. Possible values are: jpa, chm.
Database:
--db <vendor> The database vendor. Possible values are: dev-file, dev-mem, mariadb, mssql,
mysql, oracle, postgres
mysql, oracle, postgres Default: dev-file.
Transaction:

View File

@@ -29,12 +29,12 @@ Cluster:
Storage (Experimental):
--storage <type> Experimental: Sets a storage mechanism. Possible values are: chm.
--storage <type> Experimental: Sets a storage mechanism. Possible values are: jpa, chm.
Database:
--db <vendor> The database vendor. Possible values are: dev-file, dev-mem, mariadb, mssql,
mysql, oracle, postgres
mysql, oracle, postgres. Default: dev-file.
--db-password <password>
The password of the database user.
--db-pool-initial-size <size>

View File

@@ -29,13 +29,12 @@ Cluster:
Storage (Experimental):
--storage <type> Experimental: Sets a storage mechanism. Possible values are: legacy, chm. Default:
legacy.
--storage <type> Experimental: Sets a storage mechanism. Possible values are: jpa, chm.
Database:
--db <vendor> The database vendor. Possible values are: dev-file, dev-mem, mariadb, mssql,
mysql, oracle, postgres
mysql, oracle, postgres. Default: dev-file.
--db-password <password>
The password of the database user.
--db-pool-initial-size <size>