mirror of
https://github.com/keycloak/keycloak.git
synced 2026-02-20 22:29:06 -06:00
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:
@@ -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}.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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> {
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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`
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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:}"
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
115
testsuite/model/src/test/resources/test-ispn.xml
Normal file
115
testsuite/model/src/test/resources/test-ispn.xml
Normal 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>
|
||||
@@ -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:}"
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user