From 3f9cebca3986b465e77de720764936003b234a58 Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Fri, 22 Mar 2024 07:45:08 +0100 Subject: [PATCH] Ability to set the default provider for an SPI (#28135) Closes #28134 Signed-off-by: stianst --- core/src/main/java/org/keycloak/Config.java | 16 ++ .../guides/server/configuration-provider.adoc | 26 ++- .../quarkus/deployment/KeycloakProcessor.java | 36 +--- .../MicroProfileConfigProvider.java | 5 + .../DefaultKeycloakSessionFactory.java | 58 ++++--- .../services/util/JsonConfigProvider.java | 6 + .../protocol/saml/profile/util/SoapTest.java | 5 + .../DefaultKeycloakSessionFactoryTest.java | 154 ++++++++++++++++++ .../org/keycloak/testsuite/model/Config.java | 4 + 9 files changed, 256 insertions(+), 54 deletions(-) create mode 100644 services/src/test/java/org/keycloak/services/DefaultKeycloakSessionFactoryTest.java diff --git a/core/src/main/java/org/keycloak/Config.java b/core/src/main/java/org/keycloak/Config.java index 3eb1a760e2..01245540f3 100755 --- a/core/src/main/java/org/keycloak/Config.java +++ b/core/src/main/java/org/keycloak/Config.java @@ -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) { return configProvider.scope(scope); } @@ -51,6 +60,8 @@ public class Config { String getProvider(String spi); + String getDefaultProvider(String spi); + Scope scope(String... scope); } @@ -62,6 +73,11 @@ public class Config { return System.getProperties().getProperty("keycloak." + spi + ".provider"); } + @Override + public String getDefaultProvider(String spi) { + return System.getProperties().getProperty("keycloak." + spi + ".provider.default"); + } + @Override public Scope scope(String... scope) { StringBuilder sb = new StringBuilder(); diff --git a/docs/guides/server/configuration-provider.adoc b/docs/guides/server/configuration-provider.adoc index 192d185b8a..38cb1bf13d 100644 --- a/docs/guides/server/configuration-provider.adoc +++ b/docs/guides/server/configuration-provider.adoc @@ -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 <@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. -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"/> -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 diff --git a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java index a8f15bc578..107bff180e 100644 --- a/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java +++ b/quarkus/deployment/src/main/java/org/keycloak/quarkus/deployment/KeycloakProcessor.java @@ -101,6 +101,7 @@ import org.keycloak.quarkus.runtime.themes.FlatClasspathThemeResourceProviderFac import org.keycloak.representations.provider.ScriptProviderDescriptor; import org.keycloak.representations.provider.ScriptProviderMetadata; import org.keycloak.representations.userprofile.config.UPConfig; +import org.keycloak.services.DefaultKeycloakSessionFactory; import org.keycloak.services.ServicesLogger; import org.keycloak.services.resources.JsResource; import org.keycloak.services.resources.LoadBalancerResource; @@ -877,38 +878,19 @@ class KeycloakProcessor { private void checkProviders(Spi spi, Map, Map> factoriesMap, Map, String> defaultProviders) { - String defaultProvider = Config.getProvider(spi.getName()); - - if (defaultProvider != null) { + String provider = Config.getProvider(spi.getName()); + if (provider != null) { Map map = factoriesMap.get(spi.getProviderClass()); - if (map == null || map.get(defaultProvider) == null) { - throw new RuntimeException("Failed to find provider " + defaultProvider + " for " + spi.getName()); + if (map == null || map.get(provider) == null) { + throw new RuntimeException("Failed to find provider " + provider + " for " + spi.getName()); } + defaultProviders.put(spi.getProviderClass(), provider); } else { Map factories = factoriesMap.get(spi.getProviderClass()); - if (factories != null && factories.size() == 1) { - defaultProvider = factories.values().iterator().next().getId(); + String defaultProvider = DefaultKeycloakSessionFactory.resolveDefaultProvider(factories, spi); + if (defaultProvider != null) { + defaultProviders.put(spi.getProviderClass(), defaultProvider); } - - if (factories != null) { - if (defaultProvider == null) { - Optional 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()); } } diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/MicroProfileConfigProvider.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/MicroProfileConfigProvider.java index 70bf858c79..7e5f8c58d1 100644 --- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/MicroProfileConfigProvider.java +++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/MicroProfileConfigProvider.java @@ -53,6 +53,11 @@ public class MicroProfileConfigProvider implements Config.ConfigProvider { return scope(spi).get("provider"); } + @Override + public String getDefaultProvider(String spi) { + return scope(spi).get("provider.default"); + } + @Override public Config.Scope scope(String... scope) { return new MicroProfileScope(scope); diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java index 3c50bc05b8..989b4231be 100755 --- a/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java +++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java @@ -227,35 +227,49 @@ public abstract class DefaultKeycloakSessionFactory implements KeycloakSessionFa provider.clear(); for (Spi spi : spis) { - String defaultProvider = Config.getProvider(spi.getName()); - if (defaultProvider != null) { - if (getProviderFactory(spi.getProviderClass(), defaultProvider) == null) { - throw new RuntimeException("Failed to find provider " + defaultProvider + " for " + spi.getName()); + String provider = Config.getProvider(spi.getName()); + if (provider != null) { + if (getProviderFactory(spi.getProviderClass(), provider) == null) { + throw new RuntimeException("Failed to find provider " + provider + " for " + spi.getName()); } + this.provider.put(spi.getProviderClass(), provider); } else { Map factories = factoriesMap.get(spi.getProviderClass()); - if (factories != null && factories.size() == 1) { - defaultProvider = factories.values().iterator().next().getId(); - } - - if (defaultProvider == null) { - Optional 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"; + String defaultProvider = resolveDefaultProvider(factories, spi); + if (defaultProvider != null) { + this.provider.put(spi.getProviderClass(), defaultProvider); } } + } + } - if (defaultProvider != null) { - this.provider.put(spi.getProviderClass(), defaultProvider); - logger.debugv("Set default provider for {0} to {1}", spi.getName(), defaultProvider); - } else { - logger.debugv("No default provider for {0}", spi.getName()); + public static String resolveDefaultProvider(Map factories, Spi spi) { + if (factories == null) { + return null; + } + + 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 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; } } diff --git a/services/src/main/java/org/keycloak/services/util/JsonConfigProvider.java b/services/src/main/java/org/keycloak/services/util/JsonConfigProvider.java index 63cf394a0e..1854c01850 100755 --- a/services/src/main/java/org/keycloak/services/util/JsonConfigProvider.java +++ b/services/src/main/java/org/keycloak/services/util/JsonConfigProvider.java @@ -44,6 +44,12 @@ public class JsonConfigProvider implements Config.ConfigProvider { 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 public Config.Scope scope(String... path) { return new JsonScope(getNode(config, path)); diff --git a/services/src/test/java/org/keycloak/protocol/saml/profile/util/SoapTest.java b/services/src/test/java/org/keycloak/protocol/saml/profile/util/SoapTest.java index 99b7747b1e..113b569728 100644 --- a/services/src/test/java/org/keycloak/protocol/saml/profile/util/SoapTest.java +++ b/services/src/test/java/org/keycloak/protocol/saml/profile/util/SoapTest.java @@ -146,6 +146,11 @@ public class SoapTest { return null; } + @Override + public String getDefaultProvider(String spi) { + return null; + } + @Override public Config.Scope scope(String... scope) { if (scope.length == 2 && "connectionsHttpClient".equals(scope[0]) && "default".equals(scope[1])) { diff --git a/services/src/test/java/org/keycloak/services/DefaultKeycloakSessionFactoryTest.java b/services/src/test/java/org/keycloak/services/DefaultKeycloakSessionFactoryTest.java new file mode 100644 index 0000000000..3a8db0a6c5 --- /dev/null +++ b/services/src/test/java/org/keycloak/services/DefaultKeycloakSessionFactoryTest.java @@ -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 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 getProviderClass() { + return null; + } + + @Override + public Class getProviderFactoryClass() { + return null; + } + } + +} diff --git a/testsuite/model/src/main/java/org/keycloak/testsuite/model/Config.java b/testsuite/model/src/main/java/org/keycloak/testsuite/model/Config.java index a034cac90f..3e830f09ee 100644 --- a/testsuite/model/src/main/java/org/keycloak/testsuite/model/Config.java +++ b/testsuite/model/src/main/java/org/keycloak/testsuite/model/Config.java @@ -148,6 +148,10 @@ public class Config implements ConfigProvider { return getConfig().get(spiName + ".provider"); } + public String getDefaultProvider(String spiName) { + return getConfig().get(spiName + ".provider.default"); + } + public Map getConfig() { return useGlobalConfigurationFunc.getAsBoolean() ? defaultProperties : properties.get(); }