Create CacheRemoteConfigProvider (#38570)

Closes #38496

Signed-off-by: Pedro Ruivo <pruivo@redhat.com>
This commit is contained in:
Pedro Ruivo 2025-04-16 16:08:43 +01:00 committed by GitHub
parent 60fb7a5fa7
commit 636fffe0bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 847 additions and 274 deletions

View file

@ -17,8 +17,8 @@
package org.keycloak.connections.infinispan;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@ -46,17 +46,24 @@ 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;
import org.keycloak.models.cache.infinispan.events.RealmRemovedEvent;
import org.keycloak.models.cache.infinispan.events.RealmUpdatedEvent;
import org.keycloak.models.sessions.infinispan.query.ClientSessionQueries;
import org.keycloak.models.sessions.infinispan.query.UserSessionQueries;
import org.keycloak.models.sessions.infinispan.remote.RemoteInfinispanAuthenticationSessionProviderFactory;
import org.keycloak.models.sessions.infinispan.remote.RemoteUserLoginFailureProviderFactory;
import org.keycloak.models.utils.KeycloakModelUtils;
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.spi.infinispan.CacheRemoteConfigProvider;
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.ACTION_TOKEN_CACHE;
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME;
@ -64,7 +71,6 @@ import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.A
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.CLUSTERED_CACHE_NAMES;
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;
@ -83,7 +89,6 @@ import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.U
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.InfinispanConnectionProvider.skipSessionsCacheIfRequired;
import static org.keycloak.connections.infinispan.InfinispanUtil.configureTransport;
import static org.keycloak.connections.infinispan.InfinispanUtil.createCacheConfigurationBuilder;
import static org.keycloak.connections.infinispan.InfinispanUtil.getActionTokenCacheConfig;
@ -153,6 +158,10 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
cacheManager.stop();
}
});
if (remoteCacheManager != null) {
remoteCacheManager.close();
remoteCacheManager = null;
}
}
@Override
@ -175,67 +184,68 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
}
protected void lazyInit(KeycloakSession keycloakSession) {
if (cacheManager == null) {
synchronized (this) {
if (cacheManager == null) {
EmbeddedCacheManager managedCacheManager = null;
RemoteCacheManager rcm = 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);
if (InfinispanUtils.isRemoteInfinispan()) {
rcm = provider.getRemoteCacheManager(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();
if (InfinispanUtils.isRemoteInfinispan()) {
rcm = initRemote();
}
} else {
localCacheManager = initContainerManaged(managedCacheManager);
}
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 = rcm;
}
if (cacheManager != null) {
return;
}
synchronized (this) {
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);
}
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);
}
}
private RemoteCacheManager initRemote() {
var host = config.get("remoteStoreHost", "127.0.0.1");
var port = config.getInt("remoteStorePort", 11222);
org.infinispan.client.hotrod.configuration.ConfigurationBuilder builder = new org.infinispan.client.hotrod.configuration.ConfigurationBuilder();
builder.addServer().host(host).port(port);
builder.connectionPool().maxActive(16).exhaustedAction(org.infinispan.client.hotrod.configuration.ExhaustedAction.CREATE_NEW);
Marshalling.configure(builder);
RemoteCacheManager remoteCacheManager = new RemoteCacheManager(builder.build());
// establish connection to all caches
skipSessionsCacheIfRequired(Arrays.stream(CLUSTERED_CACHE_NAMES)).forEach(remoteCacheManager::getCache);
return remoteCacheManager;
protected RemoteCacheManager createRemoteCacheManager(KeycloakSession session) {
var remoteConfig = session.getProvider(CacheRemoteConfigProvider.class).configuration();
if (remoteConfig.isEmpty()) {
logger.debug("Remote Cache feature is disabled");
return null;
}
logger.debug("Remote Cache feature is enabled");
var rcm = new RemoteCacheManager(remoteConfig.get());
// upload the schema before trying to access the caches
// not caching the list; it is only used during startup
var entities = List.of(
new KeycloakIndexSchemaUtil.IndexedEntity(RemoteUserLoginFailureProviderFactory.PROTO_ENTITY, LOGIN_FAILURE_CACHE_NAME),
new KeycloakIndexSchemaUtil.IndexedEntity(RemoteInfinispanAuthenticationSessionProviderFactory.PROTO_ENTITY, AUTHENTICATION_SESSIONS_CACHE_NAME),
new KeycloakIndexSchemaUtil.IndexedEntity(ClientSessionQueries.CLIENT_SESSION, CLIENT_SESSION_CACHE_NAME),
new KeycloakIndexSchemaUtil.IndexedEntity(ClientSessionQueries.CLIENT_SESSION, OFFLINE_CLIENT_SESSION_CACHE_NAME),
new KeycloakIndexSchemaUtil.IndexedEntity(UserSessionQueries.USER_SESSION, USER_SESSION_CACHE_NAME),
new KeycloakIndexSchemaUtil.IndexedEntity(UserSessionQueries.USER_SESSION, OFFLINE_USER_SESSION_CACHE_NAME)
);
KeycloakIndexSchemaUtil.uploadAndReindexCaches(rcm, KeycloakModelSchema.INSTANCE, entities);
return rcm;
}
protected EmbeddedCacheManager initContainerManaged(EmbeddedCacheManager cacheManager) {
@ -438,6 +448,6 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
@Override
public Set<Class<? extends Provider>> dependsOn() {
return Set.of(JpaConnectionProvider.class);
return Set.of(JpaConnectionProvider.class, CacheRemoteConfigProvider.class);
}
}

View file

@ -17,6 +17,7 @@
package org.keycloak.marshalling;
import java.lang.invoke.MethodHandles;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@ -24,14 +25,24 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.infinispan.api.annotations.indexing.model.Values;
import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.client.hotrod.RemoteCacheManager;
import org.infinispan.client.hotrod.RemoteCacheManagerAdmin;
import org.infinispan.commons.internal.InternalCacheNames;
import org.infinispan.protostream.GeneratedSchema;
import org.infinispan.protostream.config.Configuration;
import org.infinispan.protostream.descriptors.AnnotationElement;
import org.infinispan.protostream.descriptors.Descriptor;
import org.infinispan.protostream.descriptors.FieldDescriptor;
import org.infinispan.protostream.descriptors.FileDescriptor;
import org.infinispan.protostream.impl.AnnotatedDescriptorImpl;
import org.infinispan.query.remote.client.ProtobufMetadataManagerConstants;
import org.jboss.logging.Logger;
public class KeycloakIndexSchemaUtil {
private static final Logger logger = Logger.getLogger(MethodHandles.lookup().lookupClass());
// Basic annotation data
private static final String BASIC_ANNOTATION = "Basic";
private static final String NAME_ATTRIBUTE = "name";
@ -44,6 +55,87 @@ public class KeycloakIndexSchemaUtil {
// we only use Basic annotation, we may need to add others in the future.
private static final List<String> INDEX_ANNOTATION = List.of(BASIC_ANNOTATION);
/**
* Uploads the {@link GeneratedSchema} to the Infinispan cluster.
* <p>
* If indexing is enabled for one or more entities present in the {@link GeneratedSchema}, users may add a list of
* entities, and the caches where they live. This method will update the indexing schema and perform the reindexing
* for the new schema. Note that reindexing may be an expensive operation, depending on the amount of data.
*
* @param remoteCacheManager The {@link RemoteCacheManager} connected to the Infinispan server.
* @param schema The {@link GeneratedSchema} instance to upload.
* @param indexedEntities The {@link List} of indexed entities and their caches. Duplicates are allowed if the
* same entity is stored in multiple caches.
* @throws NullPointerException if {@code remoteCacheManager} or {@code schema} is null.
*/
public static void uploadAndReindexCaches(RemoteCacheManager remoteCacheManager, GeneratedSchema schema, List<IndexedEntity> indexedEntities) {
var key = schema.getProtoFileName();
var current = schema.getProtoFile();
var protostreamMetadataCache = remoteCacheManager.<String, String>getCache(InternalCacheNames.PROTOBUF_METADATA_CACHE_NAME);
var stored = protostreamMetadataCache.getWithMetadata(key);
if (stored == null) {
if (protostreamMetadataCache.putIfAbsent(key, current) == null) {
logger.info("Infinispan ProtoStream schema uploaded for the first time.");
} else {
logger.info("Failed to update Infinispan ProtoStream schema. Assumed it was updated by other Keycloak server.");
}
checkForProtoSchemaErrors(protostreamMetadataCache);
return;
}
if (Objects.equals(stored.getValue(), current)) {
logger.info("Infinispan ProtoStream schema is up to date!");
return;
}
if (protostreamMetadataCache.replaceWithVersion(key, current, stored.getVersion())) {
logger.info("Infinispan ProtoStream schema successful updated.");
reindexCaches(remoteCacheManager, stored.getValue(), current, indexedEntities);
} else {
logger.info("Failed to update Infinispan ProtoStream schema. Assumed it was updated by other Keycloak server.");
}
checkForProtoSchemaErrors(protostreamMetadataCache);
}
private static void checkForProtoSchemaErrors(RemoteCache<String, String> protostreamMetadataCache) {
var errors = protostreamMetadataCache.get(ProtobufMetadataManagerConstants.ERRORS_KEY_SUFFIX);
if (errors == null) {
return;
}
for (String errorFile : errors.split("\n")) {
logger.errorf("%nThere was an error in proto file: %s%nError message: %s%nCurrent proto schema: %s%n",
errorFile,
protostreamMetadataCache.get(errorFile + ProtobufMetadataManagerConstants.ERRORS_KEY_SUFFIX),
protostreamMetadataCache.get(errorFile));
}
}
private static void reindexCaches(RemoteCacheManager remoteCacheManager, String oldSchema, String newSchema, List<IndexedEntity> indexedEntities) {
if (indexedEntities == null || indexedEntities.isEmpty()) {
return;
}
var oldPS = KeycloakModelSchema.parseProtoSchema(oldSchema);
var newPS = KeycloakModelSchema.parseProtoSchema(newSchema);
var admin = remoteCacheManager.administration();
indexedEntities.stream()
.filter(Objects::nonNull)
.filter(indexedEntity -> isEntityChanged(oldPS, newPS, indexedEntity.entity()))
.map(IndexedEntity::cache)
.distinct()
.forEach(cacheName -> updateSchemaAndReIndexCache(admin, cacheName));
}
private static boolean isEntityChanged(FileDescriptor oldSchema, FileDescriptor newSchema, String entity) {
var v1 = KeycloakModelSchema.findEntity(oldSchema, entity);
var v2 = KeycloakModelSchema.findEntity(newSchema, entity);
return v1.isPresent() && v2.isPresent() && KeycloakIndexSchemaUtil.isIndexSchemaChanged(v1.get(), v2.get());
}
private static void updateSchemaAndReIndexCache(RemoteCacheManagerAdmin admin, String cacheName) {
admin.updateIndexSchema(cacheName);
admin.reindexCache(cacheName);
}
/**
* Adds the annotations to the ProtoStream parser.
*/
@ -72,7 +164,8 @@ public class KeycloakIndexSchemaUtil {
}
/**
* Compares two entities and returns {@code true} if any indexing related annotation were changed, added or removed.
* Compares two entities and returns {@code true} if any indexing related annotation were changed, added or
* removed.
*/
public static boolean isIndexSchemaChanged(Descriptor oldDescriptor, Descriptor newDescriptor) {
var allFields = Stream.concat(
@ -157,4 +250,10 @@ public class KeycloakIndexSchemaUtil {
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getValue().getValue()));
}
public record IndexedEntity(String entity, String cache) {
public IndexedEntity {
Objects.requireNonNull(entity);
Objects.requireNonNull(cache);
}
}
}

View file

@ -0,0 +1,44 @@
/*
* 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 java.util.Optional;
import org.infinispan.client.hotrod.configuration.Configuration;
import org.keycloak.provider.Provider;
/**
* A provider to create a configuration to the Hot Rod client.
*/
public interface CacheRemoteConfigProvider extends Provider {
/**
* Creates the {@link Configuration} for the Hot Rod client.
* <p>
* The optional signal if a Hot Rod client should be instantiated and started. If present, it assumes an external
* Infinispan cluster is ready and online, otherwise Keycloak fails to start.
*
* @return The {@link Configuration} for the Hot Rod client.
*/
Optional<Configuration> configuration();
@Override
default void close() {
//no-op
}
}

View file

@ -0,0 +1,28 @@
/*
* 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;
/**
* A factory for {@link CacheRemoteConfigProvider}
*
* @see CacheRemoteConfigProvider
*/
public interface CacheRemoteConfigProviderFactory extends ProviderFactory<CacheRemoteConfigProvider> {
}

View file

@ -0,0 +1,48 @@
/*
* 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.Spi;
/**
* An SPI to generate the configuration for the Hot Rod client.
*/
public class CacheRemoteConfigProviderSpi implements Spi {
public static final String SPI_NAME = "cacheRemote";
@Override
public boolean isInternal() {
return true;
}
@Override
public String getName() {
return SPI_NAME;
}
@Override
public Class<CacheRemoteConfigProvider> getProviderClass() {
return CacheRemoteConfigProvider.class;
}
@Override
public Class<CacheRemoteConfigProviderFactory> getProviderFactoryClass() {
return CacheRemoteConfigProviderFactory.class;
}
}

View file

@ -0,0 +1,53 @@
/*
* Copyright 2025 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.spi.infinispan.impl;
import org.keycloak.config.Option;
import org.keycloak.provider.ProviderConfigurationBuilder;
/**
* Utility method for this package and subpackages
*/
public final class Util {
private Util() {
}
/**
* Copies the {@link Option} information into the {@link ProviderConfigurationBuilder}.
*
* @param builder The property to set/configure.
* @param name The desired property name.
* @param label The label of the property's argument.
* @param type The type of the property's value.
* @param option The source {@link Option} to gather the information.
* @param isSecret {@code true} if the property is a secret.
*/
public static void copyFromOption(ProviderConfigurationBuilder builder, String name, String label, String type, Option<?> option, boolean isSecret) {
var property = builder.property()
.name(name)
.helpText(option.getDescription())
.label(label)
.type(type)
.secret(isSecret);
option.getDefaultValue().ifPresent(property::defaultValue);
property.options(option.getExpectedValues());
property.add();
}
}

View file

@ -0,0 +1,358 @@
/*
* 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.remote;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.Properties;
import org.infinispan.client.hotrod.configuration.AuthenticationConfigurationBuilder;
import org.infinispan.client.hotrod.configuration.ClientIntelligence;
import org.infinispan.client.hotrod.configuration.Configuration;
import org.infinispan.client.hotrod.configuration.ConfigurationBuilder;
import org.infinispan.client.hotrod.configuration.ExhaustedAction;
import org.infinispan.client.hotrod.impl.ConfigurationProperties;
import org.infinispan.commons.dataconversion.MediaType;
import org.infinispan.configuration.cache.CacheMode;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.config.CachingOptions;
import org.keycloak.infinispan.util.InfinispanUtils;
import org.keycloak.marshalling.Marshalling;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import org.keycloak.spi.infinispan.CacheRemoteConfigProvider;
import org.keycloak.spi.infinispan.CacheRemoteConfigProviderFactory;
import javax.net.ssl.SSLContext;
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.CLUSTERED_CACHE_NAMES;
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.skipSessionsCacheIfRequired;
import static org.keycloak.spi.infinispan.impl.Util.copyFromOption;
import static org.wildfly.security.sasl.util.SaslMechanismInformation.Names.SCRAM_SHA_512;
/**
* The default implementation for {@link CacheRemoteConfigProviderFactory} and {@link CacheRemoteConfigProvider}.
* <p>
* It is used when an external Infinispan cluster is enabled.
*/
public class DefaultCacheRemoteConfigProviderFactory implements CacheRemoteConfigProviderFactory, CacheRemoteConfigProvider, EnvironmentDependentProviderFactory {
private static final String PROVIDER_ID = "default";
private static final Logger logger = Logger.getLogger(MethodHandles.lookup().lookupClass());
// configuration
private static final String PROPERTIES_FILE = "propertiesFile";
private static final String CLIENT_INTELLIGENCE = "clientIntelligence";
private static final String HOSTNAME = "hostname";
private static final String PORT = "port";
private static final String TLS_ENABLED = "tlsEnabled";
private static final String TLS_SNI_HOSTNAME = "tlsSniHostname";
private static final String USERNAME = "username";
private static final String PASSWORD = "password";
private static final String CONNECTION_POOL_MAX_ACTIVE = "connectionPoolMaxActive";
private static final String CONNECTION_POOL_EXHAUSTED_ACTION = "connectionPoolExhaustedAction";
private static final String AUTH_REALM = "authRealm";
private static final String SASL_MECHANISM = "saslMechanism";
// configuration defaults
private static final String CLIENT_INTELLIGENCE_DEFAULT = ClientIntelligence.getDefault().name();
private static final int CONNECTION_POOL_MAX_ACTIVE_DEFAULT = 16;
private static final String CONNECTION_POOL_EXHAUSTED_ACTION_DEFAULT = ExhaustedAction.CREATE_NEW.name();
private static final String SASL_MECHANISM_DEFAULT = SCRAM_SHA_512;
private volatile Configuration remoteConfiguration;
private volatile Config.Scope keycloakConfiguration;
@Override
public boolean isSupported(Config.Scope config) {
return InfinispanUtils.isRemoteInfinispan();
}
@Override
public CacheRemoteConfigProvider create(KeycloakSession session) {
lazyInit();
return this;
}
@Override
public void init(Config.Scope config) {
this.keycloakConfiguration = config;
}
@Override
public void postInit(KeycloakSessionFactory factory) {
lazyInit();
}
@Override
public Optional<Configuration> configuration() {
assert remoteConfiguration != null;
return Optional.of(remoteConfiguration);
}
@Override
public void close() {
//no-op
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public List<ProviderConfigProperty> getConfigMetadata() {
var builder = ProviderConfigurationBuilder.create();
addHostNameAndPortConfig(builder);
addClientIntelligenceConfig(builder);
addPropertiesFileConfig(builder);
addConnectionPoolConfig(builder);
addTlsConfig(builder);
addAuthenticationConfig(builder);
return builder.build();
}
/**
* Creates the {@link ConfigurationBuilder}.
* <p>
* This class is protected if power users need to extend this class for more advanced configuration. Using a
* properties file is the recommended way to configure the client in more detail. Check
* {@link ConfigurationProperties} for property keys.
*
* @return The {@link ConfigurationBuilder}. This instance can be modified.
* @throws IOException if an error occurred when reading from the properties file (if configured).
* @see ConfigurationProperties
*/
protected ConfigurationBuilder createConfigurationBuilder() throws IOException {
logger.info("Starting Infinispan remote cache manager (Hot Rod Client)");
var builder = new ConfigurationBuilder();
loadProperties(builder);
builder.clientIntelligence(ClientIntelligence.valueOf(keycloakConfiguration.get(CLIENT_INTELLIGENCE, CLIENT_INTELLIGENCE_DEFAULT)));
configureHostname(builder);
configureConnectionPool(builder);
configureTls(builder);
configureAuthentication(builder);
Marshalling.configure(builder);
configureRemoteCaches(builder);
return builder;
}
private void lazyInit() {
if (remoteConfiguration != null) {
return;
}
synchronized (this) {
if (remoteConfiguration != null) {
return;
}
try {
remoteConfiguration = createConfigurationBuilder().build();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
private void loadProperties(ConfigurationBuilder builder) throws IOException {
var path = keycloakConfiguration.get(PROPERTIES_FILE);
if (path == null) {
logger.debug("Hot Rod properties file not configured.");
return;
}
var file = new File(path);
if (!file.exists()) {
throw new RuntimeException("Hot Rod properties file not found: " + path);
}
try (var is = new FileInputStream(file)) {
var properties = new Properties();
properties.load(is);
builder.withProperties(properties);
}
}
private void configureHostname(ConfigurationBuilder builder) {
var cacheRemoteHost = keycloakConfiguration.get(HOSTNAME);
if (cacheRemoteHost == null) {
logger.debug("Hot Rod hostname not configured.");
return;
}
builder.addServer()
.host(cacheRemoteHost)
.port(keycloakConfiguration.getInt(PORT, ConfigurationProperties.DEFAULT_HOTROD_PORT));
}
private void configureConnectionPool(ConfigurationBuilder builder) {
builder.connectionPool()
.maxActive(keycloakConfiguration.getInt(CONNECTION_POOL_MAX_ACTIVE, CONNECTION_POOL_MAX_ACTIVE_DEFAULT))
.exhaustedAction(ExhaustedAction.valueOf(keycloakConfiguration.get(CONNECTION_POOL_EXHAUSTED_ACTION, CONNECTION_POOL_EXHAUSTED_ACTION_DEFAULT)));
}
private void configureTls(ConfigurationBuilder builder) {
if (!keycloakConfiguration.getBoolean(TLS_ENABLED, Boolean.FALSE)) {
logger.debug("Hot Rod TLS not enabled.");
return;
}
var sniHostName = keycloakConfiguration.get(TLS_SNI_HOSTNAME);
if (sniHostName == null) {
sniHostName = keycloakConfiguration.get(HOSTNAME);
}
builder.security().ssl()
.enable()
.sslContext(createSSLContext())
.sniHostName(sniHostName);
}
private void configureAuthentication(ConfigurationBuilder builder) {
var username = keycloakConfiguration.get(USERNAME);
var password = keycloakConfiguration.get(PASSWORD);
if (username == null && password == null) {
logger.debug("Hot Rod authentication not enabled.");
return;
}
builder.security().authentication()
.enable()
.username(username)
.password(password)
.realm(keycloakConfiguration.get(AUTH_REALM, AuthenticationConfigurationBuilder.DEFAULT_REALM))
.saslMechanism(keycloakConfiguration.get(SASL_MECHANISM, SASL_MECHANISM_DEFAULT));
}
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 shouldCreateRemoteCaches() {
// TODO convert to SPI option when we want to support this feature
// http://github.com/keycloak/keycloak/issues/32129
return Boolean.getBoolean("kc.cache-remote-create-caches");
}
private static void configureRemoteCaches(ConfigurationBuilder builder) {
if (!shouldCreateRemoteCaches()) {
return;
}
// fall back for distributed caches if not defined
logger.warn("Creating remote cache in external Infinispan server. It should not be used in production!");
var baseConfig = defaultRemoteCacheBuilder();
skipSessionsCacheIfRequired(Arrays.stream(CLUSTERED_CACHE_NAMES))
.forEach(name -> builder.remoteCache(name).configuration(baseConfig.toStringConfiguration(name)));
}
private static org.infinispan.configuration.cache.Configuration defaultRemoteCacheBuilder() {
var builder = new org.infinispan.configuration.cache.ConfigurationBuilder();
builder.clustering().cacheMode(CacheMode.DIST_SYNC);
builder.encoding().mediaType(MediaType.APPLICATION_PROTOSTREAM);
return builder.build();
}
// configuration option below
private static void addHostNameAndPortConfig(ProviderConfigurationBuilder builder) {
copyFromOption(builder, HOSTNAME, "hostname", ProviderConfigProperty.STRING_TYPE, CachingOptions.CACHE_REMOTE_HOST, false);
copyFromOption(builder, PORT, "port", ProviderConfigProperty.INTEGER_TYPE, CachingOptions.CACHE_REMOTE_PORT, false);
}
private static void addClientIntelligenceConfig(ProviderConfigurationBuilder builder) {
builder.property()
.name(CLIENT_INTELLIGENCE)
.helpText("Specifies the level of intelligence the Hot Rod client should have.")
.label("intelligence")
.type(ProviderConfigProperty.STRING_TYPE)
.defaultValue(CLIENT_INTELLIGENCE_DEFAULT)
.options(Arrays.stream(ClientIntelligence.values()).map(Enum::name).toList())
.add();
}
private static void addPropertiesFileConfig(ProviderConfigurationBuilder builder) {
builder.property()
.name(PROPERTIES_FILE)
.helpText("Path to the properties file with the Hot Rod client configuration.")
.label("file")
.type(ProviderConfigProperty.FILE_TYPE)
.add();
}
private static void addConnectionPoolConfig(ProviderConfigurationBuilder builder) {
builder.property()
.name(CONNECTION_POOL_MAX_ACTIVE)
.helpText("Sets the maximum number of connections per Infinispan server instance.")
.label("maxActive")
.type(ProviderConfigProperty.INTEGER_TYPE)
.defaultValue(CONNECTION_POOL_MAX_ACTIVE_DEFAULT)
.add();
builder.property()
.name(CONNECTION_POOL_EXHAUSTED_ACTION)
.helpText("Specifies what happens when asking for a connection from a server's pool, and that pool is exhausted.")
.label("action")
.type(ProviderConfigProperty.STRING_TYPE)
.defaultValue(CONNECTION_POOL_EXHAUSTED_ACTION_DEFAULT)
.options(Arrays.stream(ExhaustedAction.values()).map(Enum::name).toList())
.add();
}
private static void addAuthenticationConfig(ProviderConfigurationBuilder builder) {
copyFromOption(builder, USERNAME, "username", ProviderConfigProperty.STRING_TYPE, CachingOptions.CACHE_REMOTE_USERNAME, false);
copyFromOption(builder, PASSWORD, "password", ProviderConfigProperty.STRING_TYPE, CachingOptions.CACHE_REMOTE_PASSWORD, true);
builder.property()
.name(AUTH_REALM)
.helpText("Specifies the Infinispan server realm to be used for authentication.")
.label("realm")
.type(ProviderConfigProperty.STRING_TYPE)
.defaultValue(AuthenticationConfigurationBuilder.DEFAULT_REALM)
.add();
builder.property()
.name(SASL_MECHANISM)
.helpText("Selects the SASL mechanism to use for the connection to the Infinispan server.")
.label("mechanism")
.type(ProviderConfigProperty.STRING_TYPE)
.defaultValue(SASL_MECHANISM_DEFAULT)
.add();
}
private static void addTlsConfig(ProviderConfigurationBuilder builder) {
copyFromOption(builder, TLS_ENABLED, "enabled", ProviderConfigProperty.BOOLEAN_TYPE, CachingOptions.CACHE_REMOTE_TLS_ENABLED, false);
builder.property()
.name(TLS_SNI_HOSTNAME)
.helpText("Specifies the TLS SNI hostname for the connection to the Infinispan server.")
.label("hostname")
.type(ProviderConfigProperty.STRING_TYPE)
.add();
}
}

View file

@ -0,0 +1,73 @@
/*
* 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.remote;
import java.util.Optional;
import org.infinispan.client.hotrod.configuration.Configuration;
import org.keycloak.Config;
import org.keycloak.infinispan.util.InfinispanUtils;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.spi.infinispan.CacheRemoteConfigProvider;
import org.keycloak.spi.infinispan.CacheRemoteConfigProviderFactory;
/**
* Implementation used when an external Infinispan cluster is not configured.
*/
public class DisabledCacheRemoteConfigProviderFactory implements CacheRemoteConfigProviderFactory, CacheRemoteConfigProvider, EnvironmentDependentProviderFactory {
private static final String PROVIDER_ID = "disabled";
@Override
public boolean isSupported(Config.Scope config) {
return !InfinispanUtils.isRemoteInfinispan();
}
@Override
public CacheRemoteConfigProvider create(KeycloakSession session) {
return this;
}
@Override
public void init(Config.Scope config) {
//no-op
}
@Override
public void postInit(KeycloakSessionFactory factory) {
//no-op
}
@Override
public Optional<Configuration> configuration() {
// no configuration since it is disabled
return Optional.empty();
}
@Override
public void close() {
}
@Override
public String getId() {
return PROVIDER_ID;
}
}

View file

@ -15,4 +15,5 @@
# limitations under the License.
#
org.keycloak.connections.infinispan.InfinispanConnectionSpi
org.keycloak.connections.infinispan.InfinispanConnectionSpi
org.keycloak.spi.infinispan.CacheRemoteConfigProviderSpi

View file

@ -0,0 +1,19 @@
#
# 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.
#
org.keycloak.spi.infinispan.impl.remote.DisabledCacheRemoteConfigProviderFactory
org.keycloak.spi.infinispan.impl.remote.DefaultCacheRemoteConfigProviderFactory

View file

@ -92,23 +92,28 @@ final class CachingPropertyMappers {
.build(),
fromOption(CachingOptions.CACHE_REMOTE_HOST)
.paramLabel("hostname")
.to("kc.spi-cache-remote-default-hostname")
.addValidateEnabled(CachingPropertyMappers::isRemoteCacheHostEnabled, MULTI_SITE_OR_EMBEDDED_REMOTE_FEATURE_SET)
.isRequired(InfinispanUtils::isRemoteInfinispan, MULTI_SITE_FEATURE_SET)
.build(),
fromOption(CachingOptions.CACHE_REMOTE_PORT)
.isEnabled(CachingPropertyMappers::remoteHostSet, CachingPropertyMappers.REMOTE_HOST_SET)
.to("kc.spi-cache-remote-default-port")
.paramLabel("port")
.build(),
fromOption(CachingOptions.CACHE_REMOTE_TLS_ENABLED)
.isEnabled(CachingPropertyMappers::remoteHostSet, CachingPropertyMappers.REMOTE_HOST_SET)
.to("kc.spi-cache-remote-default-tls-enabled")
.build(),
fromOption(CachingOptions.CACHE_REMOTE_USERNAME)
.isEnabled(CachingPropertyMappers::remoteHostSet, CachingPropertyMappers.REMOTE_HOST_SET)
.to("kc.spi-cache-remote-default-username")
.validator((value) -> validateCachingOptionIsPresent(CachingOptions.CACHE_REMOTE_USERNAME, CachingOptions.CACHE_REMOTE_PASSWORD))
.paramLabel("username")
.build(),
fromOption(CachingOptions.CACHE_REMOTE_PASSWORD)
.isEnabled(CachingPropertyMappers::remoteHostSet, CachingPropertyMappers.REMOTE_HOST_SET)
.to("kc.spi-cache-remote-default-password")
.validator((value) -> validateCachingOptionIsPresent(CachingOptions.CACHE_REMOTE_PASSWORD, CachingOptions.CACHE_REMOTE_USERNAME))
.paramLabel("password")
.isMasked(true)

View file

@ -21,7 +21,6 @@ import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
@ -32,14 +31,7 @@ import java.util.function.Supplier;
import java.util.stream.Stream;
import io.micrometer.core.instrument.Metrics;
import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.client.hotrod.RemoteCacheManager;
import org.infinispan.client.hotrod.RemoteCacheManagerAdmin;
import org.infinispan.client.hotrod.impl.ConfigurationProperties;
import org.infinispan.commons.dataconversion.MediaType;
import org.infinispan.commons.internal.InternalCacheNames;
import org.infinispan.commons.util.concurrent.CompletableFutures;
import org.infinispan.configuration.cache.CacheMode;
import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.configuration.cache.HashConfiguration;
import org.infinispan.configuration.global.ShutdownHookBehavior;
@ -49,8 +41,6 @@ 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.infinispan.protostream.descriptors.FileDescriptor;
import org.infinispan.query.remote.client.ProtobufMetadataManagerConstants;
import org.jboss.logging.Logger;
import org.keycloak.common.Profile;
import org.keycloak.common.util.MultiSiteUtils;
@ -60,14 +50,8 @@ 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.marshalling.KeycloakIndexSchemaUtil;
import org.keycloak.marshalling.KeycloakModelSchema;
import org.keycloak.marshalling.Marshalling;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.sessions.infinispan.query.ClientSessionQueries;
import org.keycloak.models.sessions.infinispan.query.UserSessionQueries;
import org.keycloak.models.sessions.infinispan.remote.RemoteInfinispanAuthenticationSessionProviderFactory;
import org.keycloak.models.sessions.infinispan.remote.RemoteUserLoginFailureProviderFactory;
import org.keycloak.quarkus.runtime.configuration.Configuration;
import org.keycloak.quarkus.runtime.storage.infinispan.jgroups.JGroupsConfigurator;
@ -77,18 +61,15 @@ import static org.keycloak.config.CachingOptions.CACHE_REMOTE_HOST_PROPERTY;
import static org.keycloak.config.CachingOptions.CACHE_REMOTE_PASSWORD_PROPERTY;
import static org.keycloak.config.CachingOptions.CACHE_REMOTE_PORT_PROPERTY;
import static org.keycloak.config.CachingOptions.CACHE_REMOTE_USERNAME_PROPERTY;
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.AUTHENTICATION_SESSIONS_CACHE_NAME;
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.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.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;
import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.skipSessionsCacheIfRequired;
import static org.wildfly.security.sasl.util.SaslMechanismInformation.Names.SCRAM_SHA_512;
public class CacheManagerFactory {
@ -101,7 +82,6 @@ public class CacheManagerFactory {
private static final Supplier<ConfigurationBuilder> TO_NULL = () -> null;
private volatile CompletableFuture<EmbeddedCacheManager> cacheManagerFuture;
private final CompletableFuture<RemoteCacheManager> remoteCacheManagerFuture;
private final JGroupsConfigurator jGroupsConfigurator;
public CacheManagerFactory(String config) {
@ -113,14 +93,6 @@ public class CacheManagerFactory {
} else {
cacheManagerFuture = CompletableFuture.supplyAsync(() -> startEmbeddedCacheManager(null));
}
if (InfinispanUtils.isRemoteInfinispan()) {
logger.debug("Remote Cache feature is enabled");
this.remoteCacheManagerFuture = CompletableFuture.supplyAsync(this::startRemoteCacheManager);
} else {
logger.debug("Remote Cache feature is disabled");
this.remoteCacheManagerFuture = CompletableFutures.completedNull();
}
}
public EmbeddedCacheManager getOrCreateEmbeddedCacheManager(KeycloakSession keycloakSession) {
@ -137,10 +109,6 @@ public class CacheManagerFactory {
return join(cacheManagerFuture);
}
public RemoteCacheManager getOrCreateRemoteCacheManager() {
return join(remoteCacheManagerFuture);
}
private static <T> T join(Future<T> future) {
try {
return future.get(getStartTimeout(), TimeUnit.SECONDS);
@ -152,142 +120,6 @@ public class CacheManagerFactory {
}
}
private RemoteCacheManager startRemoteCacheManager() {
logger.info("Starting Infinispan remote cache manager (Hot Rod Client)");
String cacheRemoteHost = requiredStringProperty(CACHE_REMOTE_HOST_PROPERTY);
Integer cacheRemotePort = Configuration.getOptionalKcValue(CACHE_REMOTE_PORT_PROPERTY)
.map(Integer::parseInt)
.orElse(ConfigurationProperties.DEFAULT_HOTROD_PORT);
org.infinispan.client.hotrod.configuration.ConfigurationBuilder builder = new org.infinispan.client.hotrod.configuration.ConfigurationBuilder();
builder.addServer().host(cacheRemoteHost).port(cacheRemotePort);
builder.connectionPool().maxActive(16).exhaustedAction(org.infinispan.client.hotrod.configuration.ExhaustedAction.CREATE_NEW);
if (isRemoteTLSEnabled()) {
builder.security().ssl()
.enable()
.sslContext(createSSLContext())
.sniHostName(cacheRemoteHost);
}
if (isRemoteAuthenticationEnabled()) {
builder.security().authentication()
.enable()
.username(requiredStringProperty(CACHE_REMOTE_USERNAME_PROPERTY))
.password(requiredStringProperty(CACHE_REMOTE_PASSWORD_PROPERTY))
.realm("default")
.saslMechanism(SCRAM_SHA_512);
}
Marshalling.configure(builder);
if (shouldCreateRemoteCaches()) {
createRemoteCaches(builder);
}
var remoteCacheManager = new RemoteCacheManager(builder.build());
// update the schema before trying to access the caches
updateProtoSchema(remoteCacheManager);
// establish connection to all caches
if (isStartEagerly()) {
skipSessionsCacheIfRequired(Arrays.stream(CLUSTERED_CACHE_NAMES)).forEach(remoteCacheManager::getCache);
}
return remoteCacheManager;
}
private static void createRemoteCaches(org.infinispan.client.hotrod.configuration.ConfigurationBuilder builder) {
// fall back for distributed caches if not defined
logger.warn("Creating remote cache in external Infinispan server. It should not be used in production!");
var baseConfig = defaultRemoteCacheBuilder().build();
skipSessionsCacheIfRequired(Arrays.stream(CLUSTERED_CACHE_NAMES))
.forEach(name -> builder.remoteCache(name).configuration(baseConfig.toStringConfiguration(name)));
}
private static ConfigurationBuilder defaultRemoteCacheBuilder() {
var builder = new ConfigurationBuilder();
builder.clustering().cacheMode(CacheMode.DIST_SYNC);
builder.encoding().mediaType(MediaType.APPLICATION_PROTOSTREAM);
return builder;
}
private void updateProtoSchema(RemoteCacheManager remoteCacheManager) {
var key = KeycloakModelSchema.INSTANCE.getProtoFileName();
var current = KeycloakModelSchema.INSTANCE.getProtoFile();
RemoteCache<String, String> protostreamMetadataCache = remoteCacheManager.getCache(InternalCacheNames.PROTOBUF_METADATA_CACHE_NAME);
var stored = protostreamMetadataCache.getWithMetadata(key);
if (stored == null) {
if (protostreamMetadataCache.putIfAbsent(key, current) == null) {
logger.info("Infinispan ProtoStream schema uploaded for the first time.");
} else {
logger.info("Failed to update Infinispan ProtoStream schema. Assumed it was updated by other Keycloak server.");
}
checkForProtoSchemaErrors(protostreamMetadataCache);
return;
}
if (Objects.equals(stored.getValue(), current)) {
logger.info("Infinispan ProtoStream schema is up to date!");
return;
}
if (protostreamMetadataCache.replaceWithVersion(key, current, stored.getVersion())) {
logger.info("Infinispan ProtoStream schema successful updated.");
reindexCaches(remoteCacheManager, stored.getValue(), current);
} else {
logger.info("Failed to update Infinispan ProtoStream schema. Assumed it was updated by other Keycloak server.");
}
checkForProtoSchemaErrors(protostreamMetadataCache);
}
private void checkForProtoSchemaErrors(RemoteCache<String, String> protostreamMetadataCache) {
String errors = protostreamMetadataCache.get(ProtobufMetadataManagerConstants.ERRORS_KEY_SUFFIX);
if (errors != null) {
for (String errorFile : errors.split("\n")) {
logger.errorf("%nThere was an error in proto file: %s%nError message: %s%nCurrent proto schema: %s%n",
errorFile,
protostreamMetadataCache.get(errorFile + ProtobufMetadataManagerConstants.ERRORS_KEY_SUFFIX),
protostreamMetadataCache.get(errorFile));
}
}
}
private static void reindexCaches(RemoteCacheManager remoteCacheManager, String oldSchema, String newSchema) {
var oldPS = KeycloakModelSchema.parseProtoSchema(oldSchema);
var newPS = KeycloakModelSchema.parseProtoSchema(newSchema);
var admin = remoteCacheManager.administration();
if (isEntityChanged(oldPS, newPS, RemoteUserLoginFailureProviderFactory.PROTO_ENTITY)) {
updateSchemaAndReIndexCache(admin, LOGIN_FAILURE_CACHE_NAME);
}
if (isEntityChanged(oldPS, newPS, RemoteInfinispanAuthenticationSessionProviderFactory.PROTO_ENTITY)) {
updateSchemaAndReIndexCache(admin, AUTHENTICATION_SESSIONS_CACHE_NAME);
}
if (isEntityChanged(oldPS, newPS, ClientSessionQueries.CLIENT_SESSION)) {
updateSchemaAndReIndexCache(admin, CLIENT_SESSION_CACHE_NAME);
updateSchemaAndReIndexCache(admin, OFFLINE_CLIENT_SESSION_CACHE_NAME);
}
if (isEntityChanged(oldPS, newPS, UserSessionQueries.USER_SESSION)) {
updateSchemaAndReIndexCache(admin, USER_SESSION_CACHE_NAME);
updateSchemaAndReIndexCache(admin, OFFLINE_USER_SESSION_CACHE_NAME);
}
}
private static boolean isEntityChanged(FileDescriptor oldSchema, FileDescriptor newSchema, String entity) {
var v1 = KeycloakModelSchema.findEntity(oldSchema, entity);
var v2 = KeycloakModelSchema.findEntity(newSchema, entity);
return v1.isPresent() && v2.isPresent() && KeycloakIndexSchemaUtil.isIndexSchemaChanged(v1.get(), v2.get());
}
private static void updateSchemaAndReIndexCache(RemoteCacheManagerAdmin admin, String cacheName) {
admin.updateIndexSchema(cacheName);
admin.reindexCache(cacheName);
}
private EmbeddedCacheManager startEmbeddedCacheManager(KeycloakSession session) {
logger.info("Starting Infinispan embedded cache manager");
var builder = jGroupsConfigurator.holder();
@ -359,10 +191,6 @@ public class CacheManagerFactory {
Configuration.getOptionalKcValue(CACHE_REMOTE_PASSWORD_PROPERTY).isPresent();
}
private static boolean shouldCreateRemoteCaches() {
return Boolean.getBoolean("kc.cache-remote-create-caches");
}
private static SSLContext createSSLContext() {
try {
// uses the default Java Runtime TrustStore, or the one generated by Keycloak (see org.keycloak.truststore.TruststoreBuilder)

View file

@ -32,9 +32,4 @@ public final class QuarkusCacheManagerProvider implements ManagedCacheManagerPro
public <C> C getEmbeddedCacheManager(KeycloakSession keycloakSession, Config.Scope config) {
return (C) Arc.container().instance(CacheManagerFactory.class).get().getOrCreateEmbeddedCacheManager(keycloakSession);
}
@Override
public <C> C getRemoteCacheManager(Config.Scope config) {
return (C) Arc.container().instance(CacheManagerFactory.class).get().getOrCreateRemoteCacheManager();
}
}

View file

@ -31,6 +31,10 @@ public interface ManagedCacheManagerProvider {
/**
* @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.
*/
<C> C getRemoteCacheManager(Config.Scope config);
@Deprecated(since = "26.3", forRemoval = true)
default <C> C getRemoteCacheManager(Config.Scope config) {
return null;
}
}

View file

@ -189,6 +189,13 @@
}
},
"cacheRemote": {
"default": {
"hostname": "${keycloak.connectionsInfinispan.remoteStoreServer:localhost}",
"port": "${keycloak.connectionsInfinispan.remoteStorePort:11222}"
}
},
"truststore": {
"file": {
"file": "${keycloak.truststore.file:target/dependency/keystore/keycloak.truststore}",

View file

@ -54,10 +54,11 @@ import org.keycloak.provider.Spi;
import org.keycloak.services.DefaultComponentFactoryProviderFactory;
import org.keycloak.services.DefaultKeycloakSessionFactory;
import org.keycloak.services.resteasy.ResteasyKeycloakSessionFactory;
import org.keycloak.spi.infinispan.CacheRemoteConfigProviderFactory;
import org.keycloak.spi.infinispan.CacheRemoteConfigProviderSpi;
import org.keycloak.storage.DatastoreProviderFactory;
import org.keycloak.storage.DatastoreSpi;
import org.keycloak.timer.TimerSpi;
import com.google.common.collect.ImmutableSet;
import java.lang.management.LockInfo;
import java.lang.management.ManagementFactory;
@ -175,9 +176,9 @@ public abstract class KeycloakModelTest {
@Override
public Statement apply(Statement base, Description description) {
Stream<RequireProvider> st = Optional.ofNullable(description.getAnnotation(RequireProviders.class))
.map(RequireProviders::value)
.map(Stream::of)
.orElseGet(Stream::empty);
.map(RequireProviders::value)
.stream()
.flatMap(Stream::of);
RequireProvider rp = description.getAnnotation(RequireProvider.class);
if (rp != null) {
@ -232,37 +233,37 @@ public abstract class KeycloakModelTest {
}
};
private static final Set<Class<? extends Spi>> ALLOWED_SPIS = ImmutableSet.<Class<? extends Spi>>builder()
.add(AuthorizationSpi.class)
.add(PolicySpi.class)
.add(ClientScopeSpi.class)
.add(ClientSpi.class)
.add(ComponentFactorySpi.class)
.add(ClusterSpi.class)
.add(EventStoreSpi.class)
.add(ExecutorsSpi.class)
.add(GroupSpi.class)
.add(RealmSpi.class)
.add(RoleSpi.class)
.add(DeploymentStateSpi.class)
.add(StoreFactorySpi.class)
.add(TimerSpi.class)
.add(TracingSpi.class)
.add(UserLoginFailureSpi.class)
.add(UserSessionSpi.class)
.add(UserSpi.class)
.add(DatastoreSpi.class)
.build();
private static final Set<Class<? extends Spi>> ALLOWED_SPIS = Set.of(
AuthorizationSpi.class,
PolicySpi.class,
ClientScopeSpi.class,
ClientSpi.class,
ComponentFactorySpi.class,
ClusterSpi.class,
EventStoreSpi.class,
ExecutorsSpi.class,
GroupSpi.class,
RealmSpi.class,
RoleSpi.class,
DeploymentStateSpi.class,
StoreFactorySpi.class,
TimerSpi.class,
TracingSpi.class,
UserLoginFailureSpi.class,
UserSessionSpi.class,
UserSpi.class,
DatastoreSpi.class,
CacheRemoteConfigProviderSpi.class);
private static final Set<Class<? extends ProviderFactory>> ALLOWED_FACTORIES = ImmutableSet.<Class<? extends ProviderFactory>>builder()
.add(ComponentFactoryProviderFactory.class)
.add(DefaultAuthorizationProviderFactory.class)
.add(PolicyProviderFactory.class)
.add(DefaultExecutorsProviderFactory.class)
.add(DeploymentStateProviderFactory.class)
.add(DatastoreProviderFactory.class)
.add(TracingProviderFactory.class)
.build();
private static final Set<Class<? extends ProviderFactory>> ALLOWED_FACTORIES = Set.of(
ComponentFactoryProviderFactory.class,
DefaultAuthorizationProviderFactory.class,
PolicyProviderFactory.class,
DefaultExecutorsProviderFactory.class,
DeploymentStateProviderFactory.class,
DatastoreProviderFactory.class,
TracingProviderFactory.class,
CacheRemoteConfigProviderFactory.class);
protected static final List<KeycloakModelParameters> MODEL_PARAMETERS;
protected static final Config CONFIG = new Config(KeycloakModelTest::useDefaultFactory);