KEYCLOAK-12162 Modularize config backends (#6499)

* KEYCLOAK-12162 - Modularize configuration backends

* - Use JsonSerialization
- simplify backend selection (no fallbacks)

* Remove unused org.wildfly.core:wildfly-controller dependency
This commit is contained in:
Dmitry Telegin 2019-11-22 17:23:04 +03:00 committed by Stian Thorgersen
parent 623f347263
commit 79074aa380
11 changed files with 304 additions and 78 deletions

View File

@ -25,11 +25,15 @@
</resources>
<dependencies>
<module name="com.fasterxml.jackson.core.jackson-databind"/>
<module name="javax.servlet.api"/>
<module name="org.keycloak.keycloak-common"/>
<module name="org.keycloak.keycloak-core"/>
<module name="org.keycloak.keycloak-server-spi"/>
<module name="org.keycloak.keycloak-server-spi-private"/>
<module name="org.keycloak.keycloak-services"/>
<module name="org.jboss.dmr"/>
<module name="org.jboss.logging"/>
<module name="org.jboss.modules"/>
<module name="org.jboss.resteasy.resteasy-jaxrs"/>
</dependencies>

View File

@ -0,0 +1,27 @@
/*
* 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.
*/
package org.keycloak.config;
import java.util.Optional;
import org.keycloak.Config;
public interface ConfigProviderFactory {
Optional<Config.ConfigProvider> create();
}

View File

@ -88,11 +88,6 @@
<artifactId>twitter4j-core</artifactId>
</dependency>
<dependency>
<groupId>org.wildfly.core</groupId>
<artifactId>wildfly-controller</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>

View File

@ -17,13 +17,10 @@
package org.keycloak.services.resources;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.jboss.dmr.ModelNode;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.common.util.Resteasy;
import org.keycloak.common.util.SystemEnvProperties;
import org.keycloak.config.ConfigProviderFactory;
import org.keycloak.exportimport.ExportImportManager;
import org.keycloak.migration.MigrationModelManager;
import org.keycloak.models.KeycloakSession;
@ -53,7 +50,6 @@ import org.keycloak.services.scheduled.ClearExpiredEvents;
import org.keycloak.services.scheduled.ClearExpiredUserSessions;
import org.keycloak.services.scheduled.ClusterAwareScheduledTaskRunner;
import org.keycloak.services.scheduled.ScheduledTaskRunner;
import org.keycloak.services.util.JsonConfigProvider;
import org.keycloak.services.util.ObjectMapperResolver;
import org.keycloak.timer.TimerProvider;
import org.keycloak.transaction.JtaTransactionManagerLookup;
@ -63,20 +59,15 @@ import javax.servlet.ServletContext;
import javax.transaction.SystemException;
import javax.transaction.Transaction;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.UriInfo;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.NoSuchElementException;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.StringTokenizer;
@ -88,15 +79,9 @@ import java.util.concurrent.atomic.AtomicBoolean;
* @version $Revision: 1 $
*/
public class KeycloakApplication extends Application {
// This param name is defined again in Keycloak Server Subsystem class
// org.keycloak.subsystem.server.extension.KeycloakServerDeploymentProcessor. We have this value in
// two places to avoid dependency between Keycloak Subsystem and Keycloak Services module.
public static final String KEYCLOAK_CONFIG_PARAM_NAME = "org.keycloak.server-subsystem.Config";
public static final String KEYCLOAK_EMBEDDED = "keycloak.embedded";
public static final String SERVER_CONTEXT_CONFIG_PROPERTY_OVERRIDES = "keycloak.server.context.config.property-overrides";
public static final AtomicBoolean BOOTSTRAP_ADMIN_USER = new AtomicBoolean(false);
private static final Logger logger = Logger.getLogger(KeycloakApplication.class);
@ -120,7 +105,7 @@ public class KeycloakApplication extends Application {
embedded = true;
}
loadConfig(context);
loadConfig();
this.sessionFactory = createSessionFactory();
@ -279,59 +264,18 @@ public class KeycloakApplication extends Application {
}
}
public static void loadConfig(ServletContext context) {
protected void loadConfig() {
ServiceLoader<ConfigProviderFactory> loader = ServiceLoader.load(ConfigProviderFactory.class, KeycloakApplication.class.getClassLoader());
try {
JsonNode node = null;
String dmrConfig = loadDmrConfig(context);
if (dmrConfig != null) {
node = new ObjectMapper().readTree(dmrConfig);
ServicesLogger.LOGGER.loadingFrom("standalone.xml or domain.xml");
}
String configDir = System.getProperty("jboss.server.config.dir");
if (node == null && configDir != null) {
File f = new File(configDir + File.separator + "keycloak-server.json");
if (f.isFile()) {
ServicesLogger.LOGGER.loadingFrom(f.getAbsolutePath());
node = new ObjectMapper().readTree(f);
}
}
if (node == null) {
URL resource = Thread.currentThread().getContextClassLoader().getResource("META-INF/keycloak-server.json");
if (resource != null) {
ServicesLogger.LOGGER.loadingFrom(resource);
node = new ObjectMapper().readTree(resource);
}
}
if (node != null) {
Map<String, String> propertyOverridesMap = new HashMap<>();
String propertyOverrides = context.getInitParameter(SERVER_CONTEXT_CONFIG_PROPERTY_OVERRIDES);
if (context.getInitParameter(SERVER_CONTEXT_CONFIG_PROPERTY_OVERRIDES) != null) {
JsonNode jsonObj = new ObjectMapper().readTree(propertyOverrides);
jsonObj.fields().forEachRemaining(e -> propertyOverridesMap.put(e.getKey(), e.getValue().asText()));
}
Properties properties = new SystemEnvProperties(propertyOverridesMap);
Config.init(new JsonConfigProvider(node, properties));
} else {
throw new RuntimeException("Keycloak config not found.");
}
} catch (IOException e) {
throw new RuntimeException("Failed to load config", e);
ConfigProviderFactory factory = loader.iterator().next();
logger.infov("Using ConfigProvider: {0}", factory.getClass().getName());
Config.init(factory.create().orElseThrow(() -> new RuntimeException("Failed to load Keycloak configuration")));
} catch (NoSuchElementException e) {
throw new RuntimeException("No valid ConfigProvider found");
}
}
private static String loadDmrConfig(ServletContext context) {
String dmrConfig = context.getInitParameter(KEYCLOAK_CONFIG_PARAM_NAME);
if (dmrConfig == null) return null;
ModelNode dmrConfigNode = ModelNode.fromString(dmrConfig);
if (dmrConfigNode.asPropertyList().isEmpty()) return null;
// note that we need to resolve expressions BEFORE we convert to JSON
return dmrConfigNode.resolve().toJSONString(true);
}
public static KeycloakSessionFactory createSessionFactory() {

View File

@ -0,0 +1,75 @@
/*
* 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.
*/
package org.keycloak.services.util;
import org.keycloak.config.ConfigProviderFactory;
import com.fasterxml.jackson.databind.JsonNode;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.Optional;
import java.util.Properties;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.common.util.SystemEnvProperties;
import org.keycloak.services.ServicesLogger;
import org.keycloak.util.JsonSerialization;
public abstract class JsonConfigProviderFactory implements ConfigProviderFactory {
private static final Logger LOG = Logger.getLogger(JsonConfigProviderFactory.class);
@Override
public Optional<Config.ConfigProvider> create() {
JsonNode node = null;
try {
String configDir = System.getProperty("jboss.server.config.dir");
if (configDir != null) {
File f = new File(configDir + File.separator + "keycloak-server.json");
if (f.isFile()) {
ServicesLogger.LOGGER.loadingFrom(f.getAbsolutePath());
node = JsonSerialization.mapper.readTree(f);
}
}
if (node == null) {
URL resource = Thread.currentThread().getContextClassLoader().getResource("META-INF/keycloak-server.json");
if (resource != null) {
ServicesLogger.LOGGER.loadingFrom(resource);
node = JsonSerialization.mapper.readTree(resource);
}
}
} catch (IOException e) {
LOG.warn("Failed to load JSON config", e);
}
return createJsonProvider(node);
}
protected Optional<Config.ConfigProvider> createJsonProvider(JsonNode node) {
return Optional.ofNullable(node).map(n -> new JsonConfigProvider(n, getProperties()));
}
protected Properties getProperties() {
return new SystemEnvProperties();
}
}

View File

@ -24,12 +24,10 @@ import java.security.cert.X509CRL;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import javax.security.auth.x500.X500Principal;
@ -46,7 +44,6 @@ import org.jboss.logging.Logger;
import org.keycloak.common.util.BouncyIntegration;
import org.keycloak.models.KeycloakSession;
import org.keycloak.truststore.TruststoreProvider;
import org.wildfly.security.x500.X500;
/**
* @author <a href="mailto:brat000012001@gmail.com">Peter Nalyvayko</a>

View File

@ -49,6 +49,7 @@ import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.services.filters.KeycloakSessionServletFilter;
import org.keycloak.services.managers.ApplianceBootstrap;
import org.keycloak.services.resources.KeycloakApplication;
import org.keycloak.testsuite.JsonConfigProviderFactory;
import org.keycloak.testsuite.KeycloakServer;
import org.keycloak.testsuite.TestKeycloakSessionServletFilter;
import org.keycloak.testsuite.utils.tls.TLSUtils;
@ -69,7 +70,7 @@ import org.xnio.SslClientAuthMode;
public class KeycloakOnUndertow implements DeployableContainer<KeycloakOnUndertowConfiguration> {
protected final Logger log = Logger.getLogger(this.getClass());
private KeycloakUndertowJaxrsServer undertow;
private KeycloakOnUndertowConfiguration configuration;
private KeycloakSessionFactory sessionFactory;
@ -90,7 +91,7 @@ public class KeycloakOnUndertow implements DeployableContainer<KeycloakOnUnderto
di.addInitParameter(KeycloakApplication.KEYCLOAK_EMBEDDED, "true");
if (configuration.getKeycloakConfigPropertyOverridesMap() != null) {
try {
di.addInitParameter(KeycloakApplication.SERVER_CONTEXT_CONFIG_PROPERTY_OVERRIDES,
di.addInitParameter(JsonConfigProviderFactory.SERVER_CONTEXT_CONFIG_PROPERTY_OVERRIDES,
JsonSerialization.writeValueAsString(configuration.getKeycloakConfigPropertyOverridesMap()));
} catch (IOException ex) {
throw new RuntimeException(ex);

View File

@ -0,0 +1,57 @@
/*
* 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.
*/
package org.keycloak.testsuite;
import com.fasterxml.jackson.databind.JsonNode;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import javax.servlet.ServletContext;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.common.util.SystemEnvProperties;
import org.keycloak.util.JsonSerialization;
public class JsonConfigProviderFactory extends org.keycloak.services.util.JsonConfigProviderFactory {
public static final String SERVER_CONTEXT_CONFIG_PROPERTY_OVERRIDES = "keycloak.server.context.config.property-overrides";
@Override
protected Properties getProperties() {
return new SystemEnvProperties(getPropertyOverrides());
}
private Map<String, String> getPropertyOverrides() {
ServletContext context = ResteasyProviderFactory.getContextData(ServletContext.class);
Map<String, String> propertyOverridesMap = new HashMap<>();
String propertyOverrides = context.getInitParameter(SERVER_CONTEXT_CONFIG_PROPERTY_OVERRIDES);
try {
if (context.getInitParameter(SERVER_CONTEXT_CONFIG_PROPERTY_OVERRIDES) != null) {
JsonNode jsonObj = JsonSerialization.mapper.readTree(propertyOverrides);
jsonObj.fields().forEachRemaining(e -> propertyOverridesMap.put(e.getKey(), e.getValue().asText()));
}
} catch (IOException e) {
}
return propertyOverridesMap;
}
}

View File

@ -0,0 +1 @@
org.keycloak.testsuite.JsonConfigProviderFactory

View File

@ -0,0 +1,107 @@
/*
* 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.
*/
package org.keycloak.provider.wildfly;
import com.fasterxml.jackson.databind.JsonNode;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import javax.servlet.ServletContext;
import org.jboss.dmr.ModelNode;
import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.Config;
import org.keycloak.common.util.Resteasy;
import org.keycloak.common.util.SystemEnvProperties;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.util.JsonConfigProviderFactory;
import org.keycloak.util.JsonSerialization;
public class DMRConfigProviderFactory extends JsonConfigProviderFactory {
// This param name is defined again in Keycloak Server Subsystem class
// org.keycloak.subsystem.server.extension.KeycloakServerDeploymentProcessor. We have this value in
// two places to avoid dependency between Keycloak Subsystem and Keycloak Wildfly Extensions module.
public static final String KEYCLOAK_CONFIG_PARAM_NAME = "org.keycloak.server-subsystem.Config";
public static final String SERVER_CONTEXT_CONFIG_PROPERTY_OVERRIDES = "keycloak.server.context.config.property-overrides";
private static final Logger LOG = Logger.getLogger(DMRConfigProviderFactory.class);
@Override
public Optional<Config.ConfigProvider> create() {
ServletContext context = Resteasy.getContextData(ServletContext.class);
JsonNode node = null;
try {
String dmrConfig = loadDmrConfig(context);
if (dmrConfig != null) {
node = JsonSerialization.mapper.readTree(dmrConfig);
ServicesLogger.LOGGER.loadingFrom("standalone.xml or domain.xml");
}
} catch (IOException e) {
LOG.warn("Failed to load DMR config", e);
}
return createJsonProvider(node);
}
@Override
protected Properties getProperties() {
return new SystemEnvProperties(getPropertyOverrides());
}
private String loadDmrConfig(ServletContext context) {
String dmrConfig = context.getInitParameter(KEYCLOAK_CONFIG_PARAM_NAME);
if (dmrConfig == null) {
return null;
}
ModelNode dmrConfigNode = ModelNode.fromString(dmrConfig);
if (dmrConfigNode.asPropertyList().isEmpty()) {
return null;
}
// note that we need to resolve expressions BEFORE we convert to JSON
return dmrConfigNode.resolve().toJSONString(true);
}
private Map<String, String> getPropertyOverrides() {
ServletContext context = ResteasyProviderFactory.getContextData(ServletContext.class);
Map<String, String> propertyOverridesMap = new HashMap<>();
String propertyOverrides = context.getInitParameter(SERVER_CONTEXT_CONFIG_PROPERTY_OVERRIDES);
try {
if (context.getInitParameter(SERVER_CONTEXT_CONFIG_PROPERTY_OVERRIDES) != null) {
JsonNode jsonObj = JsonSerialization.mapper.readTree(propertyOverrides);
jsonObj.fields().forEachRemaining(e -> propertyOverridesMap.put(e.getKey(), e.getValue().asText()));
}
} catch (IOException e) {
}
return propertyOverridesMap;
}
}

View File

@ -0,0 +1,18 @@
#
# 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.
#
org.keycloak.provider.wildfly.DMRConfigProviderFactory