Add default configuration for cache 'crl'

Fixes #36752

Signed-off-by: Pedro Ruivo <pruivo@redhat.com>
Signed-off-by: Michal Hajas <mhajas@redhat.com>
Signed-off-by: Pedro Ruivo <pruivo@users.noreply.github.com>
Co-authored-by: Michal Hajas <mhajas@redhat.com>
Co-authored-by: Stian Thorgersen <stian@redhat.com>
This commit is contained in:
Pedro Ruivo 2025-02-03 08:53:31 +00:00 committed by GitHub
parent d522fd265d
commit beb20dc425
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 239 additions and 16 deletions

View File

@ -68,7 +68,6 @@ import static org.keycloak.connections.infinispan.InfinispanConnectionProvider.A
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_DEFAULT_MAX;
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;
@ -480,13 +479,7 @@ public class DefaultInfinispanConnectionProviderFactory implements InfinispanCon
}
protected Configuration getCrlCacheConfig() {
ConfigurationBuilder cb = createCacheConfigurationBuilder();
cb.memory()
.whenFull(EvictionStrategy.REMOVE)
.maxCount(CRL_CACHE_DEFAULT_MAX);
return cb.build();
return InfinispanUtil.getCrlCacheConfig().build();
}
private void registerSystemWideListeners(KeycloakSession session) {

View File

@ -185,6 +185,16 @@ public class InfinispanUtil {
return cb;
}
public static ConfigurationBuilder getCrlCacheConfig() {
var builder = createCacheConfigurationBuilder();
builder.memory()
.whenFull(EvictionStrategy.REMOVE)
.maxCount(InfinispanConnectionProvider.CRL_CACHE_DEFAULT_MAX);
return builder;
}
/**
* Replaces the {@link TimeService} in infinispan with the one that respects Keycloak {@link Time}.
* @param cacheManager

View File

@ -29,6 +29,7 @@ import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Stream;
@ -36,7 +37,6 @@ import io.agroal.api.AgroalDataSource;
import io.micrometer.core.instrument.Metrics;
import io.quarkus.arc.Arc;
import jakarta.persistence.EntityManager;
import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.client.hotrod.RemoteCacheManager;
import org.infinispan.client.hotrod.RemoteCacheManagerAdmin;
@ -72,6 +72,8 @@ import org.keycloak.common.Profile;
import org.keycloak.common.util.MultiSiteUtils;
import org.keycloak.config.CachingOptions;
import org.keycloak.config.MetricsOptions;
import org.keycloak.connections.infinispan.InfinispanConnectionProvider;
import org.keycloak.connections.infinispan.InfinispanUtil;
import org.keycloak.connections.jpa.JpaConnectionProvider;
import org.keycloak.connections.jpa.util.JpaUtils;
import org.keycloak.infinispan.util.InfinispanUtils;
@ -100,6 +102,8 @@ 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;
@ -111,6 +115,11 @@ import static org.wildfly.security.sasl.util.SaslMechanismInformation.Names.SCRA
public class CacheManagerFactory {
private static final Logger logger = Logger.getLogger(CacheManagerFactory.class);
// Map with the default cache configuration if the cache is not present in the XML.
private static final Map<String, Supplier<ConfigurationBuilder>> DEFAULT_CONFIGS = Map.of(
CRL_CACHE_NAME, InfinispanUtil::getCrlCacheConfig
);
private static final Supplier<ConfigurationBuilder> TO_NULL = () -> null;
private final CompletableFuture<EmbeddedCacheManager> cacheManagerFuture;
private final CompletableFuture<RemoteCacheManager> remoteCacheManagerFuture;
@ -337,6 +346,13 @@ public class CacheManagerFactory {
}
Marshalling.configure(builder.getGlobalConfigurationBuilder());
assertAllCachesAreConfigured(builder,
// skip revision caches, those are defined by DefaultInfinispanConnectionProviderFactory
Arrays.stream(LOCAL_CACHE_NAMES)
.filter(Predicate.not(InfinispanConnectionProvider.REALM_REVISIONS_CACHE_NAME::equals))
.filter(Predicate.not(InfinispanConnectionProvider.AUTHORIZATION_REVISIONS_CACHE_NAME::equals))
.filter(Predicate.not(InfinispanConnectionProvider.USER_REVISIONS_CACHE_NAME::equals))
);
if (InfinispanUtils.isRemoteInfinispan()) {
var builders = builder.getNamedConfigurationBuilders();
// remove all distributed caches
@ -352,6 +368,7 @@ public class CacheManagerFactory {
builder.getGlobalConfigurationBuilder().nonClusteredDefault();
} else {
// embedded mode!
assertAllCachesAreConfigured(builder, Arrays.stream(CLUSTERED_CACHE_NAMES));
if (builder.getNamedConfigurationBuilders().entrySet().stream().anyMatch(c -> c.getValue().clustering().cacheMode().isClustered())) {
configureTransportStack(builder, em);
configureRemoteStores(builder);
@ -600,14 +617,26 @@ public class CacheManagerFactory {
private static void configureCacheMaxCount(ConfigurationBuilderHolder holder, String[] caches) {
for (String cache : caches) {
var memory = holder.getNamedConfigurationBuilders().get(cache).memory();
String propKey = CachingOptions.cacheMaxCountProperty(cache);
Configuration.getOptionalKcValue(propKey)
.map(Integer::parseInt)
.ifPresent(maxCount -> holder.getNamedConfigurationBuilders()
.get(cache)
.memory()
.maxCount(maxCount)
);
.ifPresent(memory::maxCount);
}
}
private static void assertAllCachesAreConfigured(ConfigurationBuilderHolder holder, Stream<String> caches) {
for (var it = caches.iterator() ; it.hasNext() ; ) {
var cache = it.next();
var builder = holder.getNamedConfigurationBuilders().get(cache);
if (builder != null) {
continue;
}
builder = DEFAULT_CONFIGS.getOrDefault(cache, TO_NULL).get();
if (builder == null) {
throw new IllegalStateException("Infinispan cache '%s' not found. Make sure it is defined in your XML configuration file.".formatted(cache));
}
holder.getNamedConfigurationBuilders().put(cache, builder);
}
}

View File

@ -631,6 +631,14 @@ public final class RawKeycloakDistribution implements KeycloakDistribution {
}
}
public void copyConfigFile(Path configFilePath) {
try {
Files.copy(configFilePath, distPath.resolve("conf").resolve(configFilePath.getFileName()));
} catch (IOException cause) {
throw new RuntimeException("Failed to copy config file [" + configFilePath + "] to 'conf' directory", cause);
}
}
private void updateProperties(Consumer<Properties> propertiesConsumer, File propertiesFile) {
Properties properties = new Properties();

View File

@ -6,6 +6,7 @@ import org.keycloak.it.utils.OutputConsumer;
import org.keycloak.it.utils.RawKeycloakDistribution;
import org.keycloak.testframework.config.Config;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
@ -40,6 +41,10 @@ public class DistributionKeycloakServer implements KeycloakServer {
keycloak.copyProvider(dependency.getGroupId(), dependency.getArtifactId());
}
for (Path configFile : keycloakServerConfigBuilder.toConfigFiles()) {
keycloak.copyConfigFile(configFile);
}
keycloak.run(keycloakServerConfigBuilder.toArgs());
}

View File

@ -3,12 +3,18 @@ package org.keycloak.testframework.server;
import io.quarkus.maven.dependency.Dependency;
import org.keycloak.Keycloak;
import org.keycloak.common.Version;
import org.keycloak.platform.Platform;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Set;
import java.util.concurrent.TimeoutException;
public class EmbeddedKeycloakServer implements KeycloakServer {
private Keycloak keycloak;
private Path homeDir;
@Override
public void start(KeycloakServerConfigBuilder keycloakServerConfigBuilder) {
@ -18,6 +24,29 @@ public class EmbeddedKeycloakServer implements KeycloakServer {
builder.addDependency(dependency.getGroupId(), dependency.getArtifactId(), "");
}
Set<Path> configFiles = keycloakServerConfigBuilder.toConfigFiles();
if (!configFiles.isEmpty()) {
if (homeDir == null) {
homeDir = Platform.getPlatform().getTmpDirectory().toPath();
}
Path conf = homeDir.resolve("conf");
if (!conf.toFile().exists()) {
conf.toFile().mkdirs();
}
for (Path configFile : configFiles) {
try {
Files.copy(configFile, conf.resolve(configFile.getFileName()));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
builder.setHomeDir(homeDir);
keycloak = builder.start(keycloakServerConfigBuilder.toArgs());
}

View File

@ -6,12 +6,16 @@ import io.smallrye.config.SmallRyeConfig;
import org.eclipse.microprofile.config.spi.ConfigSource;
import org.keycloak.common.Profile;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@ -23,6 +27,7 @@ public class KeycloakServerConfigBuilder {
private final Set<String> featuresDisabled = new HashSet<>();
private final LogBuilder log = new LogBuilder();
private final Set<Dependency> dependencies = new HashSet<>();
private final Set<Path> configFiles = new HashSet<>();
private KeycloakServerConfigBuilder(String command) {
this.command = command;
@ -70,6 +75,18 @@ public class KeycloakServerConfigBuilder {
return this;
}
public KeycloakServerConfigBuilder cacheConfigFile(String resourcePath) {
try {
Path p = Paths.get(Objects.requireNonNull(getClass().getResource(resourcePath)).toURI());
configFiles.add(p);
option("cache-config-file", p.getFileName().toString());
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
return this;
}
public class LogBuilder {
private Boolean color;
@ -161,7 +178,7 @@ public class KeycloakServerConfigBuilder {
}
}
public List<String> toArgs() {
List<String> toArgs() {
log.build();
List<String> args = new LinkedList<>();
@ -179,10 +196,14 @@ public class KeycloakServerConfigBuilder {
return args;
}
public Set<Dependency> toDependencies() {
Set<Dependency> toDependencies() {
return dependencies;
}
Set<Path> toConfigFiles() {
return configFiles;
}
private Set<String> toFeatureStrings(Profile.Feature... features) {
return Arrays.stream(features).map(f -> {
if (f.getVersion() > 1) {

View File

@ -4,6 +4,7 @@ import io.quarkus.maven.dependency.Dependency;
import java.net.ConnectException;
import java.net.URL;
import java.nio.file.Path;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@ -52,6 +53,14 @@ public class RemoteKeycloakServer implements KeycloakServer {
sb.append("\n");
}
}
Set<Path> configFiles = keycloakServerConfigBuilder.toConfigFiles();
if (!configFiles.isEmpty()) {
sb.append("Copy following config files to your conf directory:\n");
for (Path c : configFiles) {
sb.append(c.toAbsolutePath());
sb.append("\n");
}
}
System.out.println(sb);
}

View File

@ -0,0 +1,34 @@
package org.keycloak.tests.infinispan;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testframework.annotations.InjectRealm;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.testframework.server.KeycloakServerConfig;
import org.keycloak.testframework.server.KeycloakServerConfigBuilder;
@KeycloakIntegrationTest(config = InfinispanXMLBackwardCompatibilityTest.ServerConfigWithCustomInfinispanXML.class)
public class InfinispanXMLBackwardCompatibilityTest {
private static final String CONFIG_FILE = "/embedded-infinispan-config/infinispan-xml-kc26.xml";
@InjectRealm
ManagedRealm realm;
@Test
void testKeycloakStartedSuccessfullyWithOlderInfinispanXML() {
RealmRepresentation representation = realm.admin().toRepresentation();
Assertions.assertNotNull(representation);
}
public static class ServerConfigWithCustomInfinispanXML implements KeycloakServerConfig {
@Override
public KeycloakServerConfigBuilder configure(KeycloakServerConfigBuilder config) {
return config.cacheConfigFile(CONFIG_FILE);
}
}
}

View File

@ -0,0 +1,85 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2019 Red Hat, Inc. and/or its affiliates
~ and other contributors as indicated by the @author tags.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<infinispan
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="urn:infinispan:config:15.0 http://www.infinispan.org/schemas/infinispan-config-15.0.xsd"
xmlns="urn:infinispan:config:15.0">
<cache-container name="keycloak">
<transport lock-timeout="60000"/>
<local-cache name="realms" simple-cache="true">
<encoding>
<key media-type="application/x-java-object"/>
<value media-type="application/x-java-object"/>
</encoding>
<memory max-count="10000"/>
</local-cache>
<local-cache name="users" simple-cache="true">
<encoding>
<key media-type="application/x-java-object"/>
<value media-type="application/x-java-object"/>
</encoding>
<memory max-count="10000"/>
</local-cache>
<distributed-cache name="sessions" owners="2">
<expiration lifespan="-1"/>
</distributed-cache>
<distributed-cache name="authenticationSessions" owners="2">
<expiration lifespan="-1"/>
</distributed-cache>
<distributed-cache name="offlineSessions" owners="2">
<expiration lifespan="-1"/>
</distributed-cache>
<distributed-cache name="clientSessions" owners="2">
<expiration lifespan="-1"/>
</distributed-cache>
<distributed-cache name="offlineClientSessions" owners="2">
<expiration lifespan="-1"/>
</distributed-cache>
<distributed-cache name="loginFailures" owners="2">
<expiration lifespan="-1"/>
</distributed-cache>
<local-cache name="authorization" simple-cache="true">
<encoding>
<key media-type="application/x-java-object"/>
<value media-type="application/x-java-object"/>
</encoding>
<memory max-count="10000"/>
</local-cache>
<replicated-cache name="work">
<expiration lifespan="-1"/>
</replicated-cache>
<local-cache name="keys" simple-cache="true">
<encoding>
<key media-type="application/x-java-object"/>
<value media-type="application/x-java-object"/>
</encoding>
<expiration max-idle="3600000"/>
<memory max-count="1000"/>
</local-cache>
<distributed-cache name="actionTokens" owners="2">
<encoding>
<key media-type="application/x-java-object"/>
<value media-type="application/x-java-object"/>
</encoding>
<expiration max-idle="-1" lifespan="-1" interval="300000"/>
<memory max-count="-1"/>
</distributed-cache>
</cache-container>
</infinispan>