[KEYCLOAK-17173] - Support for script providers in keycloak.x

This commit is contained in:
Pedro Igor 2021-02-18 23:30:33 -03:00
parent 1dc0b005fe
commit ffadbc3ba3
7 changed files with 243 additions and 34 deletions

View file

@ -30,12 +30,16 @@ import org.keycloak.scripting.ScriptingProvider;
*/
public final class DeployedScriptPolicyFactory extends JSPolicyProviderFactory {
private final ScriptProviderMetadata metadata;
private ScriptProviderMetadata metadata;
public DeployedScriptPolicyFactory(ScriptProviderMetadata metadata) {
this.metadata = metadata;
}
public DeployedScriptPolicyFactory() {
// for reflection
}
@Override
public String getId() {
return metadata.getId();
@ -81,4 +85,12 @@ public final class DeployedScriptPolicyFactory extends JSPolicyProviderFactory {
policy.setDescription(metadata.getDescription());
super.onCreate(policy, representation, authorization);
}
public ScriptProviderMetadata getMetadata() {
return metadata;
}
public void setMetadata(ScriptProviderMetadata metadata) {
this.metadata = metadata;
}
}

View file

@ -59,10 +59,18 @@ public class ScriptProviderMetadata {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getCode() {
return code;
}

View file

@ -19,12 +19,21 @@ package org.keycloak.quarkus.deployment;
import static org.keycloak.configuration.Configuration.getPropertyNames;
import static org.keycloak.configuration.Configuration.getRawValue;
import static org.keycloak.representations.provider.ScriptProviderDescriptor.AUTHENTICATORS;
import static org.keycloak.representations.provider.ScriptProviderDescriptor.MAPPERS;
import static org.keycloak.representations.provider.ScriptProviderDescriptor.POLICIES;
import javax.persistence.spi.PersistenceUnitTransactionType;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -32,6 +41,9 @@ import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.Properties;
import java.util.ServiceLoader;
import java.util.function.Function;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import io.quarkus.deployment.IsDevelopment;
import io.quarkus.deployment.builditem.HotDeploymentWatchedFileBuildItem;
@ -46,7 +58,12 @@ import org.hibernate.jpa.boot.spi.PersistenceUnitDescriptor;
import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.ResteasyDeployment;
import org.keycloak.Config;
import org.keycloak.authentication.AuthenticatorSpi;
import org.keycloak.authentication.authenticators.browser.DeployedScriptAuthenticatorFactory;
import org.keycloak.authorization.policy.provider.PolicySpi;
import org.keycloak.authorization.policy.provider.js.DeployedScriptPolicyFactory;
import org.keycloak.common.Profile;
import org.keycloak.common.util.StreamUtil;
import org.keycloak.config.ConfigProviderFactory;
import org.keycloak.configuration.Configuration;
import org.keycloak.configuration.KeycloakConfigSourceProvider;
@ -54,6 +71,8 @@ import org.keycloak.configuration.MicroProfileConfigProvider;
import org.keycloak.connections.jpa.DefaultJpaConnectionProviderFactory;
import org.keycloak.connections.jpa.updater.liquibase.LiquibaseJpaUpdaterProviderFactory;
import org.keycloak.connections.jpa.updater.liquibase.conn.DefaultLiquibaseConnectionProvider;
import org.keycloak.protocol.ProtocolMapperSpi;
import org.keycloak.protocol.oidc.mappers.DeployedScriptOIDCProtocolMapper;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.provider.KeycloakDeploymentInfo;
import org.keycloak.provider.Provider;
@ -70,18 +89,42 @@ import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.builditem.FeatureBuildItem;
import io.quarkus.hibernate.orm.deployment.PersistenceUnitDescriptorBuildItem;
import io.quarkus.vertx.http.deployment.FilterBuildItem;
import org.keycloak.representations.provider.ScriptProviderDescriptor;
import org.keycloak.representations.provider.ScriptProviderMetadata;
import org.keycloak.services.NotFoundHandler;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.health.KeycloakMetricsHandler;
import org.keycloak.services.resources.KeycloakApplication;
import org.keycloak.transaction.JBossJtaTransactionManagerLookup;
import org.keycloak.util.Environment;
import org.keycloak.util.JsonSerialization;
class KeycloakProcessor {
private static final Logger logger = Logger.getLogger(KeycloakProcessor.class);
private static final String JAR_FILE_SEPARATOR = "!/";
private static final String DEFAULT_HEALTH_ENDPOINT = "/health";
private static final Map<String, Function<ScriptProviderMetadata, ProviderFactory>> DEPLOYEABLE_SCRIPT_PROVIDERS = new HashMap<>();
private static final String KEYCLOAK_SCRIPTS_JSON_PATH = "META-INF/keycloak-scripts.json";
static {
DEPLOYEABLE_SCRIPT_PROVIDERS.put(AUTHENTICATORS, KeycloakProcessor::registerScriptAuthenticator);
DEPLOYEABLE_SCRIPT_PROVIDERS.put(POLICIES, KeycloakProcessor::registerScriptPolicy);
DEPLOYEABLE_SCRIPT_PROVIDERS.put(MAPPERS, KeycloakProcessor::registerScriptMapper);
}
private static ProviderFactory registerScriptAuthenticator(ScriptProviderMetadata metadata) {
return new DeployedScriptAuthenticatorFactory(metadata);
}
private static ProviderFactory registerScriptPolicy(ScriptProviderMetadata metadata) {
return new DeployedScriptPolicyFactory(metadata);
}
private static ProviderFactory registerScriptMapper(ScriptProviderMetadata metadata) {
return new DeployedScriptOIDCProtocolMapper(metadata);
}
@BuildStep
FeatureBuildItem getFeature() {
@ -125,8 +168,9 @@ class KeycloakProcessor {
Profile.setInstance(recorder.createProfile());
Map<Spi, Map<Class<? extends Provider>, Map<String, Class<? extends ProviderFactory>>>> factories = new HashMap<>();
Map<Class<? extends Provider>, String> defaultProviders = new HashMap<>();
Map<String, ProviderFactory> preConfiguredProviders = new HashMap<>();
for (Map.Entry<Spi, Map<Class<? extends Provider>, Map<String, ProviderFactory>>> entry : loadFactories()
for (Map.Entry<Spi, Map<Class<? extends Provider>, Map<String, ProviderFactory>>> entry : loadFactories(preConfiguredProviders)
.entrySet()) {
checkProviders(entry.getKey(), entry.getValue(), defaultProviders);
@ -139,7 +183,7 @@ class KeycloakProcessor {
}
}
recorder.configSessionFactory(factories, defaultProviders, Environment.isRebuild());
recorder.configSessionFactory(factories, defaultProviders, preConfiguredProviders, Environment.isRebuild());
}
/**
@ -245,15 +289,22 @@ class KeycloakProcessor {
hotFiles.produce(new HotDeploymentWatchedFileBuildItem("META-INF/keycloak.properties"));
}
private Map<Spi, Map<Class<? extends Provider>, Map<String, ProviderFactory>>> loadFactories() {
private Map<Spi, Map<Class<? extends Provider>, Map<String, ProviderFactory>>> loadFactories(
Map<String, ProviderFactory> preConfiguredProviders) {
loadConfig();
ProviderManager pm = new ProviderManager(KeycloakDeploymentInfo.create().services(), new BuildClassLoader());
BuildClassLoader providerClassLoader = new BuildClassLoader();
ProviderManager pm = new ProviderManager(KeycloakDeploymentInfo.create().services(), providerClassLoader);
Map<Spi, Map<Class<? extends Provider>, Map<String, ProviderFactory>>> factories = new HashMap<>();
for (Spi spi : pm.loadSpis()) {
Map<Class<? extends Provider>, Map<String, ProviderFactory>> providers = new HashMap<>();
List<ProviderFactory> loadedFactories = new ArrayList<>(pm.load(spi));
Map<String, ProviderFactory> deployedScriptProviders = loadDeployedScriptProviders(providerClassLoader, spi);
for (ProviderFactory factory : pm.load(spi)) {
loadedFactories.addAll(deployedScriptProviders.values());
preConfiguredProviders.putAll(deployedScriptProviders);
for (ProviderFactory factory : loadedFactories) {
if (Arrays.asList(
JBossJtaTransactionManagerLookup.class,
DefaultJpaConnectionProviderFactory.class,
@ -282,6 +333,86 @@ class KeycloakProcessor {
return factories;
}
private Map<String, ProviderFactory> loadDeployedScriptProviders(BuildClassLoader providerClassLoader, Spi spi) {
Map<String, ProviderFactory> providers = new HashMap<>();
if (supportsDeployeableScripts(spi)) {
try {
Enumeration<URL> urls = providerClassLoader.getResources(KEYCLOAK_SCRIPTS_JSON_PATH);
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
int fileSeparator = url.getFile().indexOf(JAR_FILE_SEPARATOR);
if (fileSeparator != -1) {
JarFile jarFile = new JarFile(url.getFile().substring("file:".length(), fileSeparator));
JarEntry descriptorEntry = jarFile.getJarEntry(KEYCLOAK_SCRIPTS_JSON_PATH);
ScriptProviderDescriptor descriptor;
try (InputStream is = jarFile.getInputStream(descriptorEntry)) {
descriptor = JsonSerialization.readValue(is, ScriptProviderDescriptor.class);
}
for (Map.Entry<String, List<ScriptProviderMetadata>> entry : descriptor.getProviders().entrySet()) {
if (isScriptForSpi(spi, entry.getKey())) {
for (ScriptProviderMetadata metadata : entry.getValue()) {
ProviderFactory provider = createDeployableScriptProvider(jarFile, entry, metadata);
providers.put(metadata.getId(), provider);
}
}
}
}
}
} catch (IOException e) {
throw new RuntimeException("Failed to discover script providers", e);
}
}
return providers;
}
private ProviderFactory createDeployableScriptProvider(JarFile jarFile, Map.Entry<String, List<ScriptProviderMetadata>> entry,
ScriptProviderMetadata metadata) throws IOException {
String fileName = metadata.getFileName();
if (fileName == null) {
throw new RuntimeException("You must provide the script file name");
}
JarEntry scriptFile = jarFile.getJarEntry(fileName);
try (InputStream in = jarFile.getInputStream(scriptFile)) {
metadata.setCode(StreamUtil.readString(in, StandardCharsets.UTF_8));
}
metadata.setId(new StringBuilder("script").append("-").append(fileName).toString());
String name = metadata.getName();
if (name == null) {
name = fileName;
}
metadata.setName(name);
return DEPLOYEABLE_SCRIPT_PROVIDERS.get(entry.getKey()).apply(metadata);
}
private boolean isScriptForSpi(Spi spi, String type) {
if (spi instanceof ProtocolMapperSpi && MAPPERS.equals(type)) {
return true;
} else if (spi instanceof PolicySpi && POLICIES.equals(type)) {
return true;
} else if (spi instanceof AuthenticatorSpi && AUTHENTICATORS.equals(type)) {
return true;
}
return false;
}
private boolean supportsDeployeableScripts(Spi spi) {
return spi instanceof ProtocolMapperSpi || spi instanceof PolicySpi || spi instanceof AuthenticatorSpi;
}
private boolean isEnabled(ProviderFactory factory, Config.Scope scope) {
if (!scope.getBoolean("enabled", true)) {
return false;

View file

@ -31,13 +31,16 @@ public final class QuarkusKeycloakSessionFactory extends DefaultKeycloakSessionF
private static QuarkusKeycloakSessionFactory INSTANCE;
private final Boolean reaugmented;
private final Map<Spi, Map<Class<? extends Provider>, Map<String, Class<? extends ProviderFactory>>>> factories;
private Map<String, ProviderFactory> preConfiguredProviders;
public QuarkusKeycloakSessionFactory(
Map<Spi, Map<Class<? extends Provider>, Map<String, Class<? extends ProviderFactory>>>> factories,
Map<Class<? extends Provider>, String> defaultProviders,
Map<String, ProviderFactory> preConfiguredProviders,
Boolean reaugmented) {
this.provider = defaultProviders;
this.factories = factories;
this.preConfiguredProviders = preConfiguredProviders;
this.reaugmented = reaugmented;
}
@ -54,7 +57,12 @@ public final class QuarkusKeycloakSessionFactory extends DefaultKeycloakSessionF
for (Spi spi : spis) {
for (Map<String, Class<? extends ProviderFactory>> factoryClazz : factories.get(spi).values()) {
for (Map.Entry<String, Class<? extends ProviderFactory>> entry : factoryClazz.entrySet()) {
ProviderFactory factory = lookupProviderFactory(entry.getValue());
ProviderFactory factory = preConfiguredProviders.get(entry.getKey());
if (factory == null) {
factory = lookupProviderFactory(entry.getValue());
}
Config.Scope scope = Config.scope(spi.getName(), factory.getId());
factory.init(scope);

View file

@ -77,9 +77,10 @@ public class KeycloakRecorder {
public void configSessionFactory(
Map<Spi, Map<Class<? extends Provider>, Map<String, Class<? extends ProviderFactory>>>> factories,
Map<Class<? extends Provider>, String> defaultProviders,
Map<String, ProviderFactory> preConfiguredProviders,
Boolean reaugmented) {
Profile.setInstance(createProfile());
QuarkusKeycloakSessionFactory.setInstance(new QuarkusKeycloakSessionFactory(factories, defaultProviders, reaugmented));
QuarkusKeycloakSessionFactory.setInstance(new QuarkusKeycloakSessionFactory(factories, defaultProviders, preConfiguredProviders, reaugmented));
}
/**

View file

@ -16,13 +16,17 @@
*/
package org.keycloak.authentication.authenticators.browser;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import org.keycloak.Config;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.Authenticator;
import org.keycloak.common.Profile;
import org.keycloak.models.AuthenticatorConfigModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.provider.ScriptProviderMetadata;
/**
@ -30,17 +34,16 @@ import org.keycloak.representations.provider.ScriptProviderMetadata;
*/
public final class DeployedScriptAuthenticatorFactory extends ScriptBasedAuthenticatorFactory {
private final AuthenticatorConfigModel model;
private ScriptProviderMetadata metadata;
private AuthenticatorConfigModel model;
private List<ProviderConfigProperty> configProperties;
public DeployedScriptAuthenticatorFactory(ScriptProviderMetadata metadata) {
model = new AuthenticatorConfigModel();
this.metadata = metadata;
}
model.setId(metadata.getId());
model.setAlias(metadata.getName());
model.setConfig(new HashMap<>());
model.getConfig().put("scriptName", metadata.getName());
model.getConfig().put("scriptCode", metadata.getCode());
model.getConfig().put("scriptDescription", metadata.getDescription());
public DeployedScriptAuthenticatorFactory() {
// for reflection
}
@Override
@ -55,7 +58,7 @@ public final class DeployedScriptAuthenticatorFactory extends ScriptBasedAuthent
@Override
public String getId() {
return model.getId();
return metadata.getId();
}
@Override
@ -82,4 +85,36 @@ public final class DeployedScriptAuthenticatorFactory extends ScriptBasedAuthent
public boolean isSupported() {
return Profile.isFeatureEnabled(Profile.Feature.SCRIPTS);
}
@Override
public void init(Config.Scope config) {
model = createModel(metadata);
configProperties = super.getConfigProperties();
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return configProperties;
}
public void setMetadata(ScriptProviderMetadata metadata) {
this.metadata = metadata;
}
public ScriptProviderMetadata getMetadata() {
return metadata;
}
private AuthenticatorConfigModel createModel(ScriptProviderMetadata metadata) {
AuthenticatorConfigModel model = new AuthenticatorConfigModel();
model.setId(metadata.getId());
model.setAlias(metadata.getName());
model.setConfig(new HashMap<>());
model.getConfig().put("scriptName", metadata.getName());
model.getConfig().put("scriptCode", metadata.getCode());
model.getConfig().put("scriptDescription", metadata.getDescription());
return model;
}
}

View file

@ -18,6 +18,7 @@ package org.keycloak.protocol.oidc.mappers;
import java.util.List;
import org.keycloak.Config;
import org.keycloak.common.Profile;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.protocol.ProtocolMapperUtils;
@ -25,30 +26,20 @@ import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import org.keycloak.representations.provider.ScriptProviderMetadata;
public final class DeployedScriptOIDCProtocolMapper extends ScriptBasedOIDCProtocolMapper {
public class DeployedScriptOIDCProtocolMapper extends ScriptBasedOIDCProtocolMapper {
private static final List<ProviderConfigProperty> configProperties;
private List<ProviderConfigProperty> configProperties;
static {
configProperties = ProviderConfigurationBuilder.create()
.property()
.name(ProtocolMapperUtils.MULTIVALUED)
.label(ProtocolMapperUtils.MULTIVALUED_LABEL)
.helpText(ProtocolMapperUtils.MULTIVALUED_HELP_TEXT)
.type(ProviderConfigProperty.BOOLEAN_TYPE)
.defaultValue(false)
.add()
.build();
OIDCAttributeMapperHelper.addAttributeConfig(configProperties, UserPropertyMapper.class);
}
private final ScriptProviderMetadata metadata;
protected ScriptProviderMetadata metadata;
public DeployedScriptOIDCProtocolMapper(ScriptProviderMetadata metadata) {
this.metadata = metadata;
}
public DeployedScriptOIDCProtocolMapper() {
// for reflection
}
@Override
public String getId() {
return metadata.getId();
@ -69,6 +60,21 @@ public final class DeployedScriptOIDCProtocolMapper extends ScriptBasedOIDCProto
return metadata.getCode();
}
@Override
public void init(Config.Scope config) {
configProperties = ProviderConfigurationBuilder.create()
.property()
.name(ProtocolMapperUtils.MULTIVALUED)
.label(ProtocolMapperUtils.MULTIVALUED_LABEL)
.helpText(ProtocolMapperUtils.MULTIVALUED_HELP_TEXT)
.type(ProviderConfigProperty.BOOLEAN_TYPE)
.defaultValue(false)
.add()
.build();
OIDCAttributeMapperHelper.addAttributeConfig(configProperties, UserPropertyMapper.class);
}
public List<ProviderConfigProperty> getConfigProperties() {
return configProperties;
}
@ -77,4 +83,12 @@ public final class DeployedScriptOIDCProtocolMapper extends ScriptBasedOIDCProto
public boolean isSupported() {
return Profile.isFeatureEnabled(Profile.Feature.SCRIPTS);
}
public void setMetadata(ScriptProviderMetadata metadata) {
this.metadata = metadata;
}
public ScriptProviderMetadata getMetadata() {
return metadata;
}
}