From eafe08a73aabc1fee64fd06f382235484eeea40f Mon Sep 17 00:00:00 2001 From: Pedro Ruivo Date: Mon, 28 Apr 2025 12:00:53 +0100 Subject: [PATCH] Create CacheEmbeddedConfigProvider Closes #38497 Signed-off-by: Pedro Ruivo Signed-off-by: Alexander Schwartz Co-authored-by: Alexander Schwartz --- .../topics/changes/changes-26_3_0.adoc | 14 + .../deploy-infinispan-kubernetes-crossdc.adoc | 3 - .../examples/generated/keycloak-ispn.yaml | 2 +- .../examples/generated/keycloak.yaml | 2 +- model/infinispan/pom.xml | 5 + ...ltInfinispanConnectionProviderFactory.java | 287 ++++------------- .../InfinispanConnectionProvider.java | 15 + .../infinispan/InfinispanUtil.java | 60 ++-- .../connections/infinispan/TopologyInfo.java | 89 ++--- .../keycloak/jgroups/JGroupsConfigurator.java | 117 ------- .../org/keycloak/jgroups/JGroupsUtil.java | 75 ----- .../JGroupsJdbcPingStackConfigurator.java | 103 ------ .../impl/JpaJGroupsTlsConfigurator.java | 85 ----- .../KEYCLOAK_JDBC_PING2.java | 21 +- .../CacheEmbeddedConfigProvider.java} | 21 +- .../CacheEmbeddedConfigProviderFactory.java | 26 ++ .../CacheEmbeddedConfigProviderSpi.java | 53 +++ .../impl/embedded/CacheConfigurator.java | 303 ++++++++++++++++++ ...ultCacheEmbeddedConfigProviderFactory.java | 250 +++++++++++++++ .../impl/embedded/JGroupsConfigurator.java | 262 +++++++++++++++ .../services/org.keycloak.provider.Spi | 2 + ...inispan.CacheEmbeddedConfigProviderFactory | 4 +- .../org/keycloak/config/MetricsOptions.java | 7 + .../quarkus/deployment/CacheBuildSteps.java | 49 --- .../quarkus/runtime/KeycloakRecorder.java | 101 ++---- .../mappers/CachingPropertyMappers.java | 7 +- .../mappers/MetricsPropertyMappers.java | 5 + .../infinispan/CacheManagerFactory.java | 300 ----------------- .../QuarkusCacheManagerProvider.java | 35 -- .../QuarkusInfinispanConnectionFactory.java | 61 ---- ...nispan.InfinispanConnectionProviderFactory | 20 -- .../test/AbstractConfigurationTest.java | 2 +- .../configuration/test/ConfigurationTest.java | 16 +- .../database/ExternalInfinispanTest.java | 17 +- .../cluster/ManagedCacheManagerProvider.java | 40 --- .../integration-arquillian/HOW-TO-RUN.md | 9 +- .../AbstractQuarkusDeployableContainer.java | 11 +- .../resources/META-INF/keycloak-server.json | 12 +- .../base/src/test/resources/arquillian.xml | 16 +- .../integration-arquillian/tests/pom.xml | 4 - .../model/parameters/Infinispan.java | 92 +++--- .../model/parameters/RemoteInfinispan.java | 14 +- .../model/src/test/resources/test-ispn.xml | 115 +++++++ .../resources/META-INF/keycloak-server.json | 11 +- 44 files changed, 1341 insertions(+), 1402 deletions(-) delete mode 100644 model/infinispan/src/main/java/org/keycloak/jgroups/JGroupsConfigurator.java delete mode 100644 model/infinispan/src/main/java/org/keycloak/jgroups/JGroupsUtil.java delete mode 100644 model/infinispan/src/main/java/org/keycloak/jgroups/impl/JGroupsJdbcPingStackConfigurator.java delete mode 100644 model/infinispan/src/main/java/org/keycloak/jgroups/impl/JpaJGroupsTlsConfigurator.java rename model/infinispan/src/main/java/org/keycloak/jgroups/{impl => protocol}/KEYCLOAK_JDBC_PING2.java (73%) rename model/infinispan/src/main/java/org/keycloak/{jgroups/JGroupsStackConfigurator.java => spi/infinispan/CacheEmbeddedConfigProvider.java} (54%) create mode 100644 model/infinispan/src/main/java/org/keycloak/spi/infinispan/CacheEmbeddedConfigProviderFactory.java create mode 100644 model/infinispan/src/main/java/org/keycloak/spi/infinispan/CacheEmbeddedConfigProviderSpi.java create mode 100644 model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/embedded/CacheConfigurator.java create mode 100644 model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/embedded/DefaultCacheEmbeddedConfigProviderFactory.java create mode 100644 model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/embedded/JGroupsConfigurator.java rename quarkus/runtime/src/main/resources/META-INF/services/org.keycloak.cluster.ManagedCacheManagerProvider => model/infinispan/src/main/resources/META-INF/services/org.keycloak.spi.infinispan.CacheEmbeddedConfigProviderFactory (81%) delete mode 100644 quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/CacheBuildSteps.java delete mode 100644 quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/CacheManagerFactory.java delete mode 100644 quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/QuarkusCacheManagerProvider.java delete mode 100644 quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/QuarkusInfinispanConnectionFactory.java delete mode 100644 quarkus/runtime/src/main/resources/META-INF/services/org.keycloak.connections.infinispan.InfinispanConnectionProviderFactory delete mode 100644 server-spi-private/src/main/java/org/keycloak/cluster/ManagedCacheManagerProvider.java create mode 100644 testsuite/model/src/test/resources/test-ispn.xml diff --git a/docs/documentation/upgrading/topics/changes/changes-26_3_0.adoc b/docs/documentation/upgrading/topics/changes/changes-26_3_0.adoc index 30c2b2ace4b..aa405b40e1c 100644 --- a/docs/documentation/upgrading/topics/changes/changes-26_3_0.adoc +++ b/docs/documentation/upgrading/topics/changes/changes-26_3_0.adoc @@ -24,6 +24,20 @@ When you want the user, who is authenticated to your client application, to link mechanism with the action `idp_link`. The proprietary custom protocol for client initiated account linking is deprecated now and might be removed in the future versions. For more information, see the Client initiated account link section of the link:{developerguide_link}[{developerguide_name}]. +=== Deprecation of `spi-connections-infinispan-quarkus-site-name` + +The option `spi-connections-infinispan-quarkus-site-name` is deprecated and no longer used for multi-site setups, and it will be removed in the future. +Use `spi-cache-embedded-default-site-name` instead in setups when running with embedded distributed caches. +See the https://www.keycloak.org/server/all-provider-config[All provider configuration] for more details on these options. + +=== Removal of `jboss.site.name` and `jboss.node.name` + +Both system properties have been used internally within Keycloak and have not been part of the official documentation. +{project_name} will fail to start if those are present. + +Instead, use the command line option `spi-cache-embedded-default-site-name` as `jboss.site.name` replacement, and `spi-cache-embedded-default-node-name` as `jboss.node.name` replacement. +See the https://www.keycloak.org/server/all-provider-config[All provider configuration] for more details on these options. + == Notable changes Notable changes where an internal behavior changed to prevent common misconfigurations, fix bugs or simplify running {project_name}. diff --git a/docs/guides/high-availability/deploy-infinispan-kubernetes-crossdc.adoc b/docs/guides/high-availability/deploy-infinispan-kubernetes-crossdc.adoc index 6ed404cd62c..15cf8f54663 100644 --- a/docs/guides/high-availability/deploy-infinispan-kubernetes-crossdc.adoc +++ b/docs/guides/high-availability/deploy-infinispan-kubernetes-crossdc.adoc @@ -304,9 +304,6 @@ include::examples/generated/keycloak-ispn.yaml[tag=keycloak-ispn] This is optional and it defaults to `11222`. <3> The Secret `name` and `key` with the {jdgserver_name} username credential. <4> The Secret `name` and `key` with the {jdgserver_name} password credential. -<5> The `spi-connections-infinispan-quarkus-site-name` is an arbitrary {jdgserver_name} site name which {project_name} needs for its Infinispan caches deployment when a remote store is used. -This site-name is related only to the Infinispan caches and does not need to match any value from the external {jdgserver_name} deployment. -If you are using multiple sites for {project_name} in a cross-DC setup such as <@links.ha id="deploy-infinispan-kubernetes-crossdc" />, the site name must be different in each site. === Architecture diff --git a/docs/guides/high-availability/examples/generated/keycloak-ispn.yaml b/docs/guides/high-availability/examples/generated/keycloak-ispn.yaml index 6b8f5bf36bb..bcbb91d0682 100644 --- a/docs/guides/high-availability/examples/generated/keycloak-ispn.yaml +++ b/docs/guides/high-availability/examples/generated/keycloak-ispn.yaml @@ -490,7 +490,7 @@ spec: secret: name: remote-store-secret key: password - - name: spi-connections-infinispan-quarkus-site-name # <5> + - name: spi-cache-embedded-default-site-name # <5> value: keycloak # end::keycloak-ispn[] - name: db-driver diff --git a/docs/guides/high-availability/examples/generated/keycloak.yaml b/docs/guides/high-availability/examples/generated/keycloak.yaml index 52271693980..7df8f4644ab 100644 --- a/docs/guides/high-availability/examples/generated/keycloak.yaml +++ b/docs/guides/high-availability/examples/generated/keycloak.yaml @@ -481,7 +481,7 @@ spec: secret: name: remote-store-secret key: password - - name: spi-connections-infinispan-quarkus-site-name + - name: spi-cache-embedded-default-site-name value: keycloak - name: db-driver value: software.amazon.jdbc.Driver diff --git a/model/infinispan/pom.xml b/model/infinispan/pom.xml index 7e511adc495..c3717a985c7 100755 --- a/model/infinispan/pom.xml +++ b/model/infinispan/pom.xml @@ -88,6 +88,11 @@ infinispan-component-processor provided + + io.micrometer + micrometer-core + provided + junit diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java index 117fb3cf948..55511c80597 100755 --- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java @@ -17,9 +17,7 @@ package org.keycloak.connections.infinispan; -import java.util.Iterator; import java.util.List; -import java.util.ServiceLoader; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; @@ -27,28 +25,18 @@ import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import org.infinispan.client.hotrod.RemoteCacheManager; -import org.infinispan.commons.dataconversion.MediaType; -import org.infinispan.configuration.cache.CacheMode; import org.infinispan.configuration.cache.Configuration; -import org.infinispan.configuration.cache.ConfigurationBuilder; -import org.infinispan.configuration.global.GlobalConfigurationBuilder; import org.infinispan.eviction.EvictionStrategy; import org.infinispan.manager.DefaultCacheManager; import org.infinispan.manager.EmbeddedCacheManager; -import org.infinispan.transaction.LockingMode; -import org.infinispan.transaction.TransactionMode; -import org.infinispan.transaction.lookup.EmbeddedTransactionManagerLookup; import org.jboss.logging.Logger; import org.keycloak.Config; import org.keycloak.cluster.ClusterEvent; import org.keycloak.cluster.ClusterProvider; -import org.keycloak.cluster.ManagedCacheManagerProvider; import org.keycloak.connections.infinispan.remote.RemoteInfinispanConnectionProvider; -import org.keycloak.connections.jpa.JpaConnectionProvider; import org.keycloak.infinispan.util.InfinispanUtils; import org.keycloak.marshalling.KeycloakIndexSchemaUtil; import org.keycloak.marshalling.KeycloakModelSchema; -import org.keycloak.marshalling.Marshalling; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.cache.infinispan.ClearCacheEvent; @@ -63,35 +51,21 @@ import org.keycloak.models.utils.PostMigrationEvent; import org.keycloak.provider.InvalidationHandler.ObjectType; import org.keycloak.provider.Provider; import org.keycloak.provider.ProviderEvent; +import org.keycloak.provider.ProviderEventListener; +import org.keycloak.spi.infinispan.CacheEmbeddedConfigProvider; import org.keycloak.spi.infinispan.CacheRemoteConfigProvider; +import org.keycloak.spi.infinispan.impl.embedded.CacheConfigurator; -import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.ACTION_TOKEN_CACHE; import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME; -import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.AUTHORIZATION_CACHE_NAME; -import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.AUTHORIZATION_REVISIONS_CACHE_DEFAULT_MAX; -import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.AUTHORIZATION_REVISIONS_CACHE_NAME; import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME; import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.CRL_CACHE_NAME; -import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.JGROUPS_BIND_ADDR; -import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.JGROUPS_UDP_MCAST_ADDR; -import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.JMX_DOMAIN; import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.KEYS_CACHE_DEFAULT_MAX; import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.KEYS_CACHE_MAX_IDLE_SECONDS; import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.KEYS_CACHE_NAME; import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.LOGIN_FAILURE_CACHE_NAME; import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME; import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME; -import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.REALM_CACHE_NAME; -import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.REALM_REVISIONS_CACHE_DEFAULT_MAX; -import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.REALM_REVISIONS_CACHE_NAME; -import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.USER_CACHE_NAME; -import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.USER_REVISIONS_CACHE_DEFAULT_MAX; -import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.USER_REVISIONS_CACHE_NAME; import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.USER_SESSION_CACHE_NAME; -import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.WORK_CACHE_NAME; -import static org.keycloak.connections.infinispan.InfinispanUtil.configureTransport; -import static org.keycloak.connections.infinispan.InfinispanUtil.createCacheConfigurationBuilder; -import static org.keycloak.connections.infinispan.InfinispanUtil.getActionTokenCacheConfig; import static org.keycloak.connections.infinispan.InfinispanUtil.setTimeServiceToKeycloakTime; import static org.keycloak.models.cache.infinispan.InfinispanCacheRealmProviderFactory.REALM_CLEAR_CACHE_EVENTS; import static org.keycloak.models.cache.infinispan.InfinispanCacheRealmProviderFactory.REALM_INVALIDATION_EVENTS; @@ -99,7 +73,7 @@ import static org.keycloak.models.cache.infinispan.InfinispanCacheRealmProviderF /** * @author Stian Thorgersen */ -public class DefaultInfinispanConnectionProviderFactory implements InfinispanConnectionProviderFactory { +public class DefaultInfinispanConnectionProviderFactory implements InfinispanConnectionProviderFactory, ProviderEventListener { private static final ReadWriteLock READ_WRITE_LOCK = new ReentrantReadWriteLock(); private static final Logger logger = Logger.getLogger(DefaultInfinispanConnectionProviderFactory.class); @@ -108,8 +82,6 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon private volatile EmbeddedCacheManager cacheManager; - protected volatile boolean containerManaged; - private volatile TopologyInfo topologyInfo; private volatile RemoteCacheManager remoteCacheManager; @@ -156,6 +128,7 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon runWithWriteLockOnCacheManager(() -> { if (cacheManager != null) { cacheManager.stop(); + cacheManager = null; } }); if (remoteCacheManager != null) { @@ -176,11 +149,7 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon @Override public void postInit(KeycloakSessionFactory factory) { - factory.register((ProviderEvent event) -> { - if (event instanceof PostMigrationEvent) { - KeycloakModelUtils.runJobInTransaction(factory, this::registerSystemWideListeners); - } - }); + factory.register(this); } protected void lazyInit(KeycloakSession keycloakSession) { @@ -188,43 +157,30 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon return; } synchronized (this) { + // if cacheManager it not null, the remoteCacheManager must be visible too. if (cacheManager != null) { return; } - EmbeddedCacheManager managedCacheManager = null; - Iterator providers = ServiceLoader.load(ManagedCacheManagerProvider.class, DefaultInfinispanConnectionProvider.class.getClassLoader()) - .iterator(); - - if (providers.hasNext()) { - ManagedCacheManagerProvider provider = providers.next(); - - if (providers.hasNext()) { - throw new RuntimeException("Multiple " + org.keycloak.cluster.ManagedCacheManagerProvider.class + " providers found."); - } - - managedCacheManager = provider.getEmbeddedCacheManager(keycloakSession, config); - } - - // store it in a locale variable first, so it is not visible to the outside, yet - EmbeddedCacheManager localCacheManager; - if (managedCacheManager == null) { - if (!config.getBoolean("embedded", false)) { - throw new RuntimeException("No " + ManagedCacheManagerProvider.class.getName() + " found. If running in embedded mode set the [embedded] property to this provider."); - } - localCacheManager = initEmbedded(); - } else { - localCacheManager = initContainerManaged(managedCacheManager); - } - + var cm = createEmbeddedCacheManager(keycloakSession); + this.remoteCacheManager = createRemoteCacheManager(keycloakSession); + this.topologyInfo = new TopologyInfo(cm); + injectKeycloakTimeService(cm); + // set cacheManager field last + this.cacheManager = cm; logger.infof(topologyInfo.toString()); - - - // only set the cache manager attribute at the very end to avoid passing a half-initialized entry callers - cacheManager = localCacheManager; - remoteCacheManager = createRemoteCacheManager(keycloakSession); } } + protected EmbeddedCacheManager createEmbeddedCacheManager(KeycloakSession session) { + var holder = session.getProvider(CacheEmbeddedConfigProvider.class).configuration(); + var cm = new DefaultCacheManager(holder, true); + cm.getCache(KEYS_CACHE_NAME, true); + cm.getCache(CRL_CACHE_NAME, true); + + logger.debugv("Using container managed Infinispan cache container, lookup={0}", cm); + return cm; + } + protected RemoteCacheManager createRemoteCacheManager(KeycloakSession session) { var remoteConfig = session.getProvider(CacheRemoteConfigProvider.class).configuration(); if (remoteConfig.isEmpty()) { @@ -248,173 +204,29 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon return rcm; } + /** + * @deprecated not invoked anymore. Overwrite {@link #createEmbeddedCacheManager(KeycloakSession)}. + */ + @Deprecated(since = "26.0", forRemoval = true) protected EmbeddedCacheManager initContainerManaged(EmbeddedCacheManager cacheManager) { - containerManaged = true; - - defineRevisionCache(cacheManager, REALM_CACHE_NAME, REALM_REVISIONS_CACHE_NAME, REALM_REVISIONS_CACHE_DEFAULT_MAX); - defineRevisionCache(cacheManager, USER_CACHE_NAME, USER_REVISIONS_CACHE_NAME, USER_REVISIONS_CACHE_DEFAULT_MAX); - defineRevisionCache(cacheManager, AUTHORIZATION_CACHE_NAME, AUTHORIZATION_REVISIONS_CACHE_NAME, AUTHORIZATION_REVISIONS_CACHE_DEFAULT_MAX); - - cacheManager.getCache(KEYS_CACHE_NAME, true); - cacheManager.getCache(CRL_CACHE_NAME, true); - - this.topologyInfo = new TopologyInfo(cacheManager, config, false, getId()); - - logger.debugv("Using container managed Infinispan cache container, lookup={0}", cacheManager); - - return cacheManager; + throw new UnsupportedOperationException(); } + /** + * @deprecated not used anymore. Overwrite {@link #createEmbeddedCacheManager(KeycloakSession)} if you want to + * create a custom {@link EmbeddedCacheManager}. + */ + @Deprecated(since = "26.3", forRemoval = true) protected EmbeddedCacheManager initEmbedded() { - GlobalConfigurationBuilder gcb = new GlobalConfigurationBuilder(); - - boolean clustered = config.getBoolean("clustered", false); - boolean async = config.getBoolean("async", false); - boolean useKeycloakTimeService = config.getBoolean("useKeycloakTimeService", false); - - this.topologyInfo = new TopologyInfo(cacheManager, config, true, getId()); - - if (clustered) { - String jgroupsUdpMcastAddr = config.get("jgroupsUdpMcastAddr", System.getProperty(JGROUPS_UDP_MCAST_ADDR)); - String jgroupsBindAddr = config.get("jgroupsBindAddr", System.getProperty(JGROUPS_BIND_ADDR)); - configureTransport(gcb, topologyInfo.getMyNodeName(), topologyInfo.getMySiteName(), jgroupsUdpMcastAddr, jgroupsBindAddr, - "default-configs/default-keycloak-jgroups-udp.xml"); - gcb.jmx() - .domain(JMX_DOMAIN + "-" + topologyInfo.getMyNodeName()).enable(); - } else { - gcb.jmx().domain(JMX_DOMAIN).enable(); - } - - Marshalling.configure(gcb); - - if (InfinispanUtils.isRemoteInfinispan()) { - // Disable JGroups, not required when the data is stored in the Remote Cache. - // The existing caches are local and do not require JGroups to work properly. - gcb.nonClusteredDefault(); - } - - EmbeddedCacheManager cacheManager = new DefaultCacheManager(gcb.build()); - if (useKeycloakTimeService) { - setTimeServiceToKeycloakTime(cacheManager); - } - containerManaged = false; - - logger.debug("Started embedded Infinispan cache container"); - - var localConfiguration = createCacheConfigurationBuilder().build(); - - // local caches first - defineLocalCache(cacheManager, REALM_CACHE_NAME, REALM_REVISIONS_CACHE_NAME, localConfiguration, REALM_REVISIONS_CACHE_DEFAULT_MAX); - defineLocalCache(cacheManager, AUTHORIZATION_CACHE_NAME, AUTHORIZATION_REVISIONS_CACHE_NAME, localConfiguration, AUTHORIZATION_REVISIONS_CACHE_DEFAULT_MAX); - defineLocalCache(cacheManager, USER_CACHE_NAME, USER_REVISIONS_CACHE_NAME, localConfiguration, USER_REVISIONS_CACHE_DEFAULT_MAX); - - cacheManager.defineConfiguration(KEYS_CACHE_NAME, getKeysCacheConfig()); - cacheManager.getCache(KEYS_CACHE_NAME, true); - - cacheManager.defineConfiguration(CRL_CACHE_NAME, getCrlCacheConfig()); - cacheManager.getCache(CRL_CACHE_NAME, true); - - var builder = createCacheConfigurationBuilder(); - if (clustered) { - builder.simpleCache(false); - String sessionsMode = config.get("sessionsMode", "distributed"); - if (sessionsMode.equalsIgnoreCase("replicated")) { - builder.clustering().cacheMode(async ? CacheMode.REPL_ASYNC : CacheMode.REPL_SYNC); - } else if (sessionsMode.equalsIgnoreCase("distributed")) { - builder.clustering().cacheMode(async ? CacheMode.DIST_ASYNC : CacheMode.DIST_SYNC); - } else { - throw new RuntimeException("Invalid value for sessionsMode"); - } - - int owners = config.getInt("sessionsOwners", 2); - logger.debugf("Session owners: %d", owners); - - int l1Lifespan = config.getInt("l1Lifespan", 600000); - boolean l1Enabled = l1Lifespan > 0; - Boolean awaitInitialTransfer = config.getBoolean("awaitInitialTransfer", true); - builder.clustering() - .hash() - .numOwners(owners) - .numSegments(config.getInt("sessionsSegments", 60)) - .l1() - .enabled(l1Enabled) - .lifespan(l1Lifespan) - .stateTransfer().awaitInitialTransfer(awaitInitialTransfer).timeout(30, TimeUnit.SECONDS); - } - - if (InfinispanUtils.isEmbeddedInfinispan()) { - // Base configuration doesn't contain any remote stores - var clusteredConfiguration = builder.build(); - - defineClusteredCache(cacheManager, USER_SESSION_CACHE_NAME, clusteredConfiguration); - defineClusteredCache(cacheManager, OFFLINE_USER_SESSION_CACHE_NAME, clusteredConfiguration); - defineClusteredCache(cacheManager, CLIENT_SESSION_CACHE_NAME, clusteredConfiguration); - defineClusteredCache(cacheManager, OFFLINE_CLIENT_SESSION_CACHE_NAME, clusteredConfiguration); - - defineClusteredCache(cacheManager, LOGIN_FAILURE_CACHE_NAME, clusteredConfiguration); - defineClusteredCache(cacheManager, AUTHENTICATION_SESSIONS_CACHE_NAME, clusteredConfiguration); - - var actionTokenBuilder = getActionTokenCacheConfig(); - if (clustered) { - actionTokenBuilder.simpleCache(false); - actionTokenBuilder.clustering().cacheMode(async ? CacheMode.REPL_ASYNC : CacheMode.REPL_SYNC); - } - defineClusteredCache(cacheManager, ACTION_TOKEN_CACHE, actionTokenBuilder.build()); - - var workBuilder = createCacheConfigurationBuilder() - .expiration().enableReaper().wakeUpInterval(15, TimeUnit.SECONDS); - if (clustered) { - workBuilder.simpleCache(false); - workBuilder.clustering().cacheMode(async ? CacheMode.REPL_ASYNC : CacheMode.REPL_SYNC); - } - defineClusteredCache(cacheManager, WORK_CACHE_NAME, workBuilder.build()); - } - - return cacheManager; - } - - private void defineLocalCache(EmbeddedCacheManager cacheManager, String cacheName, String revCacheName, Configuration configuration, long defaultMaxEntries) { - cacheManager.defineConfiguration(cacheName, configuration); - defineRevisionCache(cacheManager, cacheName, revCacheName, defaultMaxEntries); - } - - private void defineRevisionCache(EmbeddedCacheManager cacheManager, String cacheName, String revCacheName, long defaultMaxEntries) { - var maxCount = cacheManager.getCache(cacheName).getCacheConfiguration().memory().maxCount(); - maxCount = maxCount > 0 ? 2 * maxCount : defaultMaxEntries; - cacheManager.defineConfiguration(revCacheName, getRevisionCacheConfig(maxCount)); - cacheManager.getCache(revCacheName); - } - - private void defineClusteredCache(EmbeddedCacheManager cacheManager, String cacheName, Configuration baseConfiguration) { - // copy base configuration - var builder = createCacheConfigurationBuilder(); - builder.read(baseConfiguration); - cacheManager.defineConfiguration(cacheName, builder.build()); - cacheManager.getCache(cacheName); - } - - private Configuration getRevisionCacheConfig(long maxEntries) { - ConfigurationBuilder cb = createCacheConfigurationBuilder(); - cb.simpleCache(false); - cb.invocationBatching().enable().transaction().transactionMode(TransactionMode.TRANSACTIONAL); - - // Use Embedded manager even in managed ( wildfly/eap ) environment. We don't want infinispan to participate in global transaction - cb.transaction().transactionManagerLookup(new EmbeddedTransactionManagerLookup()); - - cb.transaction().lockingMode(LockingMode.PESSIMISTIC); - if (cb.memory().storage().canStoreReferences()) { - cb.encoding().mediaType(MediaType.APPLICATION_OBJECT_TYPE); - } - - cb.memory() - .whenFull(EvictionStrategy.REMOVE) - .maxCount(maxEntries); - - return cb.build(); + throw new UnsupportedOperationException(); } + /** + * @deprecated not used anymore + */ + @Deprecated(since = "26.3", forRemoval = true) protected Configuration getKeysCacheConfig() { - ConfigurationBuilder cb = createCacheConfigurationBuilder(); + var cb = CacheConfigurator.createCacheConfigurationBuilder(); cb.memory() .whenFull(EvictionStrategy.REMOVE) @@ -425,8 +237,12 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon return cb.build(); } + /** + * @deprecated Use {@link CacheConfigurator#getCrlCacheConfig()} + */ + @Deprecated(since = "26.3", forRemoval = true) protected Configuration getCrlCacheConfig() { - return InfinispanUtil.getCrlCacheConfig().build(); + return CacheConfigurator.getCrlCacheConfig().build(); } private void registerSystemWideListeners(KeycloakSession session) { @@ -446,8 +262,21 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon }); } + private void injectKeycloakTimeService(EmbeddedCacheManager cacheManager) { + if (config.getBoolean("useKeycloakTimeService", Boolean.FALSE)) { + setTimeServiceToKeycloakTime(cacheManager); + } + } + @Override public Set> dependsOn() { - return Set.of(JpaConnectionProvider.class, CacheRemoteConfigProvider.class); + return Set.of(CacheRemoteConfigProvider.class, CacheEmbeddedConfigProvider.class); + } + + @Override + public void onEvent(ProviderEvent event) { + if (event instanceof PostMigrationEvent pme) { + KeycloakModelUtils.runJobInTransaction(pme.getFactory(), this::registerSystemWideListeners); + } } } diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java index 38a0f303f0e..8e0771beba5 100755 --- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java +++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanConnectionProvider.java @@ -114,6 +114,21 @@ public interface InfinispanConnectionProvider extends Provider { String[] ALL_CACHES_NAME = Stream.concat(Arrays.stream(LOCAL_CACHE_NAMES), Arrays.stream(CLUSTERED_CACHE_NAMES)).toArray(String[]::new); + String[] LOCAL_MAX_COUNT_CACHES = new String[]{ + AUTHORIZATION_CACHE_NAME, + CRL_CACHE_NAME, + KEYS_CACHE_NAME, + REALM_CACHE_NAME, + USER_CACHE_NAME + }; + + String[] CLUSTERED_MAX_COUNT_CACHES = new String[]{ + CLIENT_SESSION_CACHE_NAME, + OFFLINE_USER_SESSION_CACHE_NAME, + OFFLINE_CLIENT_SESSION_CACHE_NAME, + USER_SESSION_CACHE_NAME + }; + /** * * Effectively the same as {@link InfinispanConnectionProvider#getCache(String, boolean)} with createIfAbsent set to {@code true} diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanUtil.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanUtil.java index 02a364d2d65..95f8e03d721 100644 --- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanUtil.java +++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/InfinispanUtil.java @@ -17,7 +17,10 @@ package org.keycloak.connections.infinispan; -import org.infinispan.commons.dataconversion.MediaType; +import java.time.Instant; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + import org.infinispan.commons.time.TimeService; import org.infinispan.commons.util.FileLookup; import org.infinispan.commons.util.FileLookupFactory; @@ -35,10 +38,7 @@ import org.jboss.logging.Logger; import org.jgroups.JChannel; import org.keycloak.common.util.Time; import org.keycloak.models.KeycloakSession; - -import java.time.Instant; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; +import org.keycloak.spi.infinispan.impl.embedded.CacheConfigurator; /** * @author Marek Posolda @@ -56,6 +56,10 @@ public class InfinispanUtil { private static final Object CHANNEL_INIT_SYNCHRONIZER = new Object(); + /** + * @deprecated to be removed without replacement. + */ + @Deprecated(since = "26.3", forRemoval = true) public static void configureTransport(GlobalConfigurationBuilder gcb, String nodeName, String siteName, String jgroupsUdpMcastAddr, String jgroupsBindAddr, String jgroupsConfigPath) { if (nodeName == null) { @@ -115,20 +119,20 @@ public class InfinispanUtil { } } + /** + * @deprecated to be removed. Use {@link CacheConfigurator#createCacheConfigurationBuilder()}. + */ + @Deprecated(since = "26.3", forRemoval = true) public static ConfigurationBuilder createCacheConfigurationBuilder() { - ConfigurationBuilder builder = new ConfigurationBuilder(); - - // need to force the encoding to application/x-java-object to avoid unnecessary conversion of keys/values. See WFLY-14356. - builder.encoding().mediaType(MediaType.APPLICATION_OBJECT_TYPE); - - // needs to be disabled if transaction is enabled - builder.simpleCache(true); - - return builder; + return CacheConfigurator.createCacheConfigurationBuilder(); } + /** + * @deprecated to be removed without replacement. + */ + @Deprecated(since = "26.3", forRemoval = true) public static ConfigurationBuilder getActionTokenCacheConfig() { - ConfigurationBuilder cb = createCacheConfigurationBuilder(); + var cb = CacheConfigurator.createCacheConfigurationBuilder(); cb.memory() .whenFull(EvictionStrategy.MANUAL) @@ -140,19 +144,26 @@ public class InfinispanUtil { return cb; } + /** + * @deprecated to be removed. Use {@link CacheConfigurator#getCrlCacheConfig()}. + */ + @Deprecated(since = "26.3", forRemoval = true) public static ConfigurationBuilder getCrlCacheConfig() { - var builder = createCacheConfigurationBuilder(); + return CacheConfigurator.getCrlCacheConfig(); + } - builder.memory() - .whenFull(EvictionStrategy.REMOVE) - .maxCount(InfinispanConnectionProvider.CRL_CACHE_DEFAULT_MAX); - - return builder; + /** + * @deprecated to be removed. Use {@link CacheConfigurator#getRevisionCacheConfig(long)}. + */ + @Deprecated(since = "26.3", forRemoval = true) + public static ConfigurationBuilder getRevisionCacheConfig(long maxEntries) { + return CacheConfigurator.getRevisionCacheConfig(maxEntries); } /** * Replaces the {@link TimeService} in infinispan with the one that respects Keycloak {@link Time}. - * @param cacheManager + * + * @param cacheManager The {@link EmbeddedCacheManager} to inject the Keycloak {@link Time}. * @return Runnable to revert replacement of the infinispan time service */ public static Runnable setTimeServiceToKeycloakTime(EmbeddedCacheManager cacheManager) { @@ -172,14 +183,13 @@ public class InfinispanUtil { /** * Forked from org.infinispan.test.TestingUtil class - * + *

* Replaces a component in a running cache manager (global component registry). * - * @param cacheMgr cache in which to replace component + * @param cacheMgr cache in which to replace component * @param componentType component type of which to replace * @param replacementComponent new instance * @param rewire if true, ComponentRegistry.rewire() is called after replacing. - * * @return the original component that was replaced */ private static T replaceComponent(EmbeddedCacheManager cacheMgr, Class componentType, T replacementComponent, boolean rewire) { diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/TopologyInfo.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/TopologyInfo.java index 56bf4feaf42..021de2898cf 100644 --- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/TopologyInfo.java +++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/TopologyInfo.java @@ -17,6 +17,10 @@ package org.keycloak.connections.infinispan; +import java.net.InetSocketAddress; +import java.security.SecureRandom; +import java.util.Objects; + import org.infinispan.Cache; import org.infinispan.distribution.DistributionManager; import org.infinispan.factories.GlobalComponentRegistry; @@ -31,10 +35,6 @@ import org.jgroups.stack.IpAddress; import org.jgroups.util.NameCache; import org.keycloak.Config; -import java.net.InetSocketAddress; -import java.security.SecureRandom; -import java.util.Objects; - /** * @author Marek Posolda */ @@ -44,7 +44,6 @@ public class TopologyInfo { // Node name used in clustered environment. This typically points to "jboss.node.name" . If "jboss.node.name" is not set, it is randomly generated - // name private final String myNodeName; // Used just if "site" is configured (typically in multi-site environment). Otherwise null @@ -53,67 +52,37 @@ public class TopologyInfo { private final boolean isGeneratedNodeName; + /** + * @deprecated Use {@link #TopologyInfo(EmbeddedCacheManager)} instead. + */ + @Deprecated(since = "26.3", forRemoval = true) public TopologyInfo(EmbeddedCacheManager cacheManager, Config.Scope config, boolean embedded, String providerId) { - String siteName; - String nodeName; - boolean isGeneratedNodeName = false; - - if (System.getProperty(InfinispanConnectionProvider.JBOSS_SITE_NAME) != null) { - throw new IllegalArgumentException( - String.format("System property %s is in use. Use --spi-connections-infinispan-%s-site-name config option instead", - InfinispanConnectionProvider.JBOSS_SITE_NAME, providerId)); - } - - if (!embedded) { - var addr = cacheManager.getAddress(); - if (addr != null) { - nodeName = addr.toString(); - siteName = cacheManager.getCacheManagerConfiguration().transport().siteId(); - if (siteName == null) { - siteName = config.get("siteName"); - } - } else { - nodeName = System.getProperty(InfinispanConnectionProvider.JBOSS_NODE_NAME); - siteName = config.get("siteName"); - } - if (nodeName == null || nodeName.equals("localhost")) { - isGeneratedNodeName = true; - nodeName = generateNodeName(); - } - } else { - boolean clustered = config.getBoolean("clustered", false); - - nodeName = config.get("nodeName", System.getProperty(InfinispanConnectionProvider.JBOSS_NODE_NAME)); - if (nodeName != null && nodeName.isEmpty()) { - nodeName = null; - } - - siteName = config.get("siteName"); - if (siteName != null && siteName.isEmpty()) { - siteName = null; - } - - if (nodeName == null) { - if (!clustered) { - isGeneratedNodeName = true; - nodeName = generateNodeName(); - } else { - throw new IllegalStateException("You must set jboss.node.name if you use clustered mode for InfinispanConnectionProvider"); - } - } - } - - this.myNodeName = nodeName; - this.mySiteName = siteName; - this.isGeneratedNodeName = isGeneratedNodeName; + this(cacheManager); } + public TopologyInfo(EmbeddedCacheManager cacheManager) { + var transportConfig = cacheManager.getCacheManagerConfiguration().transport(); + var transport = GlobalComponentRegistry.componentOf(cacheManager, Transport.class); - private String generateNodeName() { + if (transport == null) { + // non-clustered mode + // if the user sets a node name, use it; otherwise, generate the node name. + var nodeName = transportConfig.nodeName(); + this.isGeneratedNodeName = nodeName == null || nodeName.isEmpty(); + this.myNodeName = isGeneratedNodeName ? generateNodeName() : nodeName; + } else { + // clustered mode + // returns the node name configured; if not set, the node name generated by JGroups. + this.myNodeName = transport.localNodeName(); + this.isGeneratedNodeName = false; + } + this.mySiteName = transportConfig.siteId(); + } + + private static String generateNodeName() { return InfinispanConnectionProvider.NODE_PREFIX + new SecureRandom().nextInt(1000000); } - public String getMyNodeName() { return myNodeName; } @@ -122,13 +91,11 @@ public class TopologyInfo { return mySiteName; } - @Override public String toString() { return String.format("Node name: %s, Site name: %s", myNodeName, mySiteName); } - /** * True if I am primary owner of the key in case of distributed caches. In case of local caches, always return true */ diff --git a/model/infinispan/src/main/java/org/keycloak/jgroups/JGroupsConfigurator.java b/model/infinispan/src/main/java/org/keycloak/jgroups/JGroupsConfigurator.java deleted file mode 100644 index 7d032070e5a..00000000000 --- a/model/infinispan/src/main/java/org/keycloak/jgroups/JGroupsConfigurator.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright 2025 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.jgroups; - -import java.lang.invoke.MethodHandles; -import java.util.ArrayList; -import java.util.List; - -import org.infinispan.configuration.parsing.ConfigurationBuilderHolder; -import org.jboss.logging.Logger; -import org.keycloak.Config; -import org.keycloak.connections.infinispan.InfinispanConnectionSpi; -import org.keycloak.infinispan.util.InfinispanUtils; -import org.keycloak.jgroups.impl.JGroupsJdbcPingStackConfigurator; -import org.keycloak.jgroups.impl.JpaJGroupsTlsConfigurator; -import org.keycloak.models.KeycloakSession; - -/** - * Configures the JGroups stacks before starting Infinispan. - */ -public class JGroupsConfigurator { - - public static final Logger logger = Logger.getLogger(MethodHandles.lookup().lookupClass()); - - private final ConfigurationBuilderHolder holder; - private final List stackConfiguratorList; - - private JGroupsConfigurator(ConfigurationBuilderHolder holder, List stackConfiguratorList) { - this.holder = holder; - this.stackConfiguratorList = stackConfiguratorList; - } - - private static void createJdbcPingConfigurator(ConfigurationBuilderHolder holder, List configurator) { - var stackXmlAttribute = JGroupsUtil.transportStackOf(holder); - if (stackXmlAttribute.isModified() && !isJdbcPingStack(stackXmlAttribute.get())) { - logger.debugf("Custom stack configured (%s). JDBC_PING discovery disabled.", stackXmlAttribute.get()); - return; - } - logger.debug("JDBC_PING discovery enabled."); - if (!stackXmlAttribute.isModified()) { - // defaults to jdbc-ping - JGroupsUtil.transportOf(holder).stack("jdbc-ping"); - } - configurator.add(JGroupsJdbcPingStackConfigurator.INSTANCE); - } - - private static boolean isJdbcPingStack(String stackName) { - return "jdbc-ping".equals(stackName) || "jdbc-ping-udp".equals(stackName); - } - - private static void createTlsConfigurator(List configurator) { - configurator.add(JpaJGroupsTlsConfigurator.INSTANCE); - } - - private static boolean isLocal(ConfigurationBuilderHolder holder) { - return JGroupsUtil.transportOf(holder).getTransport() == null; - } - - public static JGroupsConfigurator create(ConfigurationBuilderHolder holder) { - if (InfinispanUtils.isRemoteInfinispan() || isLocal(holder)) { - logger.debug("Multi Site or local mode. Skipping JGroups configuration."); - return new JGroupsConfigurator(holder, List.of()); - } - // Configure stack from CLI options to Global Configuration - var stack = Config.scope(InfinispanConnectionSpi.SPI_NAME, "quarkus").get("stack"); - if (stack != null) { - JGroupsUtil.transportOf(holder).stack(stack); - } - var configurator = new ArrayList(2); - createJdbcPingConfigurator(holder, configurator); - createTlsConfigurator(configurator); - return new JGroupsConfigurator(holder, List.copyOf(configurator)); - } - - /** - * @return The {@link ConfigurationBuilderHolder} with the current Infinispan configuration. - */ - public ConfigurationBuilderHolder holder() { - return holder; - } - - /** - * @return {@code true} if Keycloak is run in local mode (development mode for example) and JGroups won't be used. - */ - public boolean isLocal() { - return isLocal(holder); - } - - /** - * Configures the JGroups stack. - * - * @param session The {@link KeycloakSession}. - */ - public void configure(KeycloakSession session) { - if (InfinispanUtils.isRemoteInfinispan() || isLocal()) { - return; - } - stackConfiguratorList.forEach(jGroupsStackConfigurator -> jGroupsStackConfigurator.configure(holder, session)); - JGroupsUtil.warnDeprecatedStack(holder); - } - -} diff --git a/model/infinispan/src/main/java/org/keycloak/jgroups/JGroupsUtil.java b/model/infinispan/src/main/java/org/keycloak/jgroups/JGroupsUtil.java deleted file mode 100644 index 90d7f253cb3..00000000000 --- a/model/infinispan/src/main/java/org/keycloak/jgroups/JGroupsUtil.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2025 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.jgroups; - -import org.infinispan.commons.configuration.attributes.Attribute; -import org.infinispan.configuration.global.TransportConfigurationBuilder; -import org.infinispan.configuration.parsing.ConfigurationBuilderHolder; -import org.jgroups.protocols.TCP_NIO2; -import org.jgroups.protocols.UDP; - -import static org.infinispan.configuration.global.TransportConfiguration.STACK; - -public final class JGroupsUtil { - - private JGroupsUtil() { - } - - public static TransportConfigurationBuilder transportOf(ConfigurationBuilderHolder holder) { - return holder.getGlobalConfigurationBuilder().transport(); - } - - public static Attribute transportStackOf(ConfigurationBuilderHolder holder) { - var transport = transportOf(holder); - assert transport != null; - return transport.attributes().attribute(STACK); - } - - public static void warnDeprecatedStack(ConfigurationBuilderHolder holder) { - var stackName = transportStackOf(holder).get(); - switch (stackName) { - case "jdbc-ping-udp": - case "tcp": - case "udp": - case "azure": - case "ec2": - case "google": - JGroupsConfigurator.logger.warnf("Stack '%s' is deprecated. We recommend to use 'jdbc-ping' instead", stackName); - } - } - - public static void validateTlsAvailable(ConfigurationBuilderHolder holder) { - var stackName = transportStackOf(holder).get(); - if (stackName == null) { - // unable to validate - return; - } - var config = transportOf(holder).build(); - for (var protocol : config.transport().jgroups().configurator(stackName).getProtocolStack()) { - var name = protocol.getProtocolName(); - if (name.equals(UDP.class.getSimpleName()) || - name.equals(UDP.class.getName()) || - name.equals(TCP_NIO2.class.getSimpleName()) || - name.equals(TCP_NIO2.class.getName())) { - throw new RuntimeException("Cache TLS is not available with protocol " + name); - } - } - - } - -} diff --git a/model/infinispan/src/main/java/org/keycloak/jgroups/impl/JGroupsJdbcPingStackConfigurator.java b/model/infinispan/src/main/java/org/keycloak/jgroups/impl/JGroupsJdbcPingStackConfigurator.java deleted file mode 100644 index 06c80e9349b..00000000000 --- a/model/infinispan/src/main/java/org/keycloak/jgroups/impl/JGroupsJdbcPingStackConfigurator.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright 2025 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.jgroups.impl; - -import java.util.List; -import java.util.Map; - -import org.infinispan.configuration.parsing.ConfigurationBuilderHolder; -import org.infinispan.remoting.transport.jgroups.EmbeddedJGroupsChannelConfigurator; -import org.jgroups.conf.ClassConfigurator; -import org.jgroups.conf.ProtocolConfiguration; -import org.jgroups.protocols.JDBC_PING2; -import org.jgroups.stack.Protocol; -import org.keycloak.connections.jpa.JpaConnectionProvider; -import org.keycloak.connections.jpa.JpaConnectionProviderFactory; -import org.keycloak.connections.jpa.util.JpaUtils; -import org.keycloak.jgroups.JGroupsConfigurator; -import org.keycloak.jgroups.JGroupsStackConfigurator; -import org.keycloak.jgroups.JGroupsUtil; -import org.keycloak.models.KeycloakSession; - -/** - * JGroups discovery configuration using {@link JDBC_PING2}. - */ -public class JGroupsJdbcPingStackConfigurator implements JGroupsStackConfigurator { - - public static final JGroupsStackConfigurator INSTANCE = new JGroupsJdbcPingStackConfigurator(); - - private JGroupsJdbcPingStackConfigurator() {} - - static { - // Use custom Keycloak JDBC_PING implementation to use the connection from JpaConnectionProviderFactory - // The id 1025 follows this instruction: https://github.com/belaban/JGroups/blob/38219e9ec1c629fa2f7929e3b53d1417d8e60b61/conf/jg-protocol-ids.xml#L85 - ClassConfigurator.addProtocol((short) 1025, KEYCLOAK_JDBC_PING2.class); - } - - @Override - public void configure(ConfigurationBuilderHolder holder, KeycloakSession session) { - var em = session.getProvider(JpaConnectionProvider.class).getEntityManager(); - var stackName = JGroupsUtil.transportStackOf(holder).get(); - var isUdp = stackName.endsWith("udp"); - var tableName = JpaUtils.getTableNameForNativeQuery("JGROUPS_PING", em); - var stack = getProtocolConfigurations(tableName, isUdp ? "PING" : "MPING"); - var connectionFactory = (JpaConnectionProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(JpaConnectionProvider.class); - holder.addJGroupsStack(new JpaFactoryAwareJGroupsChannelConfigurator(stackName, stack,connectionFactory, isUdp), null); - - JGroupsUtil.transportOf(holder).stack(stackName); - JGroupsConfigurator.logger.info("JGroups JDBC_PING discovery enabled."); - } - - private static List getProtocolConfigurations(String tableName, String discoveryProtocol) { - var attributes = Map.of( - // Leave initialize_sql blank as table is already created by Keycloak - "initialize_sql", "", - // Explicitly specify clear and select_all SQL to ensure "cluster_name" column is used, as the default - // "cluster" cannot be used with Oracle DB as it's a reserved word. - "clear_sql", String.format("DELETE from %s WHERE cluster_name=?", tableName), - "delete_single_sql", String.format("DELETE from %s WHERE address=?", tableName), - "insert_single_sql", String.format("INSERT INTO %s values (?, ?, ?, ?, ?)", tableName), - "select_all_pingdata_sql", String.format("SELECT address, name, ip, coord FROM %s WHERE cluster_name=?", tableName), - "remove_all_data_on_view_change", "true", - "register_shutdown_hook", "false", - "stack.combine", "REPLACE", - "stack.position", discoveryProtocol - ); - return List.of(new ProtocolConfiguration(KEYCLOAK_JDBC_PING2.class.getName(), attributes)); - } - - private static class JpaFactoryAwareJGroupsChannelConfigurator extends EmbeddedJGroupsChannelConfigurator { - - private final JpaConnectionProviderFactory factory; - - public JpaFactoryAwareJGroupsChannelConfigurator(String name, List stack, JpaConnectionProviderFactory factory, boolean isUdp) { - super(name, stack, null, isUdp ? "udp" : "tcp"); - this.factory = factory; - } - - @Override - public void afterCreation(Protocol protocol) { - super.afterCreation(protocol); - if (protocol instanceof KEYCLOAK_JDBC_PING2 kcPing) { - kcPing.setJpaConnectionProviderFactory(factory); - } - } - } - - -} diff --git a/model/infinispan/src/main/java/org/keycloak/jgroups/impl/JpaJGroupsTlsConfigurator.java b/model/infinispan/src/main/java/org/keycloak/jgroups/impl/JpaJGroupsTlsConfigurator.java deleted file mode 100644 index 218029ee240..00000000000 --- a/model/infinispan/src/main/java/org/keycloak/jgroups/impl/JpaJGroupsTlsConfigurator.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2025 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.jgroups.impl; - -import java.security.KeyManagementException; -import java.security.NoSuchAlgorithmException; - -import org.infinispan.configuration.parsing.ConfigurationBuilderHolder; -import org.infinispan.remoting.transport.jgroups.JGroupsTransport; -import org.jgroups.util.DefaultSocketFactory; -import org.jgroups.util.SocketFactory; -import org.keycloak.infinispan.module.configuration.global.KeycloakConfigurationBuilder; -import org.keycloak.jgroups.JGroupsConfigurator; -import org.keycloak.jgroups.JGroupsStackConfigurator; -import org.keycloak.jgroups.JGroupsUtil; -import org.keycloak.models.KeycloakSession; -import org.keycloak.spi.infinispan.JGroupsCertificateProvider; -import org.keycloak.storage.configuration.ServerConfigStorageProvider; - -import javax.net.ssl.KeyManager; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLParameters; -import javax.net.ssl.SSLServerSocket; -import javax.net.ssl.TrustManager; - -/** - * JGroups mTLS configuration using certificates stored by {@link ServerConfigStorageProvider}. - */ -public class JpaJGroupsTlsConfigurator implements JGroupsStackConfigurator { - - private static final String TLS_PROTOCOL_VERSION = "TLSv1.3"; - private static final String TLS_PROTOCOL = "TLS"; - public static final JpaJGroupsTlsConfigurator INSTANCE = new JpaJGroupsTlsConfigurator(); - - - @Override - public void configure(ConfigurationBuilderHolder holder, KeycloakSession session) { - var kcConfig = holder.getGlobalConfigurationBuilder().addModule(KeycloakConfigurationBuilder.class); - kcConfig.setKeycloakSessionFactory(session.getKeycloakSessionFactory()); - var provider = session.getProvider(JGroupsCertificateProvider.class); - if (provider == null || !provider.isEnabled()) { - return; - } - var factory = createSocketFactory(provider); - JGroupsUtil.transportOf(holder).addProperty(JGroupsTransport.SOCKET_FACTORY, factory); - JGroupsUtil.validateTlsAvailable(holder); - JGroupsConfigurator.logger.info("JGroups Encryption enabled (mTLS)."); - } - - - private static SocketFactory createSocketFactory(JGroupsCertificateProvider provider) { - try { - var sslContext = SSLContext.getInstance(TLS_PROTOCOL); - sslContext.init(new KeyManager[]{provider.keyManager()}, new TrustManager[]{provider.trustManager()}, null); - return createFromContext(sslContext); - } catch (KeyManagementException | NoSuchAlgorithmException e) { - // we should have valid certificates and keys. - throw new RuntimeException(e); - } - } - - private static SocketFactory createFromContext(SSLContext context) { - DefaultSocketFactory socketFactory = new DefaultSocketFactory(context); - final SSLParameters serverParameters = new SSLParameters(); - serverParameters.setProtocols(new String[]{TLS_PROTOCOL_VERSION}); - serverParameters.setNeedClientAuth(true); - socketFactory.setServerSocketConfigurator(socket -> ((SSLServerSocket) socket).setSSLParameters(serverParameters)); - return socketFactory; - } -} diff --git a/model/infinispan/src/main/java/org/keycloak/jgroups/impl/KEYCLOAK_JDBC_PING2.java b/model/infinispan/src/main/java/org/keycloak/jgroups/protocol/KEYCLOAK_JDBC_PING2.java similarity index 73% rename from model/infinispan/src/main/java/org/keycloak/jgroups/impl/KEYCLOAK_JDBC_PING2.java rename to model/infinispan/src/main/java/org/keycloak/jgroups/protocol/KEYCLOAK_JDBC_PING2.java index 4fe70eb1f9c..202a3b22913 100644 --- a/model/infinispan/src/main/java/org/keycloak/jgroups/impl/KEYCLOAK_JDBC_PING2.java +++ b/model/infinispan/src/main/java/org/keycloak/jgroups/protocol/KEYCLOAK_JDBC_PING2.java @@ -1,4 +1,21 @@ -package org.keycloak.jgroups.impl; +/* + * Copyright 2025 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.jgroups.protocol; import org.jgroups.protocols.JDBC_PING2; import org.jgroups.protocols.PingData; @@ -43,7 +60,7 @@ public class KEYCLOAK_JDBC_PING2 extends JDBC_PING2 { throw e; } finally { if (isAutocommit) { - connection.setAutoCommit(isAutocommit); + connection.setAutoCommit(true); } } } diff --git a/model/infinispan/src/main/java/org/keycloak/jgroups/JGroupsStackConfigurator.java b/model/infinispan/src/main/java/org/keycloak/spi/infinispan/CacheEmbeddedConfigProvider.java similarity index 54% rename from model/infinispan/src/main/java/org/keycloak/jgroups/JGroupsStackConfigurator.java rename to model/infinispan/src/main/java/org/keycloak/spi/infinispan/CacheEmbeddedConfigProvider.java index 48f438c3905..0198fb0f0f5 100644 --- a/model/infinispan/src/main/java/org/keycloak/jgroups/JGroupsStackConfigurator.java +++ b/model/infinispan/src/main/java/org/keycloak/spi/infinispan/CacheEmbeddedConfigProvider.java @@ -15,22 +15,27 @@ * limitations under the License. */ -package org.keycloak.jgroups; +package org.keycloak.spi.infinispan; import org.infinispan.configuration.parsing.ConfigurationBuilderHolder; -import org.keycloak.models.KeycloakSession; +import org.infinispan.manager.EmbeddedCacheManager; +import org.keycloak.provider.Provider; /** - * Interface to configure a JGroups Stack before Keycloak starts the embedded Infinispan. + * A provider to create the {@link ConfigurationBuilderHolder} to configure the {@link EmbeddedCacheManager}. */ -public interface JGroupsStackConfigurator { +public interface CacheEmbeddedConfigProvider extends Provider { /** - * Configures the stack in {@code holder}. + * The {@link ConfigurationBuilderHolder} whit the {@link EmbeddedCacheManager} configuration. It must not be + * {@code null}. * - * @param holder The Infinispan {@link ConfigurationBuilderHolder}. - * @param session The current {@link KeycloakSession}. It may be {@code null}; + * @return The {@link ConfigurationBuilderHolder} whit the {@link EmbeddedCacheManager} configuration. */ - void configure(ConfigurationBuilderHolder holder, KeycloakSession session); + ConfigurationBuilderHolder configuration(); + @Override + default void close() { + //no-op + } } diff --git a/model/infinispan/src/main/java/org/keycloak/spi/infinispan/CacheEmbeddedConfigProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/spi/infinispan/CacheEmbeddedConfigProviderFactory.java new file mode 100644 index 00000000000..e2110a3bc6f --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/spi/infinispan/CacheEmbeddedConfigProviderFactory.java @@ -0,0 +1,26 @@ +/* + * Copyright 2025 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.spi.infinispan; + +import org.keycloak.provider.ProviderFactory; + +/** + * The {@link ProviderFactory} to build {@link CacheEmbeddedConfigProvider}. + */ +public interface CacheEmbeddedConfigProviderFactory extends ProviderFactory { +} diff --git a/model/infinispan/src/main/java/org/keycloak/spi/infinispan/CacheEmbeddedConfigProviderSpi.java b/model/infinispan/src/main/java/org/keycloak/spi/infinispan/CacheEmbeddedConfigProviderSpi.java new file mode 100644 index 00000000000..597ad03e8eb --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/spi/infinispan/CacheEmbeddedConfigProviderSpi.java @@ -0,0 +1,53 @@ +/* + * Copyright 2025 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.spi.infinispan; + +import org.infinispan.configuration.parsing.ConfigurationBuilderHolder; +import org.infinispan.manager.EmbeddedCacheManager; +import org.keycloak.provider.Spi; + +/** + * The {@link Spi} implementation for the {@link CacheEmbeddedConfigProviderFactory} and + * {@link CacheEmbeddedConfigProvider}. + *

+ * It provides the {@link ConfigurationBuilderHolder} to configure the {@link EmbeddedCacheManager}. + */ +public class CacheEmbeddedConfigProviderSpi implements Spi { + + public static final String SPI_NAME = "cacheEmbedded"; + + @Override + public boolean isInternal() { + return true; + } + + @Override + public String getName() { + return SPI_NAME; + } + + @Override + public Class getProviderClass() { + return CacheEmbeddedConfigProvider.class; + } + + @Override + public Class getProviderFactoryClass() { + return CacheEmbeddedConfigProviderFactory.class; + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/embedded/CacheConfigurator.java b/model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/embedded/CacheConfigurator.java new file mode 100644 index 00000000000..f84fd30fc3e --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/embedded/CacheConfigurator.java @@ -0,0 +1,303 @@ +/* + * Copyright 2025 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.spi.infinispan.impl.embedded; + +import java.lang.invoke.MethodHandles; +import java.util.Arrays; +import java.util.Map; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import org.infinispan.commons.dataconversion.MediaType; +import org.infinispan.configuration.cache.ConfigurationBuilder; +import org.infinispan.configuration.cache.HashConfiguration; +import org.infinispan.configuration.parsing.ConfigurationBuilderHolder; +import org.infinispan.eviction.EvictionStrategy; +import org.infinispan.transaction.LockingMode; +import org.infinispan.transaction.TransactionMode; +import org.infinispan.transaction.lookup.EmbeddedTransactionManagerLookup; +import org.jboss.logging.Logger; +import org.keycloak.Config; +import org.keycloak.connections.infinispan.InfinispanConnectionProvider; + +import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.AUTHORIZATION_CACHE_NAME; +import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.AUTHORIZATION_REVISIONS_CACHE_DEFAULT_MAX; +import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.AUTHORIZATION_REVISIONS_CACHE_NAME; +import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME; +import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.CRL_CACHE_NAME; +import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.LOCAL_CACHE_NAMES; +import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.LOCAL_MAX_COUNT_CACHES; +import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME; +import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME; +import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.REALM_CACHE_NAME; +import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.REALM_REVISIONS_CACHE_DEFAULT_MAX; +import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.REALM_REVISIONS_CACHE_NAME; +import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.USER_CACHE_NAME; +import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.USER_REVISIONS_CACHE_DEFAULT_MAX; +import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.USER_REVISIONS_CACHE_NAME; +import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.USER_SESSION_CACHE_NAME; +import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.WORK_CACHE_NAME; + +/** + * Utility class related to the Infinispan cache configuration. + *

+ * This class contains methods to configure caches based on the SPI configuration options, and it provides cache + * configuration defaults. + */ +public final class CacheConfigurator { + + private static final Logger logger = Logger.getLogger(MethodHandles.lookup().lookupClass()); + + // Map with the default cache configuration if the cache is not present in the XML. + private static final Map> DEFAULT_CONFIGS = Map.of(CRL_CACHE_NAME, CacheConfigurator::getCrlCacheConfig); + private static final Supplier TO_NULL = () -> null; + private static final String MAX_COUNT_SUFFIX = "MaxCount"; + + private CacheConfigurator() { + } + + /** + * Configures the Infinispan local caches used by Keycloak (e.g., for realm or user data) using the provided + * Keycloak configuration. + * + * @param keycloakConfig The Keycloak configuration. + * @param holder The {@link ConfigurationBuilderHolder} where the caches will be defined. + * @throws IllegalStateException if an Infinispan cache is not defined. This could indicate a missing or incorrect + * configuration. + */ + public static void configureLocalCaches(Config.Scope keycloakConfig, ConfigurationBuilderHolder holder) { + logger.debug("Configuring embedded local caches"); + // configure local caches except revision caches + configureCacheMaxCount(keycloakConfig, holder, Arrays.stream(LOCAL_MAX_COUNT_CACHES)); + // configure revision caches + configureRevisionCache(holder, REALM_CACHE_NAME, REALM_REVISIONS_CACHE_NAME, REALM_REVISIONS_CACHE_DEFAULT_MAX); + configureRevisionCache(holder, USER_CACHE_NAME, USER_REVISIONS_CACHE_NAME, USER_REVISIONS_CACHE_DEFAULT_MAX); + configureRevisionCache(holder, AUTHORIZATION_CACHE_NAME, AUTHORIZATION_REVISIONS_CACHE_NAME, AUTHORIZATION_REVISIONS_CACHE_DEFAULT_MAX); + // check all caches are defined + checkCachesExist(holder, Arrays.stream(LOCAL_CACHE_NAMES)); + } + + /** + * Applies the default Infinispan cache configuration to the {@code holder}, if the cache is not present. + *

+ * Each cache may have its own default configuration. + * + * @param holder The {@link ConfigurationBuilderHolder} where the caches will be defined. + */ + public static void applyDefaultConfiguration(ConfigurationBuilderHolder holder) { + var configs = holder.getNamedConfigurationBuilders(); + for (var name : InfinispanConnectionProvider.ALL_CACHES_NAME) { + configs.computeIfAbsent(name, cacheName -> DEFAULT_CONFIGS.getOrDefault(cacheName, TO_NULL).get()); + } + } + + /** + * Verifies that all the {@code caches} are defined in the {@code holder}. + * + * @param holder The {@link ConfigurationBuilderHolder} where the caches are configured. + * @param caches The {@link Stream} containing the names of the caches to check. + * @throws IllegalStateException if one or more Infinispan caches from the provided {@code caches} stream are not + * defined in the {@code holder}. This could indicate a missing or incorrect + * configuration for those specific caches. + */ + public static void checkCachesExist(ConfigurationBuilderHolder holder, Stream caches) { + for (var it = caches.iterator(); it.hasNext(); ) { + var cache = it.next(); + var builder = holder.getNamedConfigurationBuilders().get(cache); + if (builder == null) { + throw cacheNotFound(cache); + } + } + } + + /** + * Validates that the "work" cache is present in the {@code holder} and has a valid configuration. + * + * @param holder The {@link ConfigurationBuilderHolder} where the caches are configured. + * @throws IllegalStateException if the "work" cache is not found in the holder. + * @throws RuntimeException if the "work" cache has an invalid configuration. This could include an incorrect + * settings that would prevent the cache from functioning correctly. + */ + public static void validateWorkCacheConfiguration(ConfigurationBuilderHolder holder) { + logger.debugf("Validating %s cache configuration", WORK_CACHE_NAME); + var cacheBuilder = holder.getNamedConfigurationBuilders().get(WORK_CACHE_NAME); + if (cacheBuilder == null) { + throw cacheNotFound(WORK_CACHE_NAME); + } + if (holder.getGlobalConfigurationBuilder().cacheContainer().transport().getTransport() == null) { + // non-clustered, Keycloak started in dev mode? + return; + } + var cacheMode = cacheBuilder.clustering().cacheMode(); + if (!cacheMode.isReplicated()) { + throw new RuntimeException("Unable to start Keycloak. '%s' cache must be replicated but is %s".formatted(WORK_CACHE_NAME, cacheMode.friendlyCacheModeString().toLowerCase())); + } + } + + /** + * Removes clustered caches from the {@code holder}. + * + * @param holder The {@link ConfigurationBuilderHolder} where the caches are configured. + */ + public static void removeClusteredCaches(ConfigurationBuilderHolder holder) { + logger.debug("Removing clustered caches"); + Arrays.stream(InfinispanConnectionProvider.CLUSTERED_CACHE_NAMES).forEach(holder.getNamedConfigurationBuilders()::remove); + } + + /** + * Configures the maximum number of entries for the specified caches, bounding them to this limit and preventing + * excessive memory usage. + * + * @param keycloakConfig The Keycloak configuration, which provides the maximum entry counts for the caches. + * @param holder The {@link ConfigurationBuilderHolder} where the caches are configured. + * @param caches The {@link Stream} containing the names of the caches to configure with a maximum count. + * @throws IllegalStateException if an Infinispan cache from the provided {@code caches} stream is not defined in + * the {@code holder}. This could indicate a missing or incorrect configuration. + */ + public static void configureCacheMaxCount(Config.Scope keycloakConfig, ConfigurationBuilderHolder holder, Stream caches) { + for (var it = caches.iterator(); it.hasNext(); ) { + var name = it.next(); + var builder = holder.getNamedConfigurationBuilders().get(name); + if (builder == null) { + throw cacheNotFound(name); + } + setMemoryMaxCount(keycloakConfig, name, builder); + } + } + + /** + * Configures all the sessions caches when persistent user sessions feature is enabled. + * + * @param holder The {@link ConfigurationBuilderHolder} where the caches are configured. + * @throws IllegalStateException if an Infinispan cache from the provided {@code caches} stream is not defined in + * the {@code holder}. This could indicate a missing or incorrect configuration. + */ + public static void configureSessionsCachesForPersistentSessions(ConfigurationBuilderHolder holder) { + logger.debug("Configuring session cache (persistent user sessions)"); + for (var name : Arrays.asList(USER_SESSION_CACHE_NAME, CLIENT_SESSION_CACHE_NAME, OFFLINE_USER_SESSION_CACHE_NAME, OFFLINE_CLIENT_SESSION_CACHE_NAME)) { + var builder = holder.getNamedConfigurationBuilders().get(name); + if (builder == null) { + throw cacheNotFound(name); + } + if (builder.memory().maxCount() == -1) { + logger.infof("Persistent user sessions enabled and no memory limit found in configuration. Setting max entries for %s to 10000 entries.", name); + builder.memory().maxCount(10000); + } + /* The number of owners for these caches then need to be set to `1` to avoid backup owners with inconsistent data. + As primary owner evicts a key based on its locally evaluated maxCount setting, it wouldn't tell the backup owner about this, and then the backup owner would be left with a soon-to-be-outdated key. + While a `remove` is forwarded to the backup owner regardless if the key exists on the primary owner, a `computeIfPresent` is not, and it would leave a backup owner with an outdated key. + With the number of owners set to `1`, there will be no backup owners, so this is the setting to choose with persistent sessions enabled to ensure consistent data in the caches. */ + builder.clustering().hash().numOwners(1); + } + } + + /** + * Configures all the sessions caches when persistent user sessions feature is enabled. + * + * @param holder The {@link ConfigurationBuilderHolder} where the caches are configured. + * @throws IllegalStateException if an Infinispan cache from the provided {@code caches} stream is not defined in + * the {@code holder}. This could indicate a missing or incorrect configuration. + */ + public static void configureSessionsCachesForVolatileSessions(ConfigurationBuilderHolder holder) { + logger.debug("Configuring session cache (volatile user sessions)"); + for (var name : Arrays.asList(USER_SESSION_CACHE_NAME, CLIENT_SESSION_CACHE_NAME, OFFLINE_USER_SESSION_CACHE_NAME, OFFLINE_CLIENT_SESSION_CACHE_NAME)) { + var builder = holder.getNamedConfigurationBuilders().get(name); + if (builder == null) { + throw cacheNotFound(name); + } + if (builder.memory().maxCount() != -1) { + logger.warnf("Persistent user sessions disabled and memory limit found in configuration for cache %s. This might be a misconfiguration! Update your Infinispan configuration to remove this message.", name); + } + if (builder.memory().maxCount() == 10000 && (name.equals(USER_SESSION_CACHE_NAME) || name.equals(CLIENT_SESSION_CACHE_NAME))) { + logger.warnf("Persistent user sessions disabled and memory limit is set to default value 10000. Ignoring cache limits to avoid losing sessions for cache %s.", name); + builder.memory().maxCount(-1); + } + if (builder.clustering().hash().attributes().attribute(HashConfiguration.NUM_OWNERS).get() == 1 && builder.persistence().stores().isEmpty()) { + logger.warnf("Number of owners is one for cache %s, and no persistence is configured. This might be a misconfiguration as you will lose data when a single node is restarted!", name); + } + } + } + + // private methods below + + private static void configureRevisionCache(ConfigurationBuilderHolder holder, String baseCache, String revisionCache, long defaultMaxEntries) { + var baseBuilder = holder.getNamedConfigurationBuilders().get(baseCache); + if (baseBuilder == null) { + throw cacheNotFound(baseCache); + } + var maxCount = baseBuilder.memory().maxCount(); + maxCount = maxCount > 0 ? 2 * maxCount : defaultMaxEntries; + logger.debugf("Creating revision cache '%s' with max-count %s", revisionCache, maxCount); + holder.getNamedConfigurationBuilders().put(revisionCache, getRevisionCacheConfig(maxCount)); + } + + private static void setMemoryMaxCount(Config.Scope keycloakConfig, String name, ConfigurationBuilder builder) { + var maxCount = keycloakConfig.getInt(maxCountConfigKey(name)); + if (maxCount != null) { + logger.debugf("Overwriting max-count for cache '%s' to %s entries", name, maxCount); + builder.memory().maxCount(maxCount); + } + } + + public static String maxCountConfigKey(String name) { + return name + MAX_COUNT_SUFFIX; + } + + private static IllegalStateException cacheNotFound(String cache) { + return new IllegalStateException("Infinispan cache '%s' not found.".formatted(cache)); + } + + // cache configuration below + + public static ConfigurationBuilder getCrlCacheConfig() { + var builder = createCacheConfigurationBuilder(); + builder.memory().whenFull(EvictionStrategy.REMOVE).maxCount(InfinispanConnectionProvider.CRL_CACHE_DEFAULT_MAX); + return builder; + } + + public static ConfigurationBuilder getRevisionCacheConfig(long maxEntries) { + var builder = createCacheConfigurationBuilder(); + builder.simpleCache(false); + builder.invocationBatching().enable().transaction().transactionMode(TransactionMode.TRANSACTIONAL); + + // Use Embedded manager even in managed ( wildfly/eap ) environment. We don't want infinispan to participate in global transaction + builder.transaction().transactionManagerLookup(new EmbeddedTransactionManagerLookup()); + + builder.transaction().lockingMode(LockingMode.PESSIMISTIC); + if (builder.memory().storage().canStoreReferences()) { + builder.encoding().mediaType(MediaType.APPLICATION_OBJECT_TYPE); + } + + builder.memory().whenFull(EvictionStrategy.REMOVE).maxCount(maxEntries); + + return builder; + } + + public static ConfigurationBuilder createCacheConfigurationBuilder() { + ConfigurationBuilder builder = new ConfigurationBuilder(); + + // need to force the encoding to application/x-java-object to avoid unnecessary conversion of keys/values. See WFLY-14356. + builder.encoding().mediaType(MediaType.APPLICATION_OBJECT_TYPE); + + // needs to be disabled if transaction is enabled + builder.simpleCache(true); + + return builder; + } + +} diff --git a/model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/embedded/DefaultCacheEmbeddedConfigProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/embedded/DefaultCacheEmbeddedConfigProviderFactory.java new file mode 100644 index 00000000000..c3609235555 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/embedded/DefaultCacheEmbeddedConfigProviderFactory.java @@ -0,0 +1,250 @@ +/* + * Copyright 2025 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.spi.infinispan.impl.embedded; + +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +import io.micrometer.core.instrument.Metrics; +import org.infinispan.configuration.cache.ConfigurationBuilder; +import org.infinispan.configuration.cache.StatisticsConfigurationBuilder; +import org.infinispan.configuration.global.ShutdownHookBehavior; +import org.infinispan.configuration.parsing.ConfigurationBuilderHolder; +import org.infinispan.configuration.parsing.ParserRegistry; +import org.infinispan.metrics.config.MicrometerMeterRegisterConfigurationBuilder; +import org.jboss.logging.Logger; +import org.keycloak.Config; +import org.keycloak.common.Profile; +import org.keycloak.config.CachingOptions; +import org.keycloak.config.MetricsOptions; +import org.keycloak.infinispan.module.configuration.global.KeycloakConfigurationBuilder; +import org.keycloak.infinispan.util.InfinispanUtils; +import org.keycloak.marshalling.Marshalling; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.provider.ProviderConfigurationBuilder; +import org.keycloak.spi.infinispan.CacheEmbeddedConfigProvider; +import org.keycloak.spi.infinispan.CacheEmbeddedConfigProviderFactory; +import org.keycloak.spi.infinispan.JGroupsCertificateProvider; +import org.keycloak.spi.infinispan.impl.Util; + +import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.ALL_CACHES_NAME; +import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.CLUSTERED_MAX_COUNT_CACHES; +import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.LOCAL_CACHE_NAMES; + +/** + * The default implementation of {@link CacheEmbeddedConfigProviderFactory}. + *

+ * It builds a {@link ConfigurationBuilderHolder} based on the Keycloak configuration. + *

+ * Advanced users may extend this class and overwrite the method {@link #createConfiguration(KeycloakSessionFactory)}. + * They have access to the {@link ConfigurationBuilderHolder}, and they can modify it as needed for their custom + * providers. + */ +public class DefaultCacheEmbeddedConfigProviderFactory implements CacheEmbeddedConfigProviderFactory, CacheEmbeddedConfigProvider { + + private static final Logger logger = Logger.getLogger(MethodHandles.lookup().lookupClass()); + + public static final String PROVIDER_ID = "default"; + + // Configuration + public static final String CONFIG = "configFile"; + private static final String METRICS = "metricsEnabled"; + private static final String HISTOGRAMS = "metricsHistogramsEnabled"; + public static final String STACK = "stack"; + public static final String NODE_NAME = "nodeName"; + public static final String SITE_NAME = "siteName"; + + private volatile ConfigurationBuilderHolder builderHolder; + private volatile Config.Scope keycloakConfig; + + @Override + public CacheEmbeddedConfigProvider create(KeycloakSession session) { + lazyInit(session.getKeycloakSessionFactory()); + return this; + } + + @Override + public void init(Config.Scope config) { + this.keycloakConfig = config; + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + lazyInit(factory); + } + + @Override + public ConfigurationBuilderHolder configuration() { + return builderHolder; + } + + @Override + public void close() { + //no-op + } + + @Override + public String getId() { + return PROVIDER_ID; + } + + @Override + public List getConfigMetadata() { + var builder = ProviderConfigurationBuilder.create(); + Util.copyFromOption(builder, CONFIG, "file", ProviderConfigProperty.STRING_TYPE, CachingOptions.CACHE_CONFIG_FILE, false); + Util.copyFromOption(builder, HISTOGRAMS, "enabled", ProviderConfigProperty.BOOLEAN_TYPE, CachingOptions.CACHE_METRICS_HISTOGRAMS_ENABLED, false); + Util.copyFromOption(builder, METRICS, "enabled", ProviderConfigProperty.BOOLEAN_TYPE, MetricsOptions.INFINISPAN_METRICS_ENABLED, false); + Stream.concat(Arrays.stream(LOCAL_CACHE_NAMES), Arrays.stream(CLUSTERED_MAX_COUNT_CACHES)) + .forEach(name -> Util.copyFromOption(builder, CacheConfigurator.maxCountConfigKey(name), "max-count", ProviderConfigProperty.INTEGER_TYPE, CachingOptions.maxCountOption(name), false)); + createTopologyProperties(builder); + return builder.build(); + } + + @Override + public Set> dependsOn() { + return Set.of(JGroupsCertificateProvider.class); + } + + private void lazyInit(KeycloakSessionFactory factory) { + if (builderHolder != null) { + return; + } + synchronized (this) { + if (builderHolder != null) { + return; + } + try { + builderHolder = createConfiguration(factory); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + protected ConfigurationBuilderHolder createConfiguration(KeycloakSessionFactory factory) throws IOException { + var holder = parseConfiguration(keycloakConfig, factory); + if (InfinispanUtils.isRemoteInfinispan()) { + return configureMultiSite(holder, keycloakConfig); + } + if (Profile.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS)) { + return configureSingleSiteWithPersistentSessions(holder, keycloakConfig, factory); + } + return configureSingleSiteWithVolatileSessions(holder, keycloakConfig, factory); + } + + private static ConfigurationBuilderHolder configureSingleSiteWithVolatileSessions(ConfigurationBuilderHolder holder, Config.Scope keycloakConfig, KeycloakSessionFactory factory) { + singleSiteConfiguration(keycloakConfig, holder, factory); + CacheConfigurator.configureSessionsCachesForVolatileSessions(holder); + return holder; + } + + private static ConfigurationBuilderHolder configureSingleSiteWithPersistentSessions(ConfigurationBuilderHolder holder, Config.Scope keycloakConfig, KeycloakSessionFactory factory) { + singleSiteConfiguration(keycloakConfig, holder, factory); + CacheConfigurator.configureSessionsCachesForPersistentSessions(holder); + return holder; + } + + private static ConfigurationBuilderHolder configureMultiSite(ConfigurationBuilderHolder holder, Config.Scope keycloakConfig) { + logger.debug("Configuring Infinispan for multi-site deployment"); + CacheConfigurator.removeClusteredCaches(holder); + CacheConfigurator.checkCachesExist(holder, Arrays.stream(LOCAL_CACHE_NAMES)); + configureMetrics(keycloakConfig, holder); + // Disable JGroups, not required when the data is stored in the Remote Cache. + // The existing caches are local and do not require JGroups to work properly. + holder.getGlobalConfigurationBuilder().nonClusteredDefault(); + return holder; + } + + private static ConfigurationBuilderHolder parseConfiguration(Config.Scope keycloakConfig, KeycloakSessionFactory factory) throws IOException { + var configFile = keycloakConfig.get(CONFIG); + if (configFile == null) { + throw new IllegalArgumentException("Option 'configFile' needs to be specified"); + } + var configPath = Paths.get(configFile); + var path = configPath.toFile().exists() ? + configPath.toFile().getAbsolutePath() : + configPath.getFileName().toString(); + + logger.debugf("Parsing Infinispan configuration from file: %s", path); + var holder = new ParserRegistry(DefaultCacheEmbeddedConfigProviderFactory.class.getClassLoader()) + .parseFile(path); + // We must disable the Infinispan default ShutdownHook as we manage the EmbeddedCacheManager lifecycle explicitly + // with #shutdown and multiple calls to EmbeddedCacheManager#stop can lead to Exceptions being thrown. + holder.getGlobalConfigurationBuilder().shutdown().hookBehavior(ShutdownHookBehavior.DONT_REGISTER); + Marshalling.configure(holder.getGlobalConfigurationBuilder()); + holder.getGlobalConfigurationBuilder() + .addModule(KeycloakConfigurationBuilder.class) + .setKeycloakSessionFactory(factory); + CacheConfigurator.applyDefaultConfiguration(holder); + CacheConfigurator.configureLocalCaches(keycloakConfig, holder); + JGroupsConfigurator.configureTopology(keycloakConfig, holder); + return holder; + } + + private static void singleSiteConfiguration(Config.Scope config, ConfigurationBuilderHolder holder, KeycloakSessionFactory factory) { + logger.debug("Configuring Infinispan for single-site deployment"); + CacheConfigurator.checkCachesExist(holder, Arrays.stream(ALL_CACHES_NAME)); + CacheConfigurator.configureCacheMaxCount(config, holder, Arrays.stream(CLUSTERED_MAX_COUNT_CACHES)); + CacheConfigurator.validateWorkCacheConfiguration(holder); + KeycloakModelUtils.runJobInTransaction(factory, session -> JGroupsConfigurator.configureJGroups(config, holder, session)); + configureMetrics(config, holder); + } + + private static void configureMetrics(Config.Scope keycloakConfig, ConfigurationBuilderHolder holder) { + //metrics are disabled by default (check MetricsOptions class) + if (keycloakConfig.getBoolean(METRICS, Boolean.FALSE)) { + logger.debug("Enabling Infinispan metrics"); + var builder = holder.getGlobalConfigurationBuilder(); + builder.addModule(MicrometerMeterRegisterConfigurationBuilder.class) + .meterRegistry(Metrics.globalRegistry); + builder.cacheContainer().statistics(true); + builder.metrics() + .namesAsTags(true) + .histograms(keycloakConfig.getBoolean(HISTOGRAMS, Boolean.FALSE)); + holder.getNamedConfigurationBuilders() + .values() + .stream() + .map(ConfigurationBuilder::statistics) + .forEach(StatisticsConfigurationBuilder::enable); + } + } + + private static void createTopologyProperties(ProviderConfigurationBuilder builder) { + builder.property() + .name(NODE_NAME) + .helpText("Sets the name of the current node. This is a friendly name to make logs, etc. make more sense.") + .label("name") + .type(ProviderConfigProperty.STRING_TYPE) + .add(); + builder.property() + .name(SITE_NAME) + .helpText("The name of the site where this node runs. Used for server hinting.") + .label("name") + .type(ProviderConfigProperty.STRING_TYPE) + .add(); + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/embedded/JGroupsConfigurator.java b/model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/embedded/JGroupsConfigurator.java new file mode 100644 index 00000000000..e9a302fc2d1 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/embedded/JGroupsConfigurator.java @@ -0,0 +1,262 @@ +/* + * Copyright 2025 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.spi.infinispan.impl.embedded; + +import java.lang.invoke.MethodHandles; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import org.infinispan.commons.configuration.attributes.Attribute; +import org.infinispan.configuration.global.TransportConfigurationBuilder; +import org.infinispan.configuration.parsing.ConfigurationBuilderHolder; +import org.infinispan.remoting.transport.jgroups.EmbeddedJGroupsChannelConfigurator; +import org.infinispan.remoting.transport.jgroups.JGroupsTransport; +import org.jboss.logging.Logger; +import org.jgroups.conf.ClassConfigurator; +import org.jgroups.conf.ProtocolConfiguration; +import org.jgroups.protocols.TCP_NIO2; +import org.jgroups.protocols.UDP; +import org.jgroups.stack.Protocol; +import org.jgroups.util.DefaultSocketFactory; +import org.jgroups.util.SocketFactory; +import org.keycloak.Config; +import org.keycloak.connections.infinispan.InfinispanConnectionProvider; +import org.keycloak.connections.infinispan.InfinispanConnectionSpi; +import org.keycloak.connections.jpa.JpaConnectionProvider; +import org.keycloak.connections.jpa.JpaConnectionProviderFactory; +import org.keycloak.connections.jpa.util.JpaUtils; +import org.keycloak.jgroups.protocol.KEYCLOAK_JDBC_PING2; +import org.keycloak.models.KeycloakSession; +import org.keycloak.spi.infinispan.JGroupsCertificateProvider; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLServerSocket; +import javax.net.ssl.TrustManager; + +import static org.infinispan.configuration.global.TransportConfiguration.STACK; + +/** + * Utility class to configure JGroups based on the Keycloak configuration. + */ +public final class JGroupsConfigurator { + + private static final Logger logger = Logger.getLogger(MethodHandles.lookup().lookupClass()); + private static final String TLS_PROTOCOL_VERSION = "TLSv1.3"; + private static final String TLS_PROTOCOL = "TLS"; + + private JGroupsConfigurator() { + } + + static { + // Use custom Keycloak JDBC_PING implementation that workarounds issue https://issues.redhat.com/browse/JGRP-2870 + // The id 1025 follows this instruction: https://github.com/belaban/JGroups/blob/38219e9ec1c629fa2f7929e3b53d1417d8e60b61/conf/jg-protocol-ids.xml#L85 + ClassConfigurator.addProtocol((short) 1025, KEYCLOAK_JDBC_PING2.class); + } + + /** + * Configures JGroups based on the Keycloak configuration. + * + * @param config The Keycloak configuration. + * @param holder The {@link ConfigurationBuilderHolder} where the transport is configured. + * @param session The {@link KeycloakSession} sessions for Database access. + */ + public static void configureJGroups(Config.Scope config, ConfigurationBuilderHolder holder, KeycloakSession session) { + var stack = config.get(DefaultCacheEmbeddedConfigProviderFactory.STACK); + if (stack != null) { + transportOf(holder).stack(stack); + } + configureDiscovery(holder, session); + configureTls(holder, session); + warnDeprecatedStack(holder); + } + + /** + * Configures the topology information in the Infinispan transport. + * + * @param config The Keycloak configuration. + * @param holder The {@link ConfigurationBuilderHolder} where the transport is configured. + */ + public static void configureTopology(Config.Scope config, ConfigurationBuilderHolder holder) { + if (System.getProperty(InfinispanConnectionProvider.JBOSS_SITE_NAME) != null) { + throw new IllegalArgumentException( + String.format("System property %s is in use. Use --spi-cache-embedded-%s-site-name config option instead", + InfinispanConnectionProvider.JBOSS_SITE_NAME, DefaultCacheEmbeddedConfigProviderFactory.PROVIDER_ID)); + } + if (System.getProperty(InfinispanConnectionProvider.JBOSS_NODE_NAME) != null) { + throw new IllegalArgumentException( + String.format("System property %s is in use. Use --spi-cache-embedded-%s-node-name config option instead", + InfinispanConnectionProvider.JBOSS_NODE_NAME, DefaultCacheEmbeddedConfigProviderFactory.PROVIDER_ID)); + } + var transport = transportOf(holder); + var nodeName = config.get(DefaultCacheEmbeddedConfigProviderFactory.NODE_NAME); + if (nodeName != null) { + transport.nodeName(nodeName); + } + //legacy option, for backwards compatibility --spi-connections-infinispan-quarkus-site-name + var legacySiteName = Config.scope(InfinispanConnectionSpi.SPI_NAME, "quarkus").get("site-name"); + if (legacySiteName != null) { + logger.warn("--spi-connections-infinispan-quarkus-site-name is deprecated and may be removed in the future. Use --spi-cache-embedded-%s-site-name".formatted(DefaultCacheEmbeddedConfigProviderFactory.PROVIDER_ID)); + } + var siteName = config.get(DefaultCacheEmbeddedConfigProviderFactory.SITE_NAME, legacySiteName); + if (siteName != null) { + transport.siteId(siteName); + } + } + + private static void configureTls(ConfigurationBuilderHolder holder, KeycloakSession session) { + var provider = session.getProvider(JGroupsCertificateProvider.class); + if (provider == null || !provider.isEnabled()) { + return; + } + var factory = createSocketFactory(provider); + transportOf(holder).addProperty(JGroupsTransport.SOCKET_FACTORY, factory); + validateTlsAvailable(holder); + logger.info("JGroups Encryption enabled (mTLS)."); + } + + private static SocketFactory createSocketFactory(JGroupsCertificateProvider provider) { + try { + var sslContext = SSLContext.getInstance(TLS_PROTOCOL); + sslContext.init(new KeyManager[]{provider.keyManager()}, new TrustManager[]{provider.trustManager()}, null); + return createFromContext(sslContext); + } catch (KeyManagementException | NoSuchAlgorithmException e) { + // we should have valid certificates and keys. + throw new RuntimeException(e); + } + } + + private static SocketFactory createFromContext(SSLContext context) { + DefaultSocketFactory socketFactory = new DefaultSocketFactory(context); + final SSLParameters serverParameters = new SSLParameters(); + serverParameters.setProtocols(new String[]{TLS_PROTOCOL_VERSION}); + serverParameters.setNeedClientAuth(true); + socketFactory.setServerSocketConfigurator(socket -> ((SSLServerSocket) socket).setSSLParameters(serverParameters)); + return socketFactory; + } + + private static void configureDiscovery(ConfigurationBuilderHolder holder, KeycloakSession session) { + var stackXmlAttribute = transportStackOf(holder); + if (stackXmlAttribute.isModified() && !isJdbcPingStack(stackXmlAttribute.get())) { + logger.debugf("Custom stack configured (%s). JDBC_PING discovery disabled.", stackXmlAttribute.get()); + return; + } + + logger.debug("JDBC_PING discovery enabled."); + if (!stackXmlAttribute.isModified()) { + // defaults to jdbc-ping + transportOf(holder).stack("jdbc-ping"); + } + + var em = session.getProvider(JpaConnectionProvider.class).getEntityManager(); + var stackName = transportStackOf(holder).get(); + var isUdp = stackName.endsWith("udp"); + var tableName = JpaUtils.getTableNameForNativeQuery("JGROUPS_PING", em); + var stack = getProtocolConfigurations(tableName, isUdp ? "PING" : "MPING"); + var connectionFactory = (JpaConnectionProviderFactory) session.getKeycloakSessionFactory().getProviderFactory(JpaConnectionProvider.class); + holder.addJGroupsStack(new JpaFactoryAwareJGroupsChannelConfigurator(stackName, stack, connectionFactory, isUdp), null); + + transportOf(holder).stack(stackName); + JGroupsConfigurator.logger.info("JGroups JDBC_PING discovery enabled."); + } + + private static List getProtocolConfigurations(String tableName, String discoveryProtocol) { + var attributes = Map.of( + // Leave initialize_sql blank as table is already created by Keycloak + "initialize_sql", "", + // Explicitly specify clear and select_all SQL to ensure "cluster_name" column is used, as the default + // "cluster" cannot be used with Oracle DB as it's a reserved word. + "clear_sql", String.format("DELETE from %s WHERE cluster_name=?", tableName), + "delete_single_sql", String.format("DELETE from %s WHERE address=?", tableName), + "insert_single_sql", String.format("INSERT INTO %s values (?, ?, ?, ?, ?)", tableName), + "select_all_pingdata_sql", String.format("SELECT address, name, ip, coord FROM %s WHERE cluster_name=?", tableName), + "remove_all_data_on_view_change", "true", + "register_shutdown_hook", "false", + "stack.combine", "REPLACE", + "stack.position", discoveryProtocol + ); + return List.of(new ProtocolConfiguration(KEYCLOAK_JDBC_PING2.class.getName(), attributes)); + } + + private static void warnDeprecatedStack(ConfigurationBuilderHolder holder) { + var stackName = transportStackOf(holder).get(); + switch (stackName) { + case "jdbc-ping-udp": + case "tcp": + case "udp": + case "azure": + case "ec2": + case "google": + logger.warnf("Stack '%s' is deprecated. We recommend to use 'jdbc-ping' instead", stackName); + } + } + + private static TransportConfigurationBuilder transportOf(ConfigurationBuilderHolder holder) { + return holder.getGlobalConfigurationBuilder().transport(); + } + + private static Attribute transportStackOf(ConfigurationBuilderHolder holder) { + var transport = transportOf(holder); + assert transport != null; + return transport.attributes().attribute(STACK); + } + + private static void validateTlsAvailable(ConfigurationBuilderHolder holder) { + var stackName = transportStackOf(holder).get(); + if (stackName == null) { + // unable to validate + return; + } + var config = transportOf(holder).build(); + for (var protocol : config.transport().jgroups().configurator(stackName).getProtocolStack()) { + var name = protocol.getProtocolName(); + if (name.equals(UDP.class.getSimpleName()) || + name.equals(UDP.class.getName()) || + name.equals(TCP_NIO2.class.getSimpleName()) || + name.equals(TCP_NIO2.class.getName())) { + throw new RuntimeException("Cache TLS is not available with protocol " + name); + } + } + } + + private static boolean isJdbcPingStack(String stackName) { + return "jdbc-ping".equals(stackName) || "jdbc-ping-udp".equals(stackName); + } + + private static class JpaFactoryAwareJGroupsChannelConfigurator extends EmbeddedJGroupsChannelConfigurator { + + private final JpaConnectionProviderFactory factory; + + public JpaFactoryAwareJGroupsChannelConfigurator(String name, List stack, JpaConnectionProviderFactory factory, boolean isUdp) { + super(name, stack, null, isUdp ? "udp" : "tcp"); + this.factory = Objects.requireNonNull(factory); + } + + @Override + public void afterCreation(Protocol protocol) { + super.afterCreation(protocol); + if (protocol instanceof KEYCLOAK_JDBC_PING2 kcPing) { + kcPing.setJpaConnectionProviderFactory(factory); + } + } + } +} diff --git a/model/infinispan/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/model/infinispan/src/main/resources/META-INF/services/org.keycloak.provider.Spi index 8297262fbf9..8b67e435e46 100644 --- a/model/infinispan/src/main/resources/META-INF/services/org.keycloak.provider.Spi +++ b/model/infinispan/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -18,3 +18,5 @@ org.keycloak.connections.infinispan.InfinispanConnectionSpi org.keycloak.spi.infinispan.CacheRemoteConfigProviderSpi org.keycloak.spi.infinispan.JGroupsCertificateProviderSpi +org.keycloak.spi.infinispan.CacheEmbeddedConfigProviderSpi +org.keycloak.spi.infinispan.CacheRemoteConfigProviderSpi \ No newline at end of file diff --git a/quarkus/runtime/src/main/resources/META-INF/services/org.keycloak.cluster.ManagedCacheManagerProvider b/model/infinispan/src/main/resources/META-INF/services/org.keycloak.spi.infinispan.CacheEmbeddedConfigProviderFactory similarity index 81% rename from quarkus/runtime/src/main/resources/META-INF/services/org.keycloak.cluster.ManagedCacheManagerProvider rename to model/infinispan/src/main/resources/META-INF/services/org.keycloak.spi.infinispan.CacheEmbeddedConfigProviderFactory index e0488026ff0..56395b39e75 100644 --- a/quarkus/runtime/src/main/resources/META-INF/services/org.keycloak.cluster.ManagedCacheManagerProvider +++ b/model/infinispan/src/main/resources/META-INF/services/org.keycloak.spi.infinispan.CacheEmbeddedConfigProviderFactory @@ -1,5 +1,5 @@ # -# Copyright 2021 Red Hat, Inc. and/or its affiliates +# Copyright 2025 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"); @@ -15,4 +15,4 @@ # limitations under the License. # -org.keycloak.quarkus.runtime.storage.infinispan.QuarkusCacheManagerProvider \ No newline at end of file +org.keycloak.spi.infinispan.impl.embedded.DefaultCacheEmbeddedConfigProviderFactory \ No newline at end of file diff --git a/quarkus/config-api/src/main/java/org/keycloak/config/MetricsOptions.java b/quarkus/config-api/src/main/java/org/keycloak/config/MetricsOptions.java index 8586534dfb6..72f74854028 100644 --- a/quarkus/config-api/src/main/java/org/keycloak/config/MetricsOptions.java +++ b/quarkus/config-api/src/main/java/org/keycloak/config/MetricsOptions.java @@ -15,4 +15,11 @@ public class MetricsOptions { .defaultValue(Boolean.TRUE) .hidden() // This is intended to be enabled all the time when global metrics are enabled, therefore this option is hidden .build(); + + public static final Option INFINISPAN_METRICS_ENABLED = new OptionBuilder<>("infinispan-metrics-enabled", Boolean.class) + .category(OptionCategory.METRICS) + .description("If Infinispan metrics should be collected and exposed.") + .defaultValue(Boolean.FALSE) + .hidden() // Intentional, Infinispan metrics are enabled when '--metrics-enabled' are enabled. + .build(); } diff --git a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/CacheBuildSteps.java b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/CacheBuildSteps.java deleted file mode 100644 index 3f82bcb6fe5..00000000000 --- a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/CacheBuildSteps.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2021 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.keycloak.quarkus.deployment; - -import io.quarkus.arc.deployment.SyntheticBeanBuildItem; -import io.quarkus.deployment.annotations.BuildProducer; -import io.quarkus.deployment.annotations.BuildStep; -import io.quarkus.deployment.annotations.Consume; -import io.quarkus.deployment.annotations.ExecutionTime; -import io.quarkus.deployment.annotations.Record; -import io.quarkus.deployment.logging.LoggingSetupBuildItem; -import jakarta.enterprise.context.ApplicationScoped; -import org.keycloak.quarkus.runtime.KeycloakRecorder; -import org.keycloak.quarkus.runtime.storage.infinispan.CacheManagerFactory; - -public class CacheBuildSteps { - - @Consume(ProfileBuildItem.class) - @Consume(ConfigBuildItem.class) - // Consume LoggingSetupBuildItem.class and record RUNTIME_INIT are necessary to ensure that logging is set up before the caches are initialized. - // This is to prevent the class TP in JGroups to pick up the trace logging at start up. While the logs will not appear on the console, - // they will still be created and use CPU cycles and create garbage collection. - // See: https://issues.redhat.com/browse/JGRP-2130 for the JGroups discussion, and https://github.com/keycloak/keycloak/issues/29129 for the issue Keycloak had with this. - @Consume(LoggingSetupBuildItem.class) - @Record(ExecutionTime.RUNTIME_INIT) - @BuildStep - void configureInfinispan(KeycloakRecorder recorder, BuildProducer syntheticBeanBuildItems) { - syntheticBeanBuildItems.produce(SyntheticBeanBuildItem.configure(CacheManagerFactory.class) - .scope(ApplicationScoped.class) - .unremovable() - .setRuntimeInit() - .runtimeValue(recorder.createCacheInitializer()).done()); - } -} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakRecorder.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakRecorder.java index 0d22e3b89f8..aafc9676f1c 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakRecorder.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/KeycloakRecorder.java @@ -17,21 +17,25 @@ package org.keycloak.quarkus.runtime; +import java.io.File; +import java.lang.annotation.Annotation; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; + import io.agroal.api.AgroalDataSource; import io.quarkus.agroal.DataSource; import io.quarkus.arc.Arc; import io.quarkus.arc.InstanceHandle; import io.quarkus.hibernate.orm.runtime.integration.HibernateOrmIntegrationRuntimeInitListener; -import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.annotations.Recorder; import io.vertx.core.Handler; import io.vertx.ext.web.RoutingContext; import liquibase.Scope; import liquibase.servicelocator.ServiceLocator; import org.hibernate.cfg.AvailableSettings; -import org.infinispan.commons.util.FileLookupFactory; import org.infinispan.protostream.SerializationContextInitializer; -import org.jboss.logging.Logger; import org.keycloak.Config; import org.keycloak.common.Profile; import org.keycloak.common.crypto.CryptoIntegration; @@ -46,33 +50,14 @@ import org.keycloak.quarkus.runtime.configuration.Configuration; import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider; import org.keycloak.quarkus.runtime.integration.QuarkusKeycloakSessionFactory; import org.keycloak.quarkus.runtime.storage.database.liquibase.FastServiceLocator; -import org.keycloak.quarkus.runtime.storage.infinispan.CacheManagerFactory; import org.keycloak.representations.userprofile.config.UPConfig; import org.keycloak.theme.ClasspathThemeProviderFactory; import org.keycloak.truststore.TruststoreBuilder; import org.keycloak.userprofile.DeclarativeUserProfileProviderFactory; -import java.io.BufferedReader; -import java.io.File; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.lang.annotation.Annotation; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.function.BiConsumer; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static org.keycloak.quarkus.runtime.configuration.Configuration.getKcConfigValue; - @Recorder public class KeycloakRecorder { - private static final Logger logger = Logger.getLogger(KeycloakRecorder.class); - public void initConfig() { Config.init(new MicroProfileConfigProvider()); } @@ -123,77 +108,29 @@ public class KeycloakRecorder { QuarkusKeycloakSessionFactory.setInstance(new QuarkusKeycloakSessionFactory(factories, defaultProviders, preConfiguredProviders, themes)); } - public RuntimeValue createCacheInitializer() { - try { - CacheManagerFactory cacheManagerFactory = new CacheManagerFactory(getInfinispanConfigFile()); - return new RuntimeValue<>(cacheManagerFactory); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - private String getInfinispanConfigFile() { - String configFile = getKcConfigValue("spi-connections-infinispan-quarkus-config-file").getValue(); - - if (configFile == null) { - throw new IllegalArgumentException("Option 'configFile' needs to be specified"); - } - - Path configPath = Paths.get(configFile); - String path; - - if (configPath.toFile().exists()) { - path = configPath.toFile().getAbsolutePath(); - } else { - path = configPath.getFileName().toString(); - } - - logger.debugf("Infinispan configuration file: %s", path); - - InputStream url = FileLookupFactory.newInstance().lookupFile(path, KeycloakRecorder.class.getClassLoader()); - - if (url == null) { - throw new IllegalArgumentException("Could not load cluster configuration file at [" + configPath + "]"); - } - - try (BufferedReader reader = new BufferedReader(new InputStreamReader(url))) { - return reader.lines().collect(Collectors.joining("\n")); - } catch (Exception cause) { - throw new RuntimeException("Failed to read clustering configuration from [" + url + "]", cause); - } - } - public void setDefaultUserProfileConfiguration(UPConfig configuration) { DeclarativeUserProfileProviderFactory.setDefaultConfig(configuration); } public HibernateOrmIntegrationRuntimeInitListener createUserDefinedUnitListener(String name) { - return new HibernateOrmIntegrationRuntimeInitListener() { - @Override - public void contributeRuntimeProperties(BiConsumer propertyCollector) { - try (InstanceHandle instance = Arc.container().instance( - AgroalDataSource.class, new DataSource() { - @Override public Class annotationType() { - return DataSource.class; - } + return propertyCollector -> { + try (InstanceHandle instance = Arc.container().instance( + AgroalDataSource.class, new DataSource() { + @Override public Class annotationType() { + return DataSource.class; + } - @Override public String value() { - return name; - } - })) { - propertyCollector.accept(AvailableSettings.DATASOURCE, instance.get()); - } + @Override public String value() { + return name; + } + })) { + propertyCollector.accept(AvailableSettings.DATASOURCE, instance.get()); } }; } public HibernateOrmIntegrationRuntimeInitListener createDefaultUnitListener() { - return new HibernateOrmIntegrationRuntimeInitListener() { - @Override - public void contributeRuntimeProperties(BiConsumer propertyCollector) { - propertyCollector.accept(AvailableSettings.DEFAULT_SCHEMA, Configuration.getRawValue("kc.db-schema")); - } - }; + return propertyCollector -> propertyCollector.accept(AvailableSettings.DEFAULT_SCHEMA, Configuration.getRawValue("kc.db-schema")); } public void setCryptoProvider(FipsMode fipsMode) { diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/CachingPropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/CachingPropertyMappers.java index 085ff2645fe..4935a7d083d 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/CachingPropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/CachingPropertyMappers.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.Optional; import java.util.function.BooleanSupplier; +import com.google.common.base.CaseFormat; import io.smallrye.config.ConfigSourceInterceptorContext; import org.keycloak.common.Profile; import org.keycloak.config.CachingOptions; @@ -39,7 +40,7 @@ final class CachingPropertyMappers { .build(), fromOption(CachingOptions.CACHE_STACK) .isEnabled(CachingPropertyMappers::cacheSetToInfinispan, CACHE_STACK_SET_TO_ISPN) - .to("kc.spi-connections-infinispan-quarkus-stack") + .to("kc.spi-cache-embedded-default-stack") .paramLabel("stack") .build(), fromOption(CachingOptions.CACHE_CONFIG_FILE) @@ -51,7 +52,7 @@ final class CachingPropertyMappers { } else return null; }) - .to("kc.spi-connections-infinispan-quarkus-config-file") + .to("kc.spi-cache-embedded-default-config-file") .transformer(CachingPropertyMappers::resolveConfigFile) .validator(s -> { if (!Files.exists(Paths.get(resolveConfigFile(s, null)))) { @@ -126,6 +127,7 @@ final class CachingPropertyMappers { .build(), fromOption(CachingOptions.CACHE_METRICS_HISTOGRAMS_ENABLED) .isEnabled(MetricsPropertyMappers::metricsEnabled, MetricsPropertyMappers.METRICS_ENABLED_MSG) + .to("kc.spi-cache-embedded-default-metrics-histograms-enabled") .build() ); @@ -210,6 +212,7 @@ final class CachingPropertyMappers { return fromOption(CachingOptions.maxCountOption(cacheName)) .isEnabled(isEnabled, enabledWhen) .paramLabel("max-count") + .to("kc.spi-cache-embedded-default-%s-max-count".formatted(CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_HYPHEN, cacheName))) .build(); } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/MetricsPropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/MetricsPropertyMappers.java index bb7763462d8..4176cba9bdd 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/MetricsPropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/MetricsPropertyMappers.java @@ -20,6 +20,11 @@ final class MetricsPropertyMappers { fromOption(MetricsOptions.PASSWORD_VALIDATION_COUNTER_ENABLED) .to("kc.spi-credential-keycloak-password-metrics-enabled") .isEnabled(MetricsPropertyMappers::metricsEnabled, "metrics are enabled") + .build(), + fromOption(MetricsOptions.INFINISPAN_METRICS_ENABLED) + .mapFrom(MetricsOptions.METRICS_ENABLED) + .to("kc.spi-cache-embedded-default-metrics-enabled") + .isEnabled(MetricsPropertyMappers::metricsEnabled, "metrics are enabled") .build() }; } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/CacheManagerFactory.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/CacheManagerFactory.java deleted file mode 100644 index ff4825633f4..00000000000 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/CacheManagerFactory.java +++ /dev/null @@ -1,300 +0,0 @@ -/* - * Copyright 2021 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.keycloak.quarkus.runtime.storage.infinispan; - -import java.security.KeyManagementException; -import java.security.NoSuchAlgorithmException; -import java.util.Arrays; -import java.util.Map; -import java.util.function.Predicate; -import java.util.function.Supplier; -import java.util.stream.Stream; - -import io.micrometer.core.instrument.Metrics; -import org.infinispan.configuration.cache.ConfigurationBuilder; -import org.infinispan.configuration.cache.HashConfiguration; -import org.infinispan.configuration.global.ShutdownHookBehavior; -import org.infinispan.configuration.parsing.ConfigurationBuilderHolder; -import org.infinispan.configuration.parsing.ParserRegistry; -import org.infinispan.manager.DefaultCacheManager; -import org.infinispan.manager.EmbeddedCacheManager; -import org.infinispan.metrics.config.MicrometerMeterRegisterConfigurationBuilder; -import org.infinispan.persistence.remote.configuration.RemoteStoreConfigurationBuilder; -import org.jboss.logging.Logger; -import org.keycloak.common.Profile; -import org.keycloak.common.util.MultiSiteUtils; -import org.keycloak.config.CachingOptions; -import org.keycloak.config.MetricsOptions; -import org.keycloak.config.Option; -import org.keycloak.connections.infinispan.InfinispanConnectionProvider; -import org.keycloak.connections.infinispan.InfinispanUtil; -import org.keycloak.infinispan.util.InfinispanUtils; -import org.keycloak.jgroups.JGroupsConfigurator; -import org.keycloak.marshalling.Marshalling; -import org.keycloak.models.KeycloakSession; -import org.keycloak.quarkus.runtime.configuration.Configuration; - -import javax.net.ssl.SSLContext; - -import static org.keycloak.config.CachingOptions.CACHE_REMOTE_PASSWORD_PROPERTY; -import static org.keycloak.config.CachingOptions.CACHE_REMOTE_USERNAME_PROPERTY; -import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.CLIENT_SESSION_CACHE_NAME; -import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.CLUSTERED_CACHE_NAMES; -import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.CRL_CACHE_NAME; -import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.LOCAL_CACHE_NAMES; -import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.OFFLINE_CLIENT_SESSION_CACHE_NAME; -import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.OFFLINE_USER_SESSION_CACHE_NAME; -import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.USER_AND_CLIENT_SESSION_CACHES; -import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.USER_SESSION_CACHE_NAME; -import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.WORK_CACHE_NAME; - -public class CacheManagerFactory { - - public static final Logger logger = Logger.getLogger(CacheManagerFactory.class); - // Map with the default cache configuration if the cache is not present in the XML. - private static final Map> DEFAULT_CONFIGS = Map.of( - CRL_CACHE_NAME, InfinispanUtil::getCrlCacheConfig - ); - private static final Supplier TO_NULL = () -> null; - - private volatile EmbeddedCacheManager cacheManager; - private final JGroupsConfigurator jGroupsConfigurator; - - public CacheManagerFactory(String config) { - ConfigurationBuilderHolder builder = new ParserRegistry().parse(config); - jGroupsConfigurator = JGroupsConfigurator.create(builder); - } - - public EmbeddedCacheManager getOrCreateEmbeddedCacheManager(KeycloakSession keycloakSession) { - if (cacheManager != null) - return cacheManager; - - synchronized (this) { - if (cacheManager == null) { - cacheManager = startEmbeddedCacheManager(keycloakSession); - } - } - return cacheManager; - } - - private EmbeddedCacheManager startEmbeddedCacheManager(KeycloakSession session) { - logger.info("Starting Infinispan embedded cache manager"); - var builder = jGroupsConfigurator.holder(); - - // We must disable the Infinispan default ShutdownHook as we manage the EmbeddedCacheManager lifecycle explicitly - // with #shutdown and multiple calls to EmbeddedCacheManager#stop can lead to Exceptions being thrown - builder.getGlobalConfigurationBuilder().shutdown().hookBehavior(ShutdownHookBehavior.DONT_REGISTER); - - Marshalling.configure(builder.getGlobalConfigurationBuilder()); - assertAllCachesAreConfigured(builder, - // skip revision caches, those are defined by DefaultInfinispanConnectionProviderFactory - Arrays.stream(LOCAL_CACHE_NAMES) - .filter(Predicate.not(InfinispanConnectionProvider.REALM_REVISIONS_CACHE_NAME::equals)) - .filter(Predicate.not(InfinispanConnectionProvider.AUTHORIZATION_REVISIONS_CACHE_NAME::equals)) - .filter(Predicate.not(InfinispanConnectionProvider.USER_REVISIONS_CACHE_NAME::equals)) - ); - if (InfinispanUtils.isRemoteInfinispan()) { - var builders = builder.getNamedConfigurationBuilders(); - // remove all distributed caches - logger.debug("Removing all distributed caches."); - for (String cacheName : CLUSTERED_CACHE_NAMES) { - if (hasRemoteStore(builders.get(cacheName))) { - logger.warnf("remote-store configuration detected for cache '%s'. Explicit cache configuration ignored when using '%s' or '%s' Features.", cacheName, Profile.Feature.CLUSTERLESS.getKey(), Profile.Feature.MULTI_SITE.getKey()); - } - builders.remove(cacheName); - } - // Disable JGroups, not required when the data is stored in the Remote Cache. - // The existing caches are local and do not require JGroups to work properly. - builder.getGlobalConfigurationBuilder().nonClusteredDefault(); - } else { - // embedded mode! - assertAllCachesAreConfigured(builder, Arrays.stream(CLUSTERED_CACHE_NAMES)); - if (builder.getNamedConfigurationBuilders().entrySet().stream().anyMatch(c -> c.getValue().clustering().cacheMode().isClustered())) { - if (jGroupsConfigurator.isLocal()) { - throw new RuntimeException("Unable to use clustered cache with local mode."); - } - } - jGroupsConfigurator.configure(session); - configureCacheMaxCount(builder, CachingOptions.CLUSTERED_MAX_COUNT_CACHES); - configureSessionsCaches(builder); - validateWorkCacheConfiguration(builder); - } - configureCacheMaxCount(builder, CachingOptions.LOCAL_MAX_COUNT_CACHES); - checkForRemoteStores(builder); - configureMetrics(builder); - - return new DefaultCacheManager(builder, isStartEagerly()); - } - - private static void configureMetrics(ConfigurationBuilderHolder holder) { - if (Configuration.isTrue(MetricsOptions.METRICS_ENABLED)) { - holder.getGlobalConfigurationBuilder().addModule(MicrometerMeterRegisterConfigurationBuilder.class); - holder.getGlobalConfigurationBuilder().module(MicrometerMeterRegisterConfigurationBuilder.class).meterRegistry(Metrics.globalRegistry); - holder.getGlobalConfigurationBuilder().cacheContainer().statistics(true); - holder.getGlobalConfigurationBuilder().metrics().namesAsTags(true); - if (Configuration.isTrue(CachingOptions.CACHE_METRICS_HISTOGRAMS_ENABLED)) { - holder.getGlobalConfigurationBuilder().metrics().histograms(true); - } - holder.getNamedConfigurationBuilders().forEach((s, configurationBuilder) -> configurationBuilder.statistics().enabled(true)); - } - } - - private static boolean isRemoteTLSEnabled() { - return Configuration.isTrue(CachingOptions.CACHE_REMOTE_TLS_ENABLED); - } - - private static boolean isRemoteAuthenticationEnabled() { - return Configuration.getOptionalKcValue(CACHE_REMOTE_USERNAME_PROPERTY).isPresent() || - Configuration.getOptionalKcValue(CACHE_REMOTE_PASSWORD_PROPERTY).isPresent(); - } - - private static SSLContext createSSLContext() { - try { - // uses the default Java Runtime TrustStore, or the one generated by Keycloak (see org.keycloak.truststore.TruststoreBuilder) - var sslContext = SSLContext.getInstance("TLS"); - sslContext.init(null, null, null); - return sslContext; - } catch (NoSuchAlgorithmException | KeyManagementException e) { - throw new RuntimeException(e); - } - } - - private static boolean isStartEagerly() { - // eagerly starts caches by default - return Boolean.parseBoolean(System.getProperty("kc.cache-ispn-start-eagerly", Boolean.TRUE.toString())); - } - - private static int getStartTimeout() { - return Integer.getInteger("kc.cache-ispn-start-timeout", 120); - } - - /** - * - * RemoteStores were previously used when running Keycloak in the CrossDC environment, and Keycloak code - * contained a lot of performance optimizations to make this work smoothly. - * These optimizations are now removed as recommended multi-site setup no longer relies on RemoteStores. - * A lot of blueprints in the wild may turn into very ineffective setups. - *

- * For this reason, we need to be more opinionated on what configurations we allow, - * especially for user and client sessions. - * This method is responsible for checking the Infinispan configuration used and either change the configuration to - * more effective when possible or refuse to start with recommendations for users to change their config. - * - * @param builder Cache configuration builder - */ - private static void checkForRemoteStores(ConfigurationBuilderHolder builder) { - for (String cacheName : USER_AND_CLIENT_SESSION_CACHES) { - ConfigurationBuilder cacheConfigurationBuilder = builder.getNamedConfigurationBuilders().get(cacheName); - - if (cacheConfigurationBuilder != null && hasRemoteStore(cacheConfigurationBuilder)) { - if (Profile.isFeatureEnabled(Profile.Feature.PERSISTENT_USER_SESSIONS)) { - logger.warnf("Feature %s is enabled and remote store detected for cache '%s'. Remote stores are no longer needed when sessions stored in the database. The configuration will be ignored.", Profile.Feature.PERSISTENT_USER_SESSIONS.getKey(), cacheName); - cacheConfigurationBuilder.persistence().stores().removeIf(RemoteStoreConfigurationBuilder.class::isInstance); - } else { - logger.fatalf("Remote stores are not supported for embedded caches storing user and client sessions.%nFor keeping user sessions across restarts, use feature %s which is enabled by default.%nFor multi-site support, enable %s.", - Profile.Feature.PERSISTENT_USER_SESSIONS.getKey(), Profile.Feature.MULTI_SITE.getKey()); - - throw new RuntimeException("Remote stores for storing user and client sessions are not supported."); - } - } - } - } - - private static void configureSessionsCaches(ConfigurationBuilderHolder builder) { - Stream.of(USER_SESSION_CACHE_NAME, CLIENT_SESSION_CACHE_NAME, OFFLINE_USER_SESSION_CACHE_NAME, OFFLINE_CLIENT_SESSION_CACHE_NAME) - .forEach(cacheName -> { - var configurationBuilder = builder.getNamedConfigurationBuilders().get(cacheName); - if (MultiSiteUtils.isPersistentSessionsEnabled()) { - if (configurationBuilder.memory().maxCount() == -1) { - logger.infof("Persistent user sessions enabled and no memory limit found in configuration. Setting max entries for %s to 10000 entries.", cacheName); - configurationBuilder.memory().maxCount(10000); - } - /* The number of owners for these caches then need to be set to `1` to avoid backup owners with inconsistent data. - As primary owner evicts a key based on its locally evaluated maxCount setting, it wouldn't tell the backup owner about this, and then the backup owner would be left with a soon-to-be-outdated key. - While a `remove` is forwarded to the backup owner regardless if the key exists on the primary owner, a `computeIfPresent` is not, and it would leave a backup owner with an outdated key. - With the number of owners set to `1`, there will be no backup owners, so this is the setting to choose with persistent sessions enabled to ensure consistent data in the caches. */ - configurationBuilder.clustering().hash().numOwners(1); - } else { - if (configurationBuilder.memory().maxCount() != -1) { - logger.warnf("Persistent user sessions disabled and memory limit found in configuration for cache %s. This might be a misconfiguration! Update your Infinispan configuration to remove this message.", cacheName); - } - if (configurationBuilder.memory().maxCount() == 10000 && (cacheName.equals(USER_SESSION_CACHE_NAME) || cacheName.equals(CLIENT_SESSION_CACHE_NAME))) { - logger.warnf("Persistent user sessions disabled and memory limit is set to default value 10000. Ignoring cache limits to avoid losing sessions for cache %s.", cacheName); - configurationBuilder.memory().maxCount(-1); - } - if (configurationBuilder.clustering().hash().attributes().attribute(HashConfiguration.NUM_OWNERS).get() == 1 - && configurationBuilder.persistence().stores().isEmpty()) { - logger.warnf("Number of owners is one for cache %s, and no persistence is configured. This might be a misconfiguration as you will lose data when a single node is restarted!", cacheName); - } - } - }); - } - - private static void configureCacheMaxCount(ConfigurationBuilderHolder holder, String[] caches) { - for (String cache : caches) { - var memory = holder.getNamedConfigurationBuilders().get(cache).memory(); - String propKey = CachingOptions.cacheMaxCountProperty(cache); - Configuration.getOptionalKcValue(propKey) - .map(Integer::parseInt) - .ifPresent(memory::maxCount); - } - } - - private static void assertAllCachesAreConfigured(ConfigurationBuilderHolder holder, Stream caches) { - for (var it = caches.iterator() ; it.hasNext() ; ) { - var cache = it.next(); - var builder = holder.getNamedConfigurationBuilders().get(cache); - if (builder != null) { - continue; - } - builder = DEFAULT_CONFIGS.getOrDefault(cache, TO_NULL).get(); - if (builder == null) { - throw new IllegalStateException("Infinispan cache '%s' not found. Make sure it is defined in your XML configuration file.".formatted(cache)); - } - holder.getNamedConfigurationBuilders().put(cache, builder); - } - } - - private static void validateWorkCacheConfiguration(ConfigurationBuilderHolder builder) { - var cacheBuilder = builder.getNamedConfigurationBuilders().get(WORK_CACHE_NAME); - if (cacheBuilder == null) { - throw new RuntimeException("Unable to start Keycloak. '%s' cache is missing".formatted(WORK_CACHE_NAME)); - } - if (builder.getGlobalConfigurationBuilder().cacheContainer().transport().getTransport() == null) { - // non-clustered, Keycloak started in dev mode? - return; - } - var cacheMode = cacheBuilder.clustering().cacheMode(); - if (!cacheMode.isReplicated()) { - throw new RuntimeException("Unable to start Keycloak. '%s' cache must be replicated but is %s".formatted(WORK_CACHE_NAME, cacheMode.friendlyCacheModeString().toLowerCase())); - } - } - - public static String requiredStringProperty(String propertyName) { - return Configuration.getOptionalKcValue(propertyName).orElseThrow(() -> new RuntimeException("Property " + propertyName + " required but not specified")); - } - - public static int requiredIntegerProperty(Option option) { - return Configuration.getOptionalIntegerValue(option) - .orElseThrow(() -> new RuntimeException("Property '%s' required but not specified".formatted(option.getKey()))); - } - - private static boolean hasRemoteStore(ConfigurationBuilder builder) { - return builder.persistence().stores().stream().anyMatch(RemoteStoreConfigurationBuilder.class::isInstance); - } -} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/QuarkusCacheManagerProvider.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/QuarkusCacheManagerProvider.java deleted file mode 100644 index 096080853e4..00000000000 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/QuarkusCacheManagerProvider.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2021 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.keycloak.quarkus.runtime.storage.infinispan; - -import io.quarkus.arc.Arc; -import org.keycloak.Config; -import org.keycloak.cluster.ManagedCacheManagerProvider; -import org.keycloak.models.KeycloakSession; - -/** - * @author Pedro Igor - */ -@SuppressWarnings({"unchecked", "resource"}) -public final class QuarkusCacheManagerProvider implements ManagedCacheManagerProvider { - - @Override - public C getEmbeddedCacheManager(KeycloakSession keycloakSession, Config.Scope config) { - return (C) Arc.container().instance(CacheManagerFactory.class).get().getOrCreateEmbeddedCacheManager(keycloakSession); - } -} diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/QuarkusInfinispanConnectionFactory.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/QuarkusInfinispanConnectionFactory.java deleted file mode 100644 index 0168a4b9366..00000000000 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/QuarkusInfinispanConnectionFactory.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright 2021 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.keycloak.quarkus.runtime.storage.infinispan; - -import org.infinispan.manager.EmbeddedCacheManager; -import org.keycloak.connections.infinispan.DefaultInfinispanConnectionProviderFactory; -import org.keycloak.provider.ProviderConfigProperty; -import org.keycloak.provider.ProviderConfigurationBuilder; - -import java.util.List; - -/** - * @author Pedro Igor - */ -public class QuarkusInfinispanConnectionFactory extends DefaultInfinispanConnectionProviderFactory { - - @Override - protected EmbeddedCacheManager initContainerManaged(EmbeddedCacheManager cacheManager) { - EmbeddedCacheManager result = super.initContainerManaged(cacheManager); - // force closing the cache manager when stopping the provider - // we probably want to refactor the default impl a bit to support this use case - containerManaged = false; - return result; - } - - @Override - public int order() { - return 100; - } - - @Override - public String getId() { - return "quarkus"; - } - - @Override - public List getConfigMetadata() { - return ProviderConfigurationBuilder.create() - .property() - .name("site-name") - .helpText("Site name for multi-site deployments") - .type("string") - .add() - .build(); - } -} diff --git a/quarkus/runtime/src/main/resources/META-INF/services/org.keycloak.connections.infinispan.InfinispanConnectionProviderFactory b/quarkus/runtime/src/main/resources/META-INF/services/org.keycloak.connections.infinispan.InfinispanConnectionProviderFactory deleted file mode 100644 index fec4d5c4bdd..00000000000 --- a/quarkus/runtime/src/main/resources/META-INF/services/org.keycloak.connections.infinispan.InfinispanConnectionProviderFactory +++ /dev/null @@ -1,20 +0,0 @@ -# -# /* -# * Copyright 2021 Red Hat, Inc. and/or its affiliates -# * and other contributors as indicated by the @author tags. -# * -# * Licensed under the Apache License, Version 2.0 (the "License"); -# * you may not use this file except in compliance with the License. -# * You may obtain a copy of the License at -# * -# * http://www.apache.org/licenses/LICENSE-2.0 -# * -# * Unless required by applicable law or agreed to in writing, software -# * distributed under the License is distributed on an "AS IS" BASIS, -# * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# * See the License for the specific language governing permissions and -# * limitations under the License. -# */ -# - -org.keycloak.quarkus.runtime.storage.infinispan.QuarkusInfinispanConnectionFactory \ No newline at end of file diff --git a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/AbstractConfigurationTest.java b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/AbstractConfigurationTest.java index 076b58400c3..db9278916ef 100644 --- a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/AbstractConfigurationTest.java +++ b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/AbstractConfigurationTest.java @@ -137,7 +137,7 @@ public abstract class AbstractConfigurationTest { Environment.removeHomeDir(); } - protected Config.Scope initConfig(String... scope) { + protected static Config.Scope initConfig(String... scope) { Config.init(new MicroProfileConfigProvider(createConfig())); return Config.scope(scope); } diff --git a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/ConfigurationTest.java b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/ConfigurationTest.java index a28b01d4923..e04af6e45b8 100644 --- a/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/ConfigurationTest.java +++ b/quarkus/runtime/src/test/java/org/keycloak/quarkus/runtime/configuration/test/ConfigurationTest.java @@ -50,6 +50,8 @@ import org.keycloak.quarkus.runtime.configuration.mappers.HttpPropertyMappers; import org.keycloak.quarkus.runtime.Environment; import org.keycloak.quarkus.runtime.vault.FilesKeystoreVaultProviderFactory; import org.keycloak.quarkus.runtime.vault.FilesPlainTextVaultProviderFactory; +import org.keycloak.spi.infinispan.CacheEmbeddedConfigProviderSpi; +import org.keycloak.spi.infinispan.impl.embedded.DefaultCacheEmbeddedConfigProviderFactory; import org.mariadb.jdbc.MariaDbDataSource; import org.postgresql.xa.PGXADataSource; @@ -375,21 +377,21 @@ public class ConfigurationTest extends AbstractConfigurationTest { public void testClusterConfig() { // Cluster enabled by default, but disabled for the "dev" profile String conf = Environment.getHomeDir() + File.separator + "conf" + File.separator; - Assert.assertEquals(conf + "cache-ispn.xml", initConfig("connectionsInfinispan", "quarkus").get("configFile")); + Assert.assertEquals(conf + "cache-ispn.xml", cacheEmbeddedConfiguration().get(DefaultCacheEmbeddedConfigProviderFactory.CONFIG)); // If explicitly set, then it is always used regardless of the profile System.clearProperty(org.keycloak.common.util.Environment.PROFILE); ConfigArgsConfigSource.setCliArgs("--cache-config-file=cluster-foo.xml"); - Assert.assertEquals(conf + "cluster-foo.xml", initConfig("connectionsInfinispan", "quarkus").get("configFile")); + Assert.assertEquals(conf + "cluster-foo.xml", cacheEmbeddedConfiguration().get(DefaultCacheEmbeddedConfigProviderFactory.CONFIG)); System.setProperty(org.keycloak.common.util.Environment.PROFILE, "dev"); - Assert.assertEquals(conf + "cluster-foo.xml", initConfig("connectionsInfinispan", "quarkus").get("configFile")); + Assert.assertEquals(conf + "cluster-foo.xml", cacheEmbeddedConfiguration().get(DefaultCacheEmbeddedConfigProviderFactory.CONFIG)); ConfigArgsConfigSource.setCliArgs(""); - Assert.assertEquals("cache-local.xml", initConfig("connectionsInfinispan", "quarkus").get("configFile")); + Assert.assertEquals("cache-local.xml", cacheEmbeddedConfiguration().get(DefaultCacheEmbeddedConfigProviderFactory.CONFIG)); ConfigArgsConfigSource.setCliArgs("--cache-stack=foo"); - Assert.assertEquals("foo", initConfig("connectionsInfinispan", "quarkus").get("stack")); + Assert.assertEquals("foo", cacheEmbeddedConfiguration().get(DefaultCacheEmbeddedConfigProviderFactory.STACK)); } @Test @@ -584,4 +586,8 @@ public class ConfigurationTest extends AbstractConfigurationTest { SmallRyeConfig config = createConfig(); assertEquals("200k", config.getConfigValue("quarkus.http.limits.max-header-size").getValue()); } + + private static Config.Scope cacheEmbeddedConfiguration() { + return initConfig(CacheEmbeddedConfigProviderSpi.SPI_NAME, DefaultCacheEmbeddedConfigProviderFactory.PROVIDER_ID); + } } diff --git a/quarkus/tests/integration/src/test/java/org/keycloak/it/storage/database/ExternalInfinispanTest.java b/quarkus/tests/integration/src/test/java/org/keycloak/it/storage/database/ExternalInfinispanTest.java index 0a03e9335f7..e9ddf079f52 100644 --- a/quarkus/tests/integration/src/test/java/org/keycloak/it/storage/database/ExternalInfinispanTest.java +++ b/quarkus/tests/integration/src/test/java/org/keycloak/it/storage/database/ExternalInfinispanTest.java @@ -44,7 +44,7 @@ public class ExternalInfinispanTest { "--cache-remote-username=keycloak", "--cache-remote-password=Password1!", "--cache-remote-tls-enabled=false", - "--spi-connections-infinispan-quarkus-site-name=ISPN", + "--spi-cache-embedded-default-site-name=ISPN", "--spi-load-balancer-check-remote-poll-interval=500", "-Dkc.cache-remote-create-caches=true", "--verbose" @@ -62,7 +62,7 @@ public class ExternalInfinispanTest { "--cache-remote-username=keycloak", "--cache-remote-password=Password1!", "--cache-remote-tls-enabled=false", - "--spi-connections-infinispan-quarkus-site-name=ISPN", + "--spi-cache-embedded-default-site-name=ISPN", "--spi-load-balancer-check-remote-poll-interval=500", "-Dkc.cache-remote-create-caches=true", "--verbose" @@ -93,6 +93,17 @@ public class ExternalInfinispanTest { "--verbose" }) void testSiteNameAsSystemProperty(CLIResult cliResult) { - cliResult.assertMessage("System property jboss.site.name is in use. Use --spi-connections-infinispan-quarkus-site-name config option instead"); + cliResult.assertMessage("System property jboss.site.name is in use. Use --spi-cache-embedded-default-site-name config option instead"); + } + + @Test + @Launch({ + "start-dev", + "--cache=ispn", + "-Djboss.node.name=ISPN", + "--verbose" + }) + void testNodeNameAsSystemProperty(CLIResult cliResult) { + cliResult.assertMessage("System property jboss.node.name is in use. Use --spi-cache-embedded-default-node-name config option instead"); } } diff --git a/server-spi-private/src/main/java/org/keycloak/cluster/ManagedCacheManagerProvider.java b/server-spi-private/src/main/java/org/keycloak/cluster/ManagedCacheManagerProvider.java deleted file mode 100644 index 43da64901c6..00000000000 --- a/server-spi-private/src/main/java/org/keycloak/cluster/ManagedCacheManagerProvider.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2019 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.cluster; - -import org.keycloak.Config; -import org.keycloak.models.KeycloakSession; - -/** - * A Service Provider Interface (SPI) that allows to plug-in an embedded or remote cache manager instance. - * - * @author Pedro Igor - */ -public interface ManagedCacheManagerProvider { - - C getEmbeddedCacheManager(KeycloakSession keycloakSession, Config.Scope config); - - /** - * @return A RemoteCacheManager if the features {@link org.keycloak.common.Profile.Feature#CLUSTERLESS} or {@link org.keycloak.common.Profile.Feature#MULTI_SITE} is enabled, {@code null} otherwise. - * @deprecated The RemoteCacheManager is created and managed by keycloak. Use InfinispanConnectionProvider to retrieve it and implement CacheRemoteConfigProvider to overwrite the configuration. - */ - @Deprecated(since = "26.3", forRemoval = true) - default C getRemoteCacheManager(Config.Scope config) { - return null; - } -} diff --git a/testsuite/integration-arquillian/HOW-TO-RUN.md b/testsuite/integration-arquillian/HOW-TO-RUN.md index a8a3c982268..1c0876c5f58 100644 --- a/testsuite/integration-arquillian/HOW-TO-RUN.md +++ b/testsuite/integration-arquillian/HOW-TO-RUN.md @@ -394,7 +394,6 @@ Then run any cluster test as usual. mvn -f testsuite/integration-arquillian/tests/base/pom.xml \ -Pauth-server-cluster-undertow,db-mysql \ -Dsession.cache.owners=2 \ - -Dkeycloak.connectionsInfinispan.sessionsOwners=2 \ -Dbackends.console.output=true \ -Dauth.server.log.check=false \ -Dfrontend.console.output=true \ @@ -410,8 +409,8 @@ You can use any cluster test (eg. AuthenticationSessionFailoverClusterTest) and -Dauth.server.undertow=false -Dauth.server.undertow.cluster=true -Dauth.server.cluster=true -Dkeycloak.connectionsJpa.url=jdbc:mysql://localhost/keycloak -Dkeycloak.connectionsJpa.driver=com.mysql.jdbc.Driver - -Dkeycloak.connectionsJpa.user=keycloak -Dkeycloak.connectionsJpa.password=keycloak -Dkeycloak.connectionsInfinispan.clustered=true -Dresources - -Dkeycloak.connectionsInfinispan.sessionsOwners=2 -Dsession.cache.owners=2 + -Dkeycloak.connectionsJpa.user=keycloak -Dkeycloak.connectionsJpa.password=keycloak -Dresources + -Dsession.cache.owners=2 Invalidation tests (subclass of `AbstractInvalidationClusterTest`) don't need last two properties. @@ -423,8 +422,8 @@ This mode is useful for develop/manual tests of clustering features. You will ne 1) Run KeycloakServer server1 with: -Dkeycloak.connectionsJpa.url=jdbc:mysql://localhost/keycloak -Dkeycloak.connectionsJpa.driver=com.mysql.jdbc.Driver - -Dkeycloak.connectionsJpa.user=keycloak -Dkeycloak.connectionsJpa.password=keycloak -Dkeycloak.connectionsInfinispan.clustered=true - -Dkeycloak.connectionsInfinispan.sessionsOwners=2 -Dresources + -Dkeycloak.connectionsJpa.user=keycloak -Dkeycloak.connectionsJpa.password=keycloak + -Dresources and argument: `-p 8181` diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/AbstractQuarkusDeployableContainer.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/AbstractQuarkusDeployableContainer.java index 2db9d41817c..9e74d8f036f 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/AbstractQuarkusDeployableContainer.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/AbstractQuarkusDeployableContainer.java @@ -44,7 +44,6 @@ import java.util.stream.Collectors; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSession; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; @@ -134,7 +133,7 @@ public abstract class AbstractQuarkusDeployableContainer implements DeployableCo } @Override - public void undeploy(Descriptor descriptor) throws DeploymentException { + public void undeploy(Descriptor descriptor) { } @@ -183,7 +182,7 @@ public abstract class AbstractQuarkusDeployableContainer implements DeployableCo } if (configuration.getRoute() != null) { - commands.add("-Djboss.node.name=" + configuration.getRoute()); + commands.add("--spi-cache-embedded-default-node-name=" + configuration.getRoute()); } if (System.getProperty("auth.server.quarkus.log-level") != null) { @@ -231,7 +230,7 @@ public abstract class AbstractQuarkusDeployableContainer implements DeployableCo commands.add("--cache-remote-username=keycloak"); commands.add("--cache-remote-password=Password1!"); commands.add("--cache-remote-tls-enabled=false"); - commands.add("--spi-connections-infinispan-quarkus-site-name=test"); + commands.add("--spi-cache-embedded-default-site-name=test"); configuration.appendJavaOpts("-Dkc.cache-remote-create-caches=true"); System.setProperty("kc.cache-remote-create-caches", "true"); } @@ -359,8 +358,8 @@ public abstract class AbstractQuarkusDeployableContainer implements DeployableCo } connection.disconnect(); - } catch (Exception ignore) { - e = ignore; + } catch (Exception exception) { + e = exception; } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json index aee5e9a7141..91eac8bdc98 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json @@ -173,17 +173,9 @@ } }, - "connectionsInfinispan": { + "cacheEmbedded": { "default": { - "jgroupsUdpMcastAddr": "${keycloak.connectionsInfinispan.jgroupsUdpMcastAddr:234.56.78.90}", - "jgroupsBindAddr": "${keycloak.connectionsInfinispan.jgroupsBindAddr:127.0.0.1}", - "nodeName": "${keycloak.connectionsInfinispan.nodeName,jboss.node.name:}", - "siteName": "${keycloak.connectionsInfinispan.siteName:}", - "clustered": "${keycloak.connectionsInfinispan.clustered:false}", - "async": "${keycloak.connectionsInfinispan.async:false}", - "sessionsOwners": "${keycloak.connectionsInfinispan.sessionsOwners:1}", - "l1Lifespan": "${keycloak.connectionsInfinispan.l1Lifespan:600000}", - "embedded": "${keycloak.connectionsInfinispan.embedded:true}" + "nodeName": "${keycloak.cacheEmbedded.nodeName,jboss.node.name:}" } }, diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml index f1b45033fa7..7ab273874fe 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/arquillian.xml @@ -245,9 +245,7 @@ node1 ${undertow.remote} { - "keycloak.connectionsInfinispan.jgroupsUdpMcastAddr": "234.56.78.8", - "keycloak.connectionsInfinispan.nodeName": "node1", - "keycloak.connectionsInfinispan.clustered": "${keycloak.connectionsInfinispan.clustered:true}" + "keycloak.cacheEmbedded.nodeName": "node1" } @@ -264,9 +262,7 @@ node2 ${undertow.remote} { - "keycloak.connectionsInfinispan.jgroupsUdpMcastAddr": "234.56.78.8", - "keycloak.connectionsInfinispan.nodeName": "node2", - "keycloak.connectionsInfinispan.clustered": "${keycloak.connectionsInfinispan.clustered:true}" + "keycloak.cacheEmbedded.nodeName": "node2" } @@ -327,9 +323,7 @@ ha 5005 { - "keycloak.connectionsInfinispan.jgroupsUdpMcastAddr": "234.56.78.8", - "keycloak.connectionsInfinispan.nodeName": "node1", - "keycloak.connectionsInfinispan.clustered": "${keycloak.connectionsInfinispan.clustered:true}" + "keycloak.cacheEmbedded.nodeName": "node1" } -Xms512m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=512m -Djava.net.preferIPv4Stack=true @@ -351,9 +345,7 @@ ha 5006 { - "keycloak.connectionsInfinispan.jgroupsUdpMcastAddr": "234.56.78.8", - "keycloak.connectionsInfinispan.nodeName": "node2", - "keycloak.connectionsInfinispan.clustered": "${keycloak.connectionsInfinispan.clustered:true}" + "keycloak.cacheEmbedded.nodeName": "node2" } -Xms512m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=512m -Djava.net.preferIPv4Stack=true diff --git a/testsuite/integration-arquillian/tests/pom.xml b/testsuite/integration-arquillian/tests/pom.xml index 360095162c6..1a688ce74b0 100644 --- a/testsuite/integration-arquillian/tests/pom.xml +++ b/testsuite/integration-arquillian/tests/pom.xml @@ -585,7 +585,6 @@ ${keycloak.cacheRemote.port} ${keycloak.cacheRemote.port.2} ${keycloak.cacheRemote.hostname} - ${keycloak.connectionsInfinispan.sessionsOwners} ${keycloak.testsuite.logging.pattern} ${keycloak.connectionsJpa.url.crossdc} @@ -718,7 +717,6 @@ false true true - 2 @@ -1328,8 +1326,6 @@ true true - - 2 diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Infinispan.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Infinispan.java index 4bec57507a8..83bcefd469c 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Infinispan.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/Infinispan.java @@ -1,13 +1,13 @@ /* * Copyright 2020 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. @@ -16,6 +16,9 @@ */ package org.keycloak.testsuite.model.parameters; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + import com.google.common.collect.ImmutableSet; import org.keycloak.cluster.infinispan.InfinispanClusterProviderFactory; import org.keycloak.connections.infinispan.InfinispanConnectionProviderFactory; @@ -34,6 +37,7 @@ import org.keycloak.models.cache.authorization.CachedStoreFactorySpi; import org.keycloak.models.cache.infinispan.InfinispanCacheRealmProviderFactory; import org.keycloak.models.cache.infinispan.InfinispanUserCacheProviderFactory; import org.keycloak.models.cache.infinispan.authorization.InfinispanCacheStoreFactoryProviderFactory; +import org.keycloak.models.cache.infinispan.organization.InfinispanOrganizationProviderFactory; import org.keycloak.models.session.UserSessionPersisterSpi; import org.keycloak.models.sessions.infinispan.InfinispanAuthenticationSessionProviderFactory; import org.keycloak.models.sessions.infinispan.InfinispanSingleUseObjectProviderFactory; @@ -44,16 +48,18 @@ import org.keycloak.provider.Spi; import org.keycloak.sessions.AuthenticationSessionSpi; import org.keycloak.sessions.StickySessionEncoderProviderFactory; import org.keycloak.sessions.StickySessionEncoderSpi; +import org.keycloak.spi.infinispan.CacheEmbeddedConfigProviderFactory; +import org.keycloak.spi.infinispan.CacheEmbeddedConfigProviderSpi; +import org.keycloak.spi.infinispan.JGroupsCertificateProviderFactory; +import org.keycloak.spi.infinispan.JGroupsCertificateProviderSpi; +import org.keycloak.spi.infinispan.impl.embedded.DefaultCacheEmbeddedConfigProviderFactory; +import org.keycloak.storage.configuration.ServerConfigStorageProviderFactory; +import org.keycloak.storage.configuration.ServerConfigurationStorageProviderSpi; import org.keycloak.testsuite.model.Config; import org.keycloak.testsuite.model.KeycloakModelParameters; import org.keycloak.timer.TimerProviderFactory; -import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; -import org.keycloak.models.cache.infinispan.organization.InfinispanOrganizationProviderFactory; - /** - * * @author hmlnarik */ public class Infinispan extends KeycloakModelParameters { @@ -61,44 +67,46 @@ public class Infinispan extends KeycloakModelParameters { private static final AtomicInteger NODE_COUNTER = new AtomicInteger(); static final Set> ALLOWED_SPIS = ImmutableSet.>builder() - .add(AuthenticationSessionSpi.class) - .add(CacheRealmProviderSpi.class) - .add(CachedStoreFactorySpi.class) - .add(CacheUserProviderSpi.class) - .add(InfinispanConnectionSpi.class) - .add(StickySessionEncoderSpi.class) - .add(UserSessionPersisterSpi.class) - .add(SingleUseObjectSpi.class) - .add(PublicKeyStorageSpi.class) - .add(CachePublicKeyProviderSpi.class) - - .build(); + .add(AuthenticationSessionSpi.class) + .add(CacheRealmProviderSpi.class) + .add(CachedStoreFactorySpi.class) + .add(CacheUserProviderSpi.class) + .add(InfinispanConnectionSpi.class) + .add(StickySessionEncoderSpi.class) + .add(UserSessionPersisterSpi.class) + .add(SingleUseObjectSpi.class) + .add(PublicKeyStorageSpi.class) + .add(CachePublicKeyProviderSpi.class) + .add(CacheEmbeddedConfigProviderSpi.class) + .add(JGroupsCertificateProviderSpi.class) + .add(ServerConfigurationStorageProviderSpi.class) + .build(); static final Set> ALLOWED_FACTORIES = ImmutableSet.>builder() - .add(InfinispanAuthenticationSessionProviderFactory.class) - .add(InfinispanCacheRealmProviderFactory.class) - .add(InfinispanCacheStoreFactoryProviderFactory.class) - .add(InfinispanClusterProviderFactory.class) - .add(InfinispanConnectionProviderFactory.class) - .add(InfinispanUserCacheProviderFactory.class) - .add(InfinispanUserSessionProviderFactory.class) - .add(InfinispanUserLoginFailureProviderFactory.class) - .add(InfinispanSingleUseObjectProviderFactory.class) - .add(StickySessionEncoderProviderFactory.class) - .add(TimerProviderFactory.class) - .add(InfinispanPublicKeyStorageProviderFactory.class) - .add(InfinispanCachePublicKeyProviderFactory.class) - .add(InfinispanOrganizationProviderFactory.class) - .build(); + .add(InfinispanAuthenticationSessionProviderFactory.class) + .add(InfinispanCacheRealmProviderFactory.class) + .add(InfinispanCacheStoreFactoryProviderFactory.class) + .add(InfinispanClusterProviderFactory.class) + .add(InfinispanConnectionProviderFactory.class) + .add(InfinispanUserCacheProviderFactory.class) + .add(InfinispanUserSessionProviderFactory.class) + .add(InfinispanUserLoginFailureProviderFactory.class) + .add(InfinispanSingleUseObjectProviderFactory.class) + .add(StickySessionEncoderProviderFactory.class) + .add(TimerProviderFactory.class) + .add(InfinispanPublicKeyStorageProviderFactory.class) + .add(InfinispanCachePublicKeyProviderFactory.class) + .add(InfinispanOrganizationProviderFactory.class) + .add(CacheEmbeddedConfigProviderFactory.class) + .add(JGroupsCertificateProviderFactory.class) + .add(ServerConfigStorageProviderFactory.class) + .build(); @Override public void updateConfig(Config cf) { cf.spi("connectionsInfinispan") .provider("default") - .config("embedded", "true") - .config("clustered", "true") .config("useKeycloakTimeService", "true") - .config("nodeName", "node-" + NODE_COUNTER.incrementAndGet()) .spi(UserLoginFailureSpi.NAME) .provider(InfinispanUtils.EMBEDDED_PROVIDER_ID) .config("stalledTimeoutInSeconds", "10") @@ -106,8 +114,12 @@ public class Infinispan extends KeycloakModelParameters { .provider(InfinispanUtils.EMBEDDED_PROVIDER_ID) .config("sessionPreloadStalledTimeoutInSeconds", "10") .config("offlineSessionCacheEntryLifespanOverride", "43200") - .config("offlineClientSessionCacheEntryLifespanOverride", "43200") - ; + .config("offlineClientSessionCacheEntryLifespanOverride", "43200"); + cf.spi(CacheEmbeddedConfigProviderSpi.SPI_NAME) + .provider(DefaultCacheEmbeddedConfigProviderFactory.PROVIDER_ID) + .config(DefaultCacheEmbeddedConfigProviderFactory.CONFIG, "test-ispn.xml") + .config(DefaultCacheEmbeddedConfigProviderFactory.NODE_NAME, "node-" + NODE_COUNTER.incrementAndGet()); + } public Infinispan() { diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/RemoteInfinispan.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/RemoteInfinispan.java index 6f4ca12c0ac..376563ff9eb 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/RemoteInfinispan.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/parameters/RemoteInfinispan.java @@ -31,7 +31,9 @@ import org.keycloak.models.sessions.infinispan.remote.RemoteStickySessionEncoder import org.keycloak.models.sessions.infinispan.remote.RemoteUserLoginFailureProviderFactory; import org.keycloak.models.sessions.infinispan.remote.RemoteUserSessionProviderFactory; import org.keycloak.provider.ProviderFactory; +import org.keycloak.spi.infinispan.CacheEmbeddedConfigProviderSpi; import org.keycloak.spi.infinispan.CacheRemoteConfigProviderSpi; +import org.keycloak.spi.infinispan.impl.embedded.DefaultCacheEmbeddedConfigProviderFactory; import org.keycloak.spi.infinispan.impl.remote.DefaultCacheRemoteConfigProviderFactory; import org.keycloak.testsuite.model.Config; import org.keycloak.testsuite.model.HotRodServerRule; @@ -70,16 +72,16 @@ public class RemoteInfinispan extends KeycloakModelParameters { var siteName = siteName(nodeCounter); cf.spi("connectionsInfinispan") .provider("default") - .config("embedded", "true") - .config("clustered", "true") - .config("useKeycloakTimeService", "true") - .config("nodeName", "node-" + nodeCounter) - .config("siteName", siteName) - .config("jgroupsUdpMcastAddr", mcastAddr(nodeCounter)); + .config("useKeycloakTimeService", "true"); cf.spi(CacheRemoteConfigProviderSpi.SPI_NAME) .provider(DefaultCacheRemoteConfigProviderFactory.PROVIDER_ID) .config(DefaultCacheRemoteConfigProviderFactory.HOSTNAME, "localhost") .config(DefaultCacheRemoteConfigProviderFactory.PORT, siteName.equals("site-2") ? "11333" : "11222"); + cf.spi(CacheEmbeddedConfigProviderSpi.SPI_NAME) + .provider(DefaultCacheEmbeddedConfigProviderFactory.PROVIDER_ID) + .config(DefaultCacheEmbeddedConfigProviderFactory.CONFIG, "test-ispn.xml") + .config(DefaultCacheEmbeddedConfigProviderFactory.SITE_NAME, siteName(NODE_COUNTER.get())) + .config(DefaultCacheEmbeddedConfigProviderFactory.NODE_NAME, "node-" + NODE_COUNTER.incrementAndGet()); } } diff --git a/testsuite/model/src/test/resources/test-ispn.xml b/testsuite/model/src/test/resources/test-ispn.xml new file mode 100644 index 00000000000..b305b4cd68e --- /dev/null +++ b/testsuite/model/src/test/resources/test-ispn.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/testsuite/utils/src/main/resources/META-INF/keycloak-server.json b/testsuite/utils/src/main/resources/META-INF/keycloak-server.json index d68d68b55e7..b3fe0ca7c5a 100755 --- a/testsuite/utils/src/main/resources/META-INF/keycloak-server.json +++ b/testsuite/utils/src/main/resources/META-INF/keycloak-server.json @@ -123,16 +123,9 @@ } }, - "connectionsInfinispan": { + "cacheEmbedded": { "default": { - "jgroupsUdpMcastAddr": "${keycloak.connectionsInfinispan.jgroupsUdpMcastAddr:234.56.78.90}", - "nodeName": "${keycloak.connectionsInfinispan.nodeName,jboss.node.name:}", - "siteName": "${keycloak.connectionsInfinispan.siteName:}", - "clustered": "${keycloak.connectionsInfinispan.clustered:}", - "async": "${keycloak.connectionsInfinispan.async:}", - "sessionsOwners": "${keycloak.connectionsInfinispan.sessionsOwners:}", - "l1Lifespan": "${keycloak.connectionsInfinispan.l1Lifespan:}", - "embedded": "${keycloak.connectionsInfinispan.embedded:true}" + "nodeName": "${keycloak.cacheEmbedded.nodeName,jboss.node.name:}" } },