From 636fffe0bc37a63bd5a2578b6bbcd815364c41d8 Mon Sep 17 00:00:00 2001 From: Pedro Ruivo Date: Wed, 16 Apr 2025 16:08:43 +0100 Subject: [PATCH] Create CacheRemoteConfigProvider (#38570) Closes #38496 Signed-off-by: Pedro Ruivo --- ...ltInfinispanConnectionProviderFactory.java | 130 ++++--- .../marshalling/KeycloakIndexSchemaUtil.java | 101 ++++- .../infinispan/CacheRemoteConfigProvider.java | 44 +++ .../CacheRemoteConfigProviderFactory.java | 28 ++ .../CacheRemoteConfigProviderSpi.java | 48 +++ .../keycloak/spi/infinispan/impl/Util.java | 53 +++ ...faultCacheRemoteConfigProviderFactory.java | 358 ++++++++++++++++++ ...abledCacheRemoteConfigProviderFactory.java | 73 ++++ .../services/org.keycloak.provider.Spi | 3 +- ...nfinispan.CacheRemoteConfigProviderFactory | 19 + .../mappers/CachingPropertyMappers.java | 5 + .../infinispan/CacheManagerFactory.java | 172 --------- .../QuarkusCacheManagerProvider.java | 5 - .../cluster/ManagedCacheManagerProvider.java | 6 +- .../resources/META-INF/keycloak-server.json | 7 + .../testsuite/model/KeycloakModelTest.java | 69 ++-- 16 files changed, 847 insertions(+), 274 deletions(-) create mode 100644 model/infinispan/src/main/java/org/keycloak/spi/infinispan/CacheRemoteConfigProvider.java create mode 100644 model/infinispan/src/main/java/org/keycloak/spi/infinispan/CacheRemoteConfigProviderFactory.java create mode 100644 model/infinispan/src/main/java/org/keycloak/spi/infinispan/CacheRemoteConfigProviderSpi.java create mode 100644 model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/Util.java create mode 100644 model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/remote/DefaultCacheRemoteConfigProviderFactory.java create mode 100644 model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/remote/DisabledCacheRemoteConfigProviderFactory.java create mode 100644 model/infinispan/src/main/resources/META-INF/services/org.keycloak.spi.infinispan.CacheRemoteConfigProviderFactory diff --git a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java index b9f8083b117..117fb3cf948 100755 --- a/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java +++ b/model/infinispan/src/main/java/org/keycloak/connections/infinispan/DefaultInfinispanConnectionProviderFactory.java @@ -17,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 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 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> dependsOn() { - return Set.of(JpaConnectionProvider.class); + return Set.of(JpaConnectionProvider.class, CacheRemoteConfigProvider.class); } } diff --git a/model/infinispan/src/main/java/org/keycloak/marshalling/KeycloakIndexSchemaUtil.java b/model/infinispan/src/main/java/org/keycloak/marshalling/KeycloakIndexSchemaUtil.java index c17ea319d1f..d0933a787c1 100644 --- a/model/infinispan/src/main/java/org/keycloak/marshalling/KeycloakIndexSchemaUtil.java +++ b/model/infinispan/src/main/java/org/keycloak/marshalling/KeycloakIndexSchemaUtil.java @@ -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 INDEX_ANNOTATION = List.of(BASIC_ANNOTATION); + /** + * Uploads the {@link GeneratedSchema} to the Infinispan cluster. + *

+ * 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 indexedEntities) { + var key = schema.getProtoFileName(); + var current = schema.getProtoFile(); + + var 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, 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 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 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); + } + } } diff --git a/model/infinispan/src/main/java/org/keycloak/spi/infinispan/CacheRemoteConfigProvider.java b/model/infinispan/src/main/java/org/keycloak/spi/infinispan/CacheRemoteConfigProvider.java new file mode 100644 index 00000000000..ddf4d0c1b13 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/spi/infinispan/CacheRemoteConfigProvider.java @@ -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. + *

+ * 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(); + + @Override + default void close() { + //no-op + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/spi/infinispan/CacheRemoteConfigProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/spi/infinispan/CacheRemoteConfigProviderFactory.java new file mode 100644 index 00000000000..4a89098ae6b --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/spi/infinispan/CacheRemoteConfigProviderFactory.java @@ -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 { +} diff --git a/model/infinispan/src/main/java/org/keycloak/spi/infinispan/CacheRemoteConfigProviderSpi.java b/model/infinispan/src/main/java/org/keycloak/spi/infinispan/CacheRemoteConfigProviderSpi.java new file mode 100644 index 00000000000..bb03a157417 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/spi/infinispan/CacheRemoteConfigProviderSpi.java @@ -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 getProviderClass() { + return CacheRemoteConfigProvider.class; + } + + @Override + public Class getProviderFactoryClass() { + return CacheRemoteConfigProviderFactory.class; + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/Util.java b/model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/Util.java new file mode 100644 index 00000000000..fc5aa3d30c0 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/Util.java @@ -0,0 +1,53 @@ +/* + * Copyright 2025 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.spi.infinispan.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(); + } + +} diff --git a/model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/remote/DefaultCacheRemoteConfigProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/remote/DefaultCacheRemoteConfigProviderFactory.java new file mode 100644 index 00000000000..9806e19d198 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/remote/DefaultCacheRemoteConfigProviderFactory.java @@ -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}. + *

+ * 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() { + assert remoteConfiguration != null; + return Optional.of(remoteConfiguration); + } + + @Override + public void close() { + //no-op + } + + @Override + public String getId() { + return PROVIDER_ID; + } + + @Override + public List getConfigMetadata() { + var builder = ProviderConfigurationBuilder.create(); + addHostNameAndPortConfig(builder); + addClientIntelligenceConfig(builder); + addPropertiesFileConfig(builder); + addConnectionPoolConfig(builder); + addTlsConfig(builder); + addAuthenticationConfig(builder); + return builder.build(); + } + + /** + * Creates the {@link ConfigurationBuilder}. + *

+ * 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(); + } +} diff --git a/model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/remote/DisabledCacheRemoteConfigProviderFactory.java b/model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/remote/DisabledCacheRemoteConfigProviderFactory.java new file mode 100644 index 00000000000..a721c9d2725 --- /dev/null +++ b/model/infinispan/src/main/java/org/keycloak/spi/infinispan/impl/remote/DisabledCacheRemoteConfigProviderFactory.java @@ -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() { + // no configuration since it is disabled + return Optional.empty(); + } + + @Override + public void close() { + + } + + @Override + public String getId() { + return PROVIDER_ID; + } +} diff --git a/model/infinispan/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/model/infinispan/src/main/resources/META-INF/services/org.keycloak.provider.Spi index 5dd9d5ded33..46f7509c5b8 100644 --- a/model/infinispan/src/main/resources/META-INF/services/org.keycloak.provider.Spi +++ b/model/infinispan/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -15,4 +15,5 @@ # limitations under the License. # -org.keycloak.connections.infinispan.InfinispanConnectionSpi \ No newline at end of file +org.keycloak.connections.infinispan.InfinispanConnectionSpi +org.keycloak.spi.infinispan.CacheRemoteConfigProviderSpi \ No newline at end of file diff --git a/model/infinispan/src/main/resources/META-INF/services/org.keycloak.spi.infinispan.CacheRemoteConfigProviderFactory b/model/infinispan/src/main/resources/META-INF/services/org.keycloak.spi.infinispan.CacheRemoteConfigProviderFactory new file mode 100644 index 00000000000..09ec19b1498 --- /dev/null +++ b/model/infinispan/src/main/resources/META-INF/services/org.keycloak.spi.infinispan.CacheRemoteConfigProviderFactory @@ -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 \ No newline at end of file diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/CachingPropertyMappers.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/CachingPropertyMappers.java index 60439081746..633cb69cab7 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/CachingPropertyMappers.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/mappers/CachingPropertyMappers.java @@ -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) diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/CacheManagerFactory.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/CacheManagerFactory.java index 947bb064e0f..dae25f35e9f 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/CacheManagerFactory.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/CacheManagerFactory.java @@ -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 TO_NULL = () -> null; private volatile CompletableFuture cacheManagerFuture; - private final CompletableFuture 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 join(Future 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 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 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) diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/QuarkusCacheManagerProvider.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/QuarkusCacheManagerProvider.java index 0324efd423b..096080853e4 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/QuarkusCacheManagerProvider.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/storage/infinispan/QuarkusCacheManagerProvider.java @@ -32,9 +32,4 @@ public final class QuarkusCacheManagerProvider implements ManagedCacheManagerPro public C getEmbeddedCacheManager(KeycloakSession keycloakSession, Config.Scope config) { return (C) Arc.container().instance(CacheManagerFactory.class).get().getOrCreateEmbeddedCacheManager(keycloakSession); } - - @Override - public C getRemoteCacheManager(Config.Scope config) { - return (C) Arc.container().instance(CacheManagerFactory.class).get().getOrCreateRemoteCacheManager(); - } } diff --git a/server-spi-private/src/main/java/org/keycloak/cluster/ManagedCacheManagerProvider.java b/server-spi-private/src/main/java/org/keycloak/cluster/ManagedCacheManagerProvider.java index 3f9cbb3c91a..43da64901c6 100644 --- a/server-spi-private/src/main/java/org/keycloak/cluster/ManagedCacheManagerProvider.java +++ b/server-spi-private/src/main/java/org/keycloak/cluster/ManagedCacheManagerProvider.java @@ -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 getRemoteCacheManager(Config.Scope config); + @Deprecated(since = "26.3", forRemoval = true) + default C getRemoteCacheManager(Config.Scope config) { + return null; + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json index 132d744a63f..9f36c81bd95 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json @@ -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}", diff --git a/testsuite/model/src/test/java/org/keycloak/testsuite/model/KeycloakModelTest.java b/testsuite/model/src/test/java/org/keycloak/testsuite/model/KeycloakModelTest.java index c7506ea9c66..13272c6e92b 100644 --- a/testsuite/model/src/test/java/org/keycloak/testsuite/model/KeycloakModelTest.java +++ b/testsuite/model/src/test/java/org/keycloak/testsuite/model/KeycloakModelTest.java @@ -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 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> ALLOWED_SPIS = ImmutableSet.>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> 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> ALLOWED_FACTORIES = ImmutableSet.>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> ALLOWED_FACTORIES = Set.of( + ComponentFactoryProviderFactory.class, + DefaultAuthorizationProviderFactory.class, + PolicyProviderFactory.class, + DefaultExecutorsProviderFactory.class, + DeploymentStateProviderFactory.class, + DatastoreProviderFactory.class, + TracingProviderFactory.class, + CacheRemoteConfigProviderFactory.class); protected static final List MODEL_PARAMETERS; protected static final Config CONFIG = new Config(KeycloakModelTest::useDefaultFactory);