Ability to set the default provider for an SPI (#28135)

Closes #28134

Signed-off-by: stianst <stianst@gmail.com>
This commit is contained in:
Stian Thorgersen 2024-03-22 07:45:08 +01:00 committed by GitHub
parent cae92cbe8c
commit 3f9cebca39
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 256 additions and 54 deletions

View file

@ -43,6 +43,15 @@ public class Config {
} }
} }
public static String getDefaultProvider(String spi) {
String provider = configProvider.getDefaultProvider(spi);
if (provider == null || provider.trim().equals("")) {
return null;
} else {
return provider;
}
}
public static Scope scope(String... scope) { public static Scope scope(String... scope) {
return configProvider.scope(scope); return configProvider.scope(scope);
} }
@ -51,6 +60,8 @@ public class Config {
String getProvider(String spi); String getProvider(String spi);
String getDefaultProvider(String spi);
Scope scope(String... scope); Scope scope(String... scope);
} }
@ -62,6 +73,11 @@ public class Config {
return System.getProperties().getProperty("keycloak." + spi + ".provider"); return System.getProperties().getProperty("keycloak." + spi + ".provider");
} }
@Override
public String getDefaultProvider(String spi) {
return System.getProperties().getProperty("keycloak." + spi + ".provider.default");
}
@Override @Override
public Scope scope(String... scope) { public Scope scope(String... scope) {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();

View file

@ -45,17 +45,33 @@ Provider configuration options are provided when starting the server. See all su
.Setting the `connection-pool-size` for the `default` provider of the `connections-http-client` SPI .Setting the `connection-pool-size` for the `default` provider of the `connections-http-client` SPI
<@kc.start parameters="--spi-connections-http-client-default-connection-pool-size=10"/> <@kc.start parameters="--spi-connections-http-client-default-connection-pool-size=10"/>
== Configuring a default provider == Configuring a single provider for an SPI
Depending on the SPI, multiple provider implementations can co-exist but only one of them is going to be used at runtime. Depending on the SPI, multiple provider implementations can co-exist but only one of them is going to be used at runtime.
For these SPIs, a default provider is the primary implementation that is going to be active and used at runtime. For these SPIs, a specific provider is the primary implementation that is going to be active and used at runtime.
To configure a provider as the default you should run the `build` command as follows: To configure a provider as the single provider you should run the `build` command as follows:
.Marking the `mycustomprovider` provider as the default provider for the `email-template` SPI .Marking the `mycustomprovider` provider as the single provider for the `email-template` SPI
<@kc.build parameters="--spi-email-template-provider=mycustomprovider"/> <@kc.build parameters="--spi-email-template-provider=mycustomprovider"/>
In the example above, we are using the `provider` property to set the id of the provider we want to mark as the default. == Configuring a default provider for an SPI
Depending on the SPI, multiple provider implementations can co-exist and one is used by default.
For these SPIs, a specific provider is the default implementation that is going to selected unless a specific provider
is requested.
The following logic is used to determine the default provider:
1. The explicitly configured default provider
2. The provider with the highest order (providers with order <= 0 are ignored)
3. The provider with the id set to `default`
To configure a provider as the default provider you should run the `build` command as follows:
.Marking the `mycustomhash` provider as the default provider for the `password-hashing` SPI
<@kc.build parameters="--spi-password-hashing-provider-default=mycustomprovider"/>
== Enabling and disabling a provider == Enabling and disabling a provider

View file

@ -101,6 +101,7 @@ import org.keycloak.quarkus.runtime.themes.FlatClasspathThemeResourceProviderFac
import org.keycloak.representations.provider.ScriptProviderDescriptor; import org.keycloak.representations.provider.ScriptProviderDescriptor;
import org.keycloak.representations.provider.ScriptProviderMetadata; import org.keycloak.representations.provider.ScriptProviderMetadata;
import org.keycloak.representations.userprofile.config.UPConfig; import org.keycloak.representations.userprofile.config.UPConfig;
import org.keycloak.services.DefaultKeycloakSessionFactory;
import org.keycloak.services.ServicesLogger; import org.keycloak.services.ServicesLogger;
import org.keycloak.services.resources.JsResource; import org.keycloak.services.resources.JsResource;
import org.keycloak.services.resources.LoadBalancerResource; import org.keycloak.services.resources.LoadBalancerResource;
@ -877,38 +878,19 @@ class KeycloakProcessor {
private void checkProviders(Spi spi, private void checkProviders(Spi spi,
Map<Class<? extends Provider>, Map<String, ProviderFactory>> factoriesMap, Map<Class<? extends Provider>, Map<String, ProviderFactory>> factoriesMap,
Map<Class<? extends Provider>, String> defaultProviders) { Map<Class<? extends Provider>, String> defaultProviders) {
String defaultProvider = Config.getProvider(spi.getName()); String provider = Config.getProvider(spi.getName());
if (provider != null) {
if (defaultProvider != null) {
Map<String, ProviderFactory> map = factoriesMap.get(spi.getProviderClass()); Map<String, ProviderFactory> map = factoriesMap.get(spi.getProviderClass());
if (map == null || map.get(defaultProvider) == null) { if (map == null || map.get(provider) == null) {
throw new RuntimeException("Failed to find provider " + defaultProvider + " for " + spi.getName()); throw new RuntimeException("Failed to find provider " + provider + " for " + spi.getName());
} }
defaultProviders.put(spi.getProviderClass(), provider);
} else { } else {
Map<String, ProviderFactory> factories = factoriesMap.get(spi.getProviderClass()); Map<String, ProviderFactory> factories = factoriesMap.get(spi.getProviderClass());
if (factories != null && factories.size() == 1) { String defaultProvider = DefaultKeycloakSessionFactory.resolveDefaultProvider(factories, spi);
defaultProvider = factories.values().iterator().next().getId(); if (defaultProvider != null) {
defaultProviders.put(spi.getProviderClass(), defaultProvider);
} }
if (factories != null) {
if (defaultProvider == null) {
Optional<ProviderFactory> highestPriority = factories.values().stream()
.max(Comparator.comparing(ProviderFactory::order));
if (highestPriority.isPresent() && highestPriority.get().order() > 0) {
defaultProvider = highestPriority.get().getId();
}
}
}
if (defaultProvider == null && (factories == null || factories.containsKey("default"))) {
defaultProvider = "default";
}
}
if (defaultProvider != null) {
defaultProviders.put(spi.getProviderClass(), defaultProvider);
} else {
logger.debugv("No default provider for {0}", spi.getName());
} }
} }

View file

@ -53,6 +53,11 @@ public class MicroProfileConfigProvider implements Config.ConfigProvider {
return scope(spi).get("provider"); return scope(spi).get("provider");
} }
@Override
public String getDefaultProvider(String spi) {
return scope(spi).get("provider.default");
}
@Override @Override
public Config.Scope scope(String... scope) { public Config.Scope scope(String... scope) {
return new MicroProfileScope(scope); return new MicroProfileScope(scope);

View file

@ -227,35 +227,49 @@ public abstract class DefaultKeycloakSessionFactory implements KeycloakSessionFa
provider.clear(); provider.clear();
for (Spi spi : spis) { for (Spi spi : spis) {
String defaultProvider = Config.getProvider(spi.getName()); String provider = Config.getProvider(spi.getName());
if (defaultProvider != null) { if (provider != null) {
if (getProviderFactory(spi.getProviderClass(), defaultProvider) == null) { if (getProviderFactory(spi.getProviderClass(), provider) == null) {
throw new RuntimeException("Failed to find provider " + defaultProvider + " for " + spi.getName()); throw new RuntimeException("Failed to find provider " + provider + " for " + spi.getName());
} }
this.provider.put(spi.getProviderClass(), provider);
} else { } else {
Map<String, ProviderFactory> factories = factoriesMap.get(spi.getProviderClass()); Map<String, ProviderFactory> factories = factoriesMap.get(spi.getProviderClass());
if (factories != null && factories.size() == 1) { String defaultProvider = resolveDefaultProvider(factories, spi);
defaultProvider = factories.values().iterator().next().getId(); if (defaultProvider != null) {
} this.provider.put(spi.getProviderClass(), defaultProvider);
if (defaultProvider == null) {
Optional<ProviderFactory> highestPriority = factories.values().stream().max(Comparator.comparing(ProviderFactory::order));
if (highestPriority.isPresent() && highestPriority.get().order() > 0) {
defaultProvider = highestPriority.get().getId();
}
}
if (defaultProvider == null && factories.containsKey("default")) {
defaultProvider = "default";
} }
} }
}
}
if (defaultProvider != null) { public static String resolveDefaultProvider(Map<String, ProviderFactory> factories, Spi spi) {
this.provider.put(spi.getProviderClass(), defaultProvider); if (factories == null) {
logger.debugv("Set default provider for {0} to {1}", spi.getName(), defaultProvider); return null;
} else { }
logger.debugv("No default provider for {0}", spi.getName());
String defaultProvider = Config.getDefaultProvider(spi.getName());
if (defaultProvider != null) {
if (!factories.containsKey(defaultProvider)) {
throw new RuntimeException("Failed to find provider " + defaultProvider + " for " + spi.getName());
} }
} else if (factories.size() == 1) {
defaultProvider = factories.values().iterator().next().getId();
} else {
Optional<ProviderFactory> highestPriority = factories.values().stream().filter(p -> p.order() > 0).max(Comparator.comparing(ProviderFactory::order));
if (highestPriority.isPresent()) {
defaultProvider = highestPriority.get().getId();
} else if (factories.containsKey("default")) {
defaultProvider = "default";
}
}
if (defaultProvider != null) {
logger.debugv("Set default provider for {0} to {1}", spi.getName(), defaultProvider);
return defaultProvider;
} else {
logger.debugv("No default provider for {0}", spi.getName());
return null;
} }
} }

View file

@ -44,6 +44,12 @@ public class JsonConfigProvider implements Config.ConfigProvider {
return n != null ? replaceProperties(n.textValue()) : null; return n != null ? replaceProperties(n.textValue()) : null;
} }
@Override
public String getDefaultProvider(String spi) {
JsonNode n = getNode(config, spi, "provider-default");
return n != null ? replaceProperties(n.textValue()) : null;
}
@Override @Override
public Config.Scope scope(String... path) { public Config.Scope scope(String... path) {
return new JsonScope(getNode(config, path)); return new JsonScope(getNode(config, path));

View file

@ -146,6 +146,11 @@ public class SoapTest {
return null; return null;
} }
@Override
public String getDefaultProvider(String spi) {
return null;
}
@Override @Override
public Config.Scope scope(String... scope) { public Config.Scope scope(String... scope) {
if (scope.length == 2 && "connectionsHttpClient".equals(scope[0]) && "default".equals(scope[1])) { if (scope.length == 2 && "connectionsHttpClient".equals(scope[0]) && "default".equals(scope[1])) {

View file

@ -0,0 +1,154 @@
package org.keycloak.services;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
import java.util.HashMap;
import java.util.Map;
public class DefaultKeycloakSessionFactoryTest {
private DummyConfigurationProvider config;
private DummySpi spi;
@Before
public void before() {
config = new DummyConfigurationProvider();
Config.init(config);
}
@After
public void after() {
Config.init(new Config.SystemPropertiesConfigProvider());
}
@Test
public void defaultProviderFromConfigTest() {
Map<String, ProviderFactory> map = new HashMap<>(Map.of(
"two", new DummyProviderFactory("two", 2),
"one", new DummyProviderFactory("one", 0),
"three", new DummyProviderFactory("three", 3)));
spi = new DummySpi();
// Default provider configured
config.defaultProvider = "one";
Assert.assertEquals("one", DefaultKeycloakSessionFactory.resolveDefaultProvider(map, spi));
// Highest priority selected
config.defaultProvider = null;
Assert.assertEquals("three", DefaultKeycloakSessionFactory.resolveDefaultProvider(map, spi));
// No default, with order=0
map.values().stream().forEach(p -> ((DummyProviderFactory) p).order = 0);
Assert.assertNull(DefaultKeycloakSessionFactory.resolveDefaultProvider(map, spi));
// Provider with id=default selected
map.put("default", new DummyProviderFactory("default", 0));
Assert.assertEquals("default", DefaultKeycloakSessionFactory.resolveDefaultProvider(map, spi));
// Default set if single provider exists
map.remove("default");
map.remove("two");
map.remove("three");
Assert.assertEquals("one", DefaultKeycloakSessionFactory.resolveDefaultProvider(map, spi));
// Throw error if default configured not found
config.defaultProvider = "nosuch";
try {
DefaultKeycloakSessionFactory.resolveDefaultProvider(map, spi);
Assert.fail("Expected exception");
} catch (RuntimeException e) {
Assert.assertEquals("Failed to find provider nosuch for dummy", e.getMessage());
}
}
private class DummyConfigurationProvider implements Config.ConfigProvider {
String defaultProvider;
@Override
public String getProvider(String spi) {
return null;
}
@Override
public String getDefaultProvider(String spi) {
return defaultProvider;
}
@Override
public Config.Scope scope(String... scope) {
return null;
}
}
private class DummyProviderFactory implements ProviderFactory {
private String id;
private int order;
public DummyProviderFactory(String id, int order) {
this.id = id;
this.order = order;
}
@Override
public Provider create(KeycloakSession session) {
return null;
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return id;
}
@Override
public int order() {
return order;
}
}
private class DummySpi implements Spi {
@Override
public boolean isInternal() {
return false;
}
@Override
public String getName() {
return "dummy";
}
@Override
public Class<? extends Provider> getProviderClass() {
return null;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return null;
}
}
}

View file

@ -148,6 +148,10 @@ public class Config implements ConfigProvider {
return getConfig().get(spiName + ".provider"); return getConfig().get(spiName + ".provider");
} }
public String getDefaultProvider(String spiName) {
return getConfig().get(spiName + ".provider.default");
}
public Map<String, String> getConfig() { public Map<String, String> getConfig() {
return useGlobalConfigurationFunc.getAsBoolean() ? defaultProperties : properties.get(); return useGlobalConfigurationFunc.getAsBoolean() ? defaultProperties : properties.get();
} }