mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-25 16:42:34 +00:00
Integrate the JPA map store (#13097)
Co-authored-by: Alexander Schwartz <alexander.schwartz@gmx.net>
This commit is contained in:
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
|
||||
@@ -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> {
|
||||
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -26,6 +26,7 @@ public class StorageOptions {
|
||||
|
||||
public enum StorageType {
|
||||
|
||||
jpa("jpa"),
|
||||
chm("concurrenthashmap");
|
||||
|
||||
private final String provider;
|
||||
|
||||
@@ -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)
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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 {};
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user