Create CacheEmbeddedConfigProvider

Closes #38497

Signed-off-by: Pedro Ruivo <pruivo@redhat.com>
Signed-off-by: Alexander Schwartz <aschwart@redhat.com>
Co-authored-by: Alexander Schwartz <aschwart@redhat.com>
This commit is contained in:
Pedro Ruivo
2025-04-28 12:00:53 +01:00
committed by GitHub
parent 660217dc41
commit eafe08a73a
44 changed files with 1341 additions and 1402 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -88,6 +88,11 @@
<artifactId>infinispan-component-processor</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>

View File

@@ -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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
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<ManagedCacheManagerProvider> 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<Class<? extends Provider>> 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);
}
}
}

View File

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

View File

@@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
@@ -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
*
* <p>
* 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> T replaceComponent(EmbeddedCacheManager cacheMgr, Class<T> componentType, T replacementComponent, boolean rewire) {

View File

@@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@@ -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
*/

View File

@@ -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<JGroupsStackConfigurator> stackConfiguratorList;
private JGroupsConfigurator(ConfigurationBuilderHolder holder, List<JGroupsStackConfigurator> stackConfiguratorList) {
this.holder = holder;
this.stackConfiguratorList = stackConfiguratorList;
}
private static void createJdbcPingConfigurator(ConfigurationBuilderHolder holder, List<JGroupsStackConfigurator> 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<JGroupsStackConfigurator> 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<JGroupsStackConfigurator>(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);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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}.
* <p>
* 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<CacheEmbeddedConfigProvider> getProviderClass() {
return CacheEmbeddedConfigProvider.class;
}
@Override
public Class<CacheEmbeddedConfigProviderFactory> getProviderFactoryClass() {
return CacheEmbeddedConfigProviderFactory.class;
}
}

View File

@@ -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.
* <p>
* 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<String, Supplier<ConfigurationBuilder>> DEFAULT_CONFIGS = Map.of(CRL_CACHE_NAME, CacheConfigurator::getCrlCacheConfig);
private static final Supplier<ConfigurationBuilder> 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.
* <p>
* 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<String> 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<String> 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;
}
}

View File

@@ -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}.
* <p>
* It builds a {@link ConfigurationBuilderHolder} based on the Keycloak configuration.
* <p>
* 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<ProviderConfigProperty> 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<Class<? extends Provider>> 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();
}
}

View File

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

View File

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

View File

@@ -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
org.keycloak.spi.infinispan.impl.embedded.DefaultCacheEmbeddedConfigProviderFactory

View File

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

View File

@@ -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<SyntheticBeanBuildItem> syntheticBeanBuildItems) {
syntheticBeanBuildItems.produce(SyntheticBeanBuildItem.configure(CacheManagerFactory.class)
.scope(ApplicationScoped.class)
.unremovable()
.setRuntimeInit()
.runtimeValue(recorder.createCacheInitializer()).done());
}
}

View File

@@ -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<CacheManagerFactory> 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<String, Object> propertyCollector) {
try (InstanceHandle<AgroalDataSource> instance = Arc.container().instance(
AgroalDataSource.class, new DataSource() {
@Override public Class<? extends Annotation> annotationType() {
return DataSource.class;
}
return propertyCollector -> {
try (InstanceHandle<AgroalDataSource> instance = Arc.container().instance(
AgroalDataSource.class, new DataSource() {
@Override public Class<? extends Annotation> 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<String, Object> 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) {

View File

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

View File

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

View File

@@ -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<String, Supplier<ConfigurationBuilder>> DEFAULT_CONFIGS = Map.of(
CRL_CACHE_NAME, InfinispanUtil::getCrlCacheConfig
);
private static final Supplier<ConfigurationBuilder> 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.
* <p />
* 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<String> 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<Integer> 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);
}
}

View File

@@ -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 <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
@SuppressWarnings({"unchecked", "resource"})
public final class QuarkusCacheManagerProvider implements ManagedCacheManagerProvider {
@Override
public <C> C getEmbeddedCacheManager(KeycloakSession keycloakSession, Config.Scope config) {
return (C) Arc.container().instance(CacheManagerFactory.class).get().getOrCreateEmbeddedCacheManager(keycloakSession);
}
}

View File

@@ -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 <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
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<ProviderConfigProperty> getConfigMetadata() {
return ProviderConfigurationBuilder.create()
.property()
.name("site-name")
.helpText("Site name for multi-site deployments")
.type("string")
.add()
.build();
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public interface ManagedCacheManagerProvider {
<C> 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> C getRemoteCacheManager(Config.Scope config) {
return null;
}
}

View File

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

View File

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

View File

@@ -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:}"
}
},

View File

@@ -245,9 +245,7 @@
<property name="route">node1</property>
<property name="remoteMode">${undertow.remote}</property>
<property name="keycloakConfigPropertyOverrides">{
"keycloak.connectionsInfinispan.jgroupsUdpMcastAddr": "234.56.78.8",
"keycloak.connectionsInfinispan.nodeName": "node1",
"keycloak.connectionsInfinispan.clustered": "${keycloak.connectionsInfinispan.clustered:true}"
"keycloak.cacheEmbedded.nodeName": "node1"
}
</property>
</configuration>
@@ -264,9 +262,7 @@
<property name="route">node2</property>
<property name="remoteMode">${undertow.remote}</property>
<property name="keycloakConfigPropertyOverrides">{
"keycloak.connectionsInfinispan.jgroupsUdpMcastAddr": "234.56.78.8",
"keycloak.connectionsInfinispan.nodeName": "node2",
"keycloak.connectionsInfinispan.clustered": "${keycloak.connectionsInfinispan.clustered:true}"
"keycloak.cacheEmbedded.nodeName": "node2"
}
</property>
</configuration>
@@ -327,9 +323,7 @@
<property name="profile">ha</property>
<property name="debugPort">5005</property>
<property name="keycloakConfigPropertyOverrides">{
"keycloak.connectionsInfinispan.jgroupsUdpMcastAddr": "234.56.78.8",
"keycloak.connectionsInfinispan.nodeName": "node1",
"keycloak.connectionsInfinispan.clustered": "${keycloak.connectionsInfinispan.clustered:true}"
"keycloak.cacheEmbedded.nodeName": "node1"
}
</property>
<property name="javaOpts">-Xms512m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=512m -Djava.net.preferIPv4Stack=true</property>
@@ -351,9 +345,7 @@
<property name="profile">ha</property>
<property name="debugPort">5006</property>
<property name="keycloakConfigPropertyOverrides">{
"keycloak.connectionsInfinispan.jgroupsUdpMcastAddr": "234.56.78.8",
"keycloak.connectionsInfinispan.nodeName": "node2",
"keycloak.connectionsInfinispan.clustered": "${keycloak.connectionsInfinispan.clustered:true}"
"keycloak.cacheEmbedded.nodeName": "node2"
}
</property>
<property name="javaOpts">-Xms512m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=512m -Djava.net.preferIPv4Stack=true</property>

View File

@@ -585,7 +585,6 @@
<keycloak.cacheRemote.port>${keycloak.cacheRemote.port}</keycloak.cacheRemote.port>
<keycloak.cacheRemote.port.2>${keycloak.cacheRemote.port.2}</keycloak.cacheRemote.port.2>
<keycloak.cacheRemote.hostname>${keycloak.cacheRemote.hostname}</keycloak.cacheRemote.hostname>
<keycloak.connectionsInfinispan.sessionsOwners>${keycloak.connectionsInfinispan.sessionsOwners}</keycloak.connectionsInfinispan.sessionsOwners>
<keycloak.testsuite.logging.pattern>${keycloak.testsuite.logging.pattern}</keycloak.testsuite.logging.pattern>
<keycloak.connectionsJpa.url.crossdc>${keycloak.connectionsJpa.url.crossdc}</keycloak.connectionsJpa.url.crossdc>
@@ -718,7 +717,6 @@
<auth.server.quarkus.skip.unpack>false</auth.server.quarkus.skip.unpack>
<auth.server.undertow.skip.unpack>true</auth.server.undertow.skip.unpack>
<auth.server.jboss.skip.unpack>true</auth.server.jboss.skip.unpack>
<keycloak.connectionsInfinispan.sessionsOwners>2</keycloak.connectionsInfinispan.sessionsOwners>
</properties>
<build>
<plugins>
@@ -1328,8 +1326,6 @@
<auth.server.undertow.cluster>true</auth.server.undertow.cluster>
<auth.server.jboss.skip.unpack>true</auth.server.jboss.skip.unpack>
<keycloak.connectionsInfinispan.sessionsOwners>2</keycloak.connectionsInfinispan.sessionsOwners>
</properties>
<build>
<plugins>

View File

@@ -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<Class<? extends Spi>> ALLOWED_SPIS = ImmutableSet.<Class<? extends Spi>>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<Class<? extends ProviderFactory>> ALLOWED_FACTORIES = ImmutableSet.<Class<? extends ProviderFactory>>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() {

View File

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

View File

@@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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.
-->
<infinispan
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="urn:infinispan:config:15.0 http://www.infinispan.org/schemas/infinispan-config-15.0.xsd
urn:org:jgroups http://www.jgroups.org/schema/jgroups-5.3.xsd"
xmlns="urn:infinispan:config:15.0"
xmlns:ispn="urn:infinispan:config:15.0">
<jgroups>
<stack name="test" extends="tcp">
<!-- no network traffic as all messages are handled inside the JVM -->
<SHARED_LOOPBACK xmlns="urn:org:jgroups" ispn:stack.combine="REPLACE" ispn:stack.position="TCP"
thread_pool.use_virtual_threads="true"
bundler_type="no-bundler"/>
<SHARED_LOOPBACK_PING xmlns="urn:org:jgroups" ispn:stack.combine="REPLACE" ispn:stack.position="MPING"/>
<!-- in JVM cluster, no failure detection, no flow control, no fragmentation. -->
<RED xmlns="urn:org:jgroups" ispn:stack.combine="REMOVE"/>
<FD_SOCK2 xmlns="urn:org:jgroups" ispn:stack.combine="REMOVE"/>
<UFC xmlns="urn:org:jgroups" ispn:stack.combine="REMOVE"/>
<MFC xmlns="urn:org:jgroups" ispn:stack.combine="REMOVE"/>
<FRAG4 xmlns="urn:org:jgroups" ispn:stack.combine="REMOVE"/>
</stack>
</jgroups>
<cache-container name="keycloak">
<transport lock-timeout="60000" stack="test"/>
<local-cache name="realms" simple-cache="true">
<encoding>
<key media-type="application/x-java-object"/>
<value media-type="application/x-java-object"/>
</encoding>
<memory max-count="10000"/>
</local-cache>
<local-cache name="users" simple-cache="true">
<encoding>
<key media-type="application/x-java-object"/>
<value media-type="application/x-java-object"/>
</encoding>
<memory max-count="10000"/>
</local-cache>
<distributed-cache name="sessions" owners="1">
<expiration lifespan="-1"/>
<memory max-count="10000"/>
</distributed-cache>
<distributed-cache name="authenticationSessions" owners="2">
<expiration lifespan="-1"/>
</distributed-cache>
<distributed-cache name="offlineSessions" owners="1">
<expiration lifespan="-1"/>
<memory max-count="10000"/>
</distributed-cache>
<distributed-cache name="clientSessions" owners="1">
<expiration lifespan="-1"/>
<memory max-count="10000"/>
</distributed-cache>
<distributed-cache name="offlineClientSessions" owners="1">
<expiration lifespan="-1"/>
<memory max-count="10000"/>
</distributed-cache>
<distributed-cache name="loginFailures" owners="2">
<expiration lifespan="-1"/>
</distributed-cache>
<local-cache name="authorization" simple-cache="true">
<encoding>
<key media-type="application/x-java-object"/>
<value media-type="application/x-java-object"/>
</encoding>
<memory max-count="10000"/>
</local-cache>
<replicated-cache name="work">
<expiration lifespan="-1"/>
</replicated-cache>
<local-cache name="keys" simple-cache="true">
<encoding>
<key media-type="application/x-java-object"/>
<value media-type="application/x-java-object"/>
</encoding>
<expiration max-idle="3600000"/>
<memory max-count="1000"/>
</local-cache>
<local-cache name="crl" simple-cache="true">
<encoding>
<key media-type="application/x-java-object"/>
<value media-type="application/x-java-object"/>
</encoding>
<expiration lifespan="-1"/>
<memory max-count="1000"/>
</local-cache>
<distributed-cache name="actionTokens" owners="2">
<encoding>
<key media-type="application/x-java-object"/>
<value media-type="application/x-java-object"/>
</encoding>
<expiration max-idle="-1" lifespan="-1" interval="300000"/>
<memory max-count="-1"/>
</distributed-cache>
</cache-container>
</infinispan>

View File

@@ -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:}"
}
},