mirror of
https://github.com/keycloak/keycloak.git
synced 2025-12-30 11:29:57 -06:00
refinement of propertymapperinterceptor names (#37504)
* fix: generalizing the reporting of names by property mapping closes: #37503 #37781 #37780 Signed-off-by: Steve Hawkins <shawkins@redhat.com> * Update quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/configuration/NestedPropertyMappingInterceptor.java Co-authored-by: Václav Muzikář <vaclav@muzikari.cz> Signed-off-by: Steven Hawkins <shawkins@redhat.com> * adding more explanation of going from a parent to wildcard values Signed-off-by: Steve Hawkins <shawkins@redhat.com> * refining the nested logic and comments Signed-off-by: Steve Hawkins <shawkins@redhat.com> * preventing nested expressions from always resolving the mapped value Signed-off-by: Steve Hawkins <shawkins@redhat.com> --------- Signed-off-by: Steve Hawkins <shawkins@redhat.com> Signed-off-by: Steven Hawkins <shawkins@redhat.com> Co-authored-by: Václav Muzikář <vaclav@muzikari.cz>
This commit is contained in:
@@ -2,7 +2,6 @@ package org.keycloak.config;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
@@ -39,7 +39,6 @@ import java.util.EnumMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
@@ -382,57 +381,46 @@ public class Picocli {
|
||||
}
|
||||
if (options.includeRuntime) {
|
||||
disabledMappers.addAll(PropertyMappers.getDisabledRuntimeMappers().values());
|
||||
} else {
|
||||
checkRuntimeSpiOptions(options, ignoredRunTime);
|
||||
}
|
||||
|
||||
for (OptionCategory category : abstractCommand.getOptionCategories()) {
|
||||
List<PropertyMapper<?>> mappers = new ArrayList<>(disabledMappers);
|
||||
var categories = new HashSet<>(abstractCommand.getOptionCategories());
|
||||
|
||||
// first validate the advertised property names
|
||||
// - this allows for efficient resolution of wildcard values and checking spi options
|
||||
Configuration.getConfig().getPropertyNames().forEach(name -> {
|
||||
if (!options.includeRuntime) {
|
||||
checkRuntimeSpiOptions(name, ignoredRunTime);
|
||||
}
|
||||
PropertyMapper<?> mapper = PropertyMappers.getMapper(name);
|
||||
if (mapper == null) {
|
||||
return; // TODO: need to look for disabled Wildcard mappers
|
||||
}
|
||||
if (!categories.contains(mapper.getCategory())) {
|
||||
return; // not of interest to this command
|
||||
// TODO: due to picking values up from the env and auto-builds, this probably isn't correct
|
||||
// - the same issue exists with the second pass
|
||||
}
|
||||
String from = mapper.getFrom();
|
||||
if (!mapper.hasWildcard()) {
|
||||
return; // non-wildcard options will be validated in the next pass
|
||||
}
|
||||
from = mapper.forKey(name).getFrom();
|
||||
validateProperty(abstractCommand, options, ignoredRunTime, disabledBuildTime, disabledRunTime,
|
||||
deprecatedInUse, missingOption, disabledMappers, mapper, from);
|
||||
});
|
||||
|
||||
// second pass validate any property mapper not seen in the first pass
|
||||
// - this will catch required values, anything missing from the property names, or disabled
|
||||
List<PropertyMapper<?>> mappers = new ArrayList<>(disabledMappers);
|
||||
for (OptionCategory category : categories) {
|
||||
Optional.ofNullable(PropertyMappers.getRuntimeMappers().get(category)).ifPresent(mappers::addAll);
|
||||
Optional.ofNullable(PropertyMappers.getBuildTimeMappers().get(category)).ifPresent(mappers::addAll);
|
||||
for (PropertyMapper<?> mapper : mappers) {
|
||||
mapper.getKcConfigValues().forEach(configValue -> {
|
||||
String configValueStr = configValue.getValue();
|
||||
}
|
||||
|
||||
// don't consider missing or anything below standard env properties
|
||||
if (configValueStr != null && !isUserModifiable(configValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (disabledMappers.contains(mapper)) {
|
||||
if (!PropertyMappers.isDisabledMapper(mapper.getFrom())) {
|
||||
return; // we found enabled mapper with the same name
|
||||
}
|
||||
|
||||
// only check build-time for a rebuild, we'll check the runtime later
|
||||
if (configValueStr != null && (!mapper.isRunTime() || !isRebuild())) {
|
||||
if (PropertyMapper.isCliOption(configValue)) {
|
||||
throw new KcUnmatchedArgumentException(abstractCommand.getCommandLine().orElseThrow(), List.of(mapper.getCliFormat()));
|
||||
} else {
|
||||
handleDisabled(mapper.isRunTime() ? disabledRunTime : disabledBuildTime, mapper);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (mapper.isRunTime() && !options.includeRuntime) {
|
||||
if (configValueStr != null) {
|
||||
ignoredRunTime.add(mapper.getFrom());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (configValueStr == null) {
|
||||
if (mapper.isRequired()) {
|
||||
handleRequired(missingOption, mapper);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
mapper.validate(configValue);
|
||||
|
||||
mapper.getDeprecatedMetadata().ifPresent(metadata -> handleDeprecated(deprecatedInUse, mapper, configValueStr, metadata));
|
||||
});
|
||||
for (PropertyMapper<?> mapper : mappers) {
|
||||
if (!mapper.hasWildcard()) {
|
||||
validateProperty(abstractCommand, options, ignoredRunTime, disabledBuildTime, disabledRunTime,
|
||||
deprecatedInUse, missingOption, disabledMappers, mapper, mapper.getFrom());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -462,27 +450,72 @@ public class Picocli {
|
||||
}
|
||||
}
|
||||
|
||||
private void validateProperty(AbstractCommand abstractCommand, IncludeOptions options,
|
||||
final List<String> ignoredRunTime, final Set<String> disabledBuildTime, final Set<String> disabledRunTime,
|
||||
final Set<String> deprecatedInUse, final Set<String> missingOption,
|
||||
final Set<PropertyMapper<?>> disabledMappers, PropertyMapper<?> mapper, String from) {
|
||||
ConfigValue configValue = Configuration.getConfigValue(from);
|
||||
String configValueStr = configValue.getValue();
|
||||
|
||||
// don't consider missing or anything below standard env properties
|
||||
if (configValueStr != null && !isUserModifiable(configValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (disabledMappers.contains(mapper)) {
|
||||
if (!PropertyMappers.isDisabledMapper(from)) {
|
||||
return; // we found enabled mapper with the same name
|
||||
}
|
||||
|
||||
// only check build-time for a rebuild, we'll check the runtime later
|
||||
if (configValueStr != null && (!mapper.isRunTime() || !isRebuild())) {
|
||||
if (PropertyMapper.isCliOption(configValue)) {
|
||||
throw new KcUnmatchedArgumentException(abstractCommand.getCommandLine().orElseThrow(), List.of(mapper.getCliFormat()));
|
||||
} else {
|
||||
handleDisabled(mapper.isRunTime() ? disabledRunTime : disabledBuildTime, mapper);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (mapper.isRunTime() && !options.includeRuntime) {
|
||||
if (configValueStr != null) {
|
||||
ignoredRunTime.add(mapper.getFrom());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (configValueStr == null) {
|
||||
if (mapper.isRequired()) {
|
||||
handleRequired(missingOption, mapper);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
mapper.validate(configValue);
|
||||
|
||||
mapper.getDeprecatedMetadata().ifPresent(metadata -> handleDeprecated(deprecatedInUse, mapper, configValueStr, metadata));
|
||||
}
|
||||
|
||||
private static boolean isUserModifiable(ConfigValue configValue) {
|
||||
// This could check as low as SysPropConfigSource DEFAULT_ORDINAL, which is 400
|
||||
// for now we won't validate these as it's not expected for the user to specify options via system properties
|
||||
return configValue.getConfigSourceOrdinal() >= KeycloakPropertiesConfigSource.PROPERTIES_FILE_ORDINAL;
|
||||
}
|
||||
|
||||
private static void checkRuntimeSpiOptions(IncludeOptions options, final List<String> ignoredRunTime) {
|
||||
for (String key : Configuration.getConfig().getPropertyNames()) {
|
||||
if (!key.startsWith(PropertyMappers.KC_SPI_PREFIX)) {
|
||||
continue;
|
||||
}
|
||||
boolean buildTimeOption = PropertyMappers.isSpiBuildTimeProperty(key);
|
||||
private static void checkRuntimeSpiOptions(String key, final List<String> ignoredRunTime) {
|
||||
if (!key.startsWith(PropertyMappers.KC_SPI_PREFIX)) {
|
||||
return;
|
||||
}
|
||||
boolean buildTimeOption = PropertyMappers.isSpiBuildTimeProperty(key);
|
||||
|
||||
if (!buildTimeOption) {
|
||||
ConfigValue configValue = Configuration.getConfigValue(key);
|
||||
String configValueStr = configValue.getValue();
|
||||
if (!buildTimeOption) {
|
||||
ConfigValue configValue = Configuration.getConfigValue(key);
|
||||
String configValueStr = configValue.getValue();
|
||||
|
||||
// don't consider missing or anything below standard env properties
|
||||
if (configValueStr != null && isUserModifiable(configValue)) {
|
||||
ignoredRunTime.add(key);
|
||||
}
|
||||
// don't consider missing or anything below standard env properties
|
||||
if (configValueStr != null && isUserModifiable(configValue)) {
|
||||
ignoredRunTime.add(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,20 +125,6 @@ public class ConfigArgsConfigSource extends PropertiesConfigSource {
|
||||
key = NS_KEYCLOAK_PREFIX + key.substring(2);
|
||||
|
||||
properties.put(key, value);
|
||||
|
||||
PropertyMapper<?> mapper = PropertyMappers.getMapper(key);
|
||||
|
||||
if (mapper != null) {
|
||||
mapper = mapper.forKey(key);
|
||||
|
||||
String to = mapper.getTo();
|
||||
|
||||
if (to != null) {
|
||||
properties.put(mapper.getTo(), value);
|
||||
}
|
||||
|
||||
properties.put(mapper.getFrom(), value);
|
||||
}
|
||||
}
|
||||
}, ignored -> {});
|
||||
|
||||
|
||||
@@ -28,8 +28,6 @@ import io.smallrye.config.ConfigValue;
|
||||
import io.smallrye.config.SmallRyeConfig;
|
||||
|
||||
import org.keycloak.config.Option;
|
||||
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper;
|
||||
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers;
|
||||
import org.keycloak.utils.StringUtil;
|
||||
|
||||
import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX;
|
||||
@@ -153,17 +151,6 @@ public final class Configuration {
|
||||
return getConfig().getOptionalValue(NS_KEYCLOAK_PREFIX.concat(propertyName), Integer.class);
|
||||
}
|
||||
|
||||
public static String getMappedPropertyName(String key) {
|
||||
PropertyMapper<?> mapper = PropertyMappers.getMapper(key);
|
||||
|
||||
if (mapper == null) {
|
||||
return key;
|
||||
}
|
||||
|
||||
// we also need to make sure the target property is available when defined such as when defining alias for provider config (no spi-prefix).
|
||||
return mapper.getTo() == null ? mapper.getFrom() : mapper.getTo();
|
||||
}
|
||||
|
||||
public static String toEnvVarFormat(String key) {
|
||||
return replaceNonAlphanumericByUnderscores(key).toUpperCase();
|
||||
}
|
||||
|
||||
@@ -130,11 +130,6 @@ public class IgnoredArtifacts {
|
||||
}
|
||||
});
|
||||
|
||||
// since the default may not be a known property name, look for it explicitly
|
||||
Configuration.getOptionalValue("quarkus.datasource.db-kind")
|
||||
.flatMap(Database::getVendor)
|
||||
.ifPresent(vendorsOfAllDatasources::add);
|
||||
|
||||
final Set<String> jdbcArtifacts = vendorsOfAllDatasources.stream()
|
||||
.map(vendor -> switch (vendor) {
|
||||
case H2 -> JDBC_H2;
|
||||
|
||||
@@ -24,6 +24,7 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import io.smallrye.config.PropertiesConfigSource;
|
||||
|
||||
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper;
|
||||
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers;
|
||||
|
||||
@@ -50,13 +51,7 @@ public class KcEnvConfigSource extends PropertiesConfigSource {
|
||||
PropertyMapper<?> mapper = PropertyMappers.getMapper(key);
|
||||
|
||||
if (mapper != null) {
|
||||
mapper = mapper.forEnvKey(key);
|
||||
|
||||
String to = mapper.getTo();
|
||||
|
||||
if (to != null) {
|
||||
properties.put(to, value);
|
||||
}
|
||||
mapper = mapper.forKey(key);
|
||||
|
||||
properties.put(mapper.getFrom(), value);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
|
||||
package org.keycloak.quarkus.runtime.configuration;
|
||||
|
||||
import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_KEYCLOAK;
|
||||
import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
@@ -33,23 +36,16 @@ import java.util.regex.Pattern;
|
||||
import org.eclipse.microprofile.config.spi.ConfigSource;
|
||||
import org.eclipse.microprofile.config.spi.ConfigSourceProvider;
|
||||
import org.keycloak.quarkus.runtime.Environment;
|
||||
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper;
|
||||
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers;
|
||||
|
||||
import io.smallrye.config.AbstractLocationConfigSourceLoader;
|
||||
import io.smallrye.config.PropertiesConfigSource;
|
||||
import io.smallrye.config.common.utils.ConfigSourceUtil;
|
||||
|
||||
import static org.keycloak.quarkus.runtime.configuration.Configuration.getMappedPropertyName;
|
||||
import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_KEYCLOAK;
|
||||
import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX;
|
||||
import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_QUARKUS;
|
||||
|
||||
/**
|
||||
* A configuration source for {@code keycloak.conf}.
|
||||
*/
|
||||
public class KeycloakPropertiesConfigSource extends AbstractLocationConfigSourceLoader {
|
||||
|
||||
|
||||
public static final int PROPERTIES_FILE_ORDINAL = 475;
|
||||
|
||||
private static final Pattern DOT_SPLIT = Pattern.compile("\\.");
|
||||
@@ -122,8 +118,9 @@ public class KeycloakPropertiesConfigSource extends AbstractLocationConfigSource
|
||||
public Path getConfigurationFile() {
|
||||
String filePath = System.getProperty(KEYCLOAK_CONFIG_FILE_PROP);
|
||||
|
||||
if (filePath == null)
|
||||
if (filePath == null) {
|
||||
filePath = System.getenv(KEYCLOAK_CONFIG_FILE_ENV);
|
||||
}
|
||||
|
||||
if (filePath == null) {
|
||||
String homeDir = Environment.getHomeDir();
|
||||
@@ -147,22 +144,10 @@ public class KeycloakPropertiesConfigSource extends AbstractLocationConfigSource
|
||||
|
||||
private static Map<String, String> transform(Map<String, String> properties) {
|
||||
Map<String, String> result = new HashMap<>(properties.size());
|
||||
properties.keySet().forEach(k -> {
|
||||
String key = transformKey(k);
|
||||
PropertyMapper<?> mapper = PropertyMappers.getMapper(key);
|
||||
|
||||
//TODO: remove explicit checks for spi and feature options once we have proper support in our config mappers
|
||||
if (mapper != null
|
||||
|| key.contains(NS_KEYCLOAK_PREFIX + "spi")
|
||||
|| key.contains(NS_KEYCLOAK_PREFIX + "feature")) {
|
||||
String value = properties.get(k);
|
||||
|
||||
result.put(key, value);
|
||||
|
||||
if (mapper != null && key.charAt(0) != '%') {
|
||||
result.put(getMappedPropertyName(key), value);
|
||||
}
|
||||
}
|
||||
properties.entrySet().forEach(entry -> {
|
||||
String key = transformKey(entry.getKey());
|
||||
result.put(key, entry.getValue());
|
||||
});
|
||||
|
||||
return result;
|
||||
@@ -176,7 +161,7 @@ public class KeycloakPropertiesConfigSource extends AbstractLocationConfigSource
|
||||
* @return the same key but prefixed with the namespace
|
||||
*/
|
||||
private static String transformKey(String key) {
|
||||
String namespace;
|
||||
String namespace = NS_KEYCLOAK;
|
||||
String[] keyParts = DOT_SPLIT.split(key);
|
||||
String extension = keyParts[0];
|
||||
String profile = "";
|
||||
@@ -188,12 +173,6 @@ public class KeycloakPropertiesConfigSource extends AbstractLocationConfigSource
|
||||
transformed = key.substring(key.indexOf('.') + 1);
|
||||
}
|
||||
|
||||
if (extension.equalsIgnoreCase(NS_QUARKUS)) {
|
||||
return key;
|
||||
} else {
|
||||
namespace = NS_KEYCLOAK;
|
||||
}
|
||||
|
||||
return profile + namespace + "." + transformed;
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright 2021 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.quarkus.runtime.configuration;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers;
|
||||
|
||||
import io.smallrye.config.ConfigSourceInterceptor;
|
||||
import io.smallrye.config.ConfigSourceInterceptorContext;
|
||||
import io.smallrye.config.ConfigValue;
|
||||
import io.smallrye.config.Priorities;
|
||||
import jakarta.annotation.Priority;
|
||||
|
||||
/**
|
||||
* Some resolution of values that come from PropertyMappers
|
||||
* happens at the ExpressionConfigSourceInterceptor, which is after
|
||||
* property mapping. This interceptor appears just after the expression
|
||||
* interceptor and will restart the context for anything not actively recursing.
|
||||
* This is needed in case the expression contains something that requires property mapping.
|
||||
*/
|
||||
@Priority(Priorities.LIBRARY + 299)
|
||||
public class NestedPropertyMappingInterceptor implements ConfigSourceInterceptor {
|
||||
|
||||
static final ThreadLocal<LinkedHashSet<String>> recursions = new ThreadLocal<>();
|
||||
|
||||
@Override
|
||||
public ConfigValue getValue(ConfigSourceInterceptorContext context, String name) {
|
||||
// don't look up the mapped value for direct env references
|
||||
if (Character.isUpperCase(name.charAt(0))) {
|
||||
return context.proceed(name);
|
||||
}
|
||||
return resolve(context::restart, context::proceed, name, false);
|
||||
}
|
||||
|
||||
private static <T> T resolve(Function<String, T> resolver, Function<String, T> nonRecursiveResolver, String name, boolean startNew) {
|
||||
LinkedHashSet<String> recursing = recursions.get();
|
||||
if (recursing == null && startNew) {
|
||||
recursing = new LinkedHashSet<String>();
|
||||
recursions.set(recursing);
|
||||
}
|
||||
if (recursing != null && recursing.add(name)) {
|
||||
try {
|
||||
return resolver.apply(name);
|
||||
} finally {
|
||||
recursing.remove(name);
|
||||
if (recursing.isEmpty()) {
|
||||
recursions.set(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
return nonRecursiveResolver.apply(name);
|
||||
}
|
||||
|
||||
public static Optional<String> getResolvingRoot() {
|
||||
return Optional.ofNullable(recursions.get()).filter(s -> !s.isEmpty()).map(s -> s.iterator().next());
|
||||
}
|
||||
|
||||
public static ConfigValue getValueFromPropertyMappers(ConfigSourceInterceptorContext context, String name) {
|
||||
Function<String, ConfigValue> resolver = (n) -> PropertyMappers.getValue(context, n);
|
||||
return resolve(resolver, resolver, name, true);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,24 +16,25 @@
|
||||
*/
|
||||
package org.keycloak.quarkus.runtime.configuration;
|
||||
|
||||
import io.smallrye.config.ConfigSourceInterceptor;
|
||||
import io.smallrye.config.ConfigSourceInterceptorContext;
|
||||
import io.smallrye.config.ConfigValue;
|
||||
import static org.keycloak.quarkus.runtime.Environment.isRebuild;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import io.smallrye.config.Priorities;
|
||||
import jakarta.annotation.Priority;
|
||||
import org.apache.commons.collections4.IteratorUtils;
|
||||
import org.apache.commons.collections4.iterators.FilterIterator;
|
||||
import org.keycloak.config.OptionCategory;
|
||||
import org.keycloak.quarkus.runtime.Environment;
|
||||
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper;
|
||||
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers;
|
||||
import org.keycloak.quarkus.runtime.configuration.mappers.WildcardPropertyMapper;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.keycloak.quarkus.runtime.Environment.isRebuild;
|
||||
import io.smallrye.config.ConfigSourceInterceptor;
|
||||
import io.smallrye.config.ConfigSourceInterceptorContext;
|
||||
import io.smallrye.config.ConfigValue;
|
||||
import io.smallrye.config.Priorities;
|
||||
import jakarta.annotation.Priority;
|
||||
|
||||
/**
|
||||
* <p>This interceptor is responsible for mapping Keycloak properties to their corresponding properties in Quarkus.
|
||||
@@ -54,7 +55,6 @@ import static org.keycloak.quarkus.runtime.Environment.isRebuild;
|
||||
public class PropertyMappingInterceptor implements ConfigSourceInterceptor {
|
||||
|
||||
private static final ThreadLocal<Boolean> disable = new ThreadLocal<>();
|
||||
private static final ThreadLocal<Boolean> disableAdditionalNames = new ThreadLocal<>();
|
||||
|
||||
public static void disable() {
|
||||
disable.set(true);
|
||||
@@ -64,40 +64,74 @@ public class PropertyMappingInterceptor implements ConfigSourceInterceptor {
|
||||
disable.remove();
|
||||
}
|
||||
|
||||
static Iterator<String> filterRuntime(Iterator<String> iter) {
|
||||
if (!isRebuild() && !Environment.isRebuildCheck()) {
|
||||
return iter;
|
||||
}
|
||||
return new FilterIterator<>(iter, item -> !isRuntime(item));
|
||||
}
|
||||
|
||||
static boolean isRuntime(String name) {
|
||||
PropertyMapper<?> mapper = PropertyMappers.getMapper(name);
|
||||
return mapper != null && mapper.isRunTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides a curated iteration of names based upon the mapping logic.
|
||||
* Quarkus logic, such as config mapping, is dependent upon seeing the quarkus
|
||||
* form of the key. We want to expose that here, rather than in the config sources
|
||||
* because we lack a simple way to do name mapping for some sources, such as the
|
||||
* keystore config source.
|
||||
* <p>
|
||||
* We currently expose:
|
||||
* <li>anything based upon a property mapper that has a map to a quarkus property - including
|
||||
* our kc. properties that have defaults.
|
||||
* <li>wildcard key names for wildcard keys that map from a keycloak property (e.g. kc.log-level)
|
||||
*
|
||||
* We selectively exclude:
|
||||
* <li>Config keystore properties at build time
|
||||
*/
|
||||
@Override
|
||||
public Iterator<String> iterateNames(ConfigSourceInterceptorContext context) {
|
||||
// We need to iterate through names to get wildcard option names.
|
||||
// Additionally, wildcardValuesTransformer might also trigger iterateNames.
|
||||
// Hence we need to disable this to prevent infinite recursion.
|
||||
// But we don't want to disable the whole interceptor, as wildcardValuesTransformer
|
||||
// might still need mappers to work.
|
||||
List<String> mappedWildcardNames = List.of();
|
||||
if (!Boolean.TRUE.equals(disableAdditionalNames.get())) {
|
||||
disableAdditionalNames.set(true);
|
||||
try {
|
||||
mappedWildcardNames = PropertyMappers.getWildcardMappers().stream()
|
||||
.map(WildcardPropertyMapper::getToWithWildcards)
|
||||
.flatMap(Set::stream)
|
||||
.toList();
|
||||
} finally {
|
||||
disableAdditionalNames.remove();
|
||||
}
|
||||
}
|
||||
Iterable<String> iterable = () -> context.iterateNames();
|
||||
|
||||
// this could be optimized by filtering the wildcard names in the stream above
|
||||
return filterRuntime(IteratorUtils.chainedIterator(mappedWildcardNames.iterator(), context.iterateNames()));
|
||||
final Set<PropertyMapper<?>> allMappers = PropertyMappers.getMappers();
|
||||
|
||||
//TODO: this is still not a complete list - things like quarkus.log.console.enabled
|
||||
// come from kc.log - but via a map from, not to.
|
||||
// so we'd need additional logic like the getWildcardMappedFrom case for that
|
||||
|
||||
boolean filterRuntime = isRebuild() || Environment.isRebuildCheck();
|
||||
|
||||
var baseStream = StreamSupport.stream(iterable.spliterator(), false).flatMap(name -> {
|
||||
PropertyMapper<?> mapper = PropertyMappers.getMapper(name);
|
||||
|
||||
if (mapper == null) {
|
||||
return Stream.of(name);
|
||||
}
|
||||
if (filterRuntime && mapper.getCategory() == OptionCategory.CONFIG) {
|
||||
return Stream.of(); // advertising the keystore type causes the keystore to be used early
|
||||
}
|
||||
allMappers.remove(mapper);
|
||||
|
||||
if (!mapper.hasWildcard()) {
|
||||
// this is not a wildcard value, but may map to wildcards
|
||||
// the current example is something like log-level=wildcardCat1:level,wildcardCat2:level
|
||||
var wildCard = PropertyMappers.getWildcardMappedFrom(mapper.getOption());
|
||||
if (wildCard != null) {
|
||||
ConfigValue value = context.proceed(name);
|
||||
if (value != null && value.getValue() != null) {
|
||||
return Stream.concat(Stream.of(name), wildCard.getToFromWildcardTransformer(value.getValue()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mapper = mapper.forKey(name);
|
||||
|
||||
// there is a corner case here: -1 for the reload period has no 'to' value.
|
||||
// if that becomes an issue we could use more metadata to perform a full mapping
|
||||
return toDistinctStream(name, mapper.getTo());
|
||||
});
|
||||
|
||||
// include anything remaining that has a default value
|
||||
var defaultStream = allMappers.stream()
|
||||
.filter(m -> !m.getDefaultValue().isEmpty() && !m.hasWildcard()
|
||||
&& m.getCategory() != OptionCategory.CONFIG) // advertising the keystore type causes the keystore to be used early
|
||||
.flatMap(m -> toDistinctStream(m.getTo()));
|
||||
|
||||
return IteratorUtils.chainedIterator(baseStream.iterator(), defaultStream.iterator());
|
||||
}
|
||||
|
||||
private static Stream<String> toDistinctStream(String... values) {
|
||||
return Stream.of(values).filter(Objects::nonNull).distinct();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -105,6 +139,8 @@ public class PropertyMappingInterceptor implements ConfigSourceInterceptor {
|
||||
if (Boolean.TRUE.equals(disable.get())) {
|
||||
return context.proceed(name);
|
||||
}
|
||||
return PropertyMappers.getValue(context, name);
|
||||
|
||||
// Call through NestedPropertyMappingInterceptor to track what we are currently getting the value for
|
||||
return NestedPropertyMappingInterceptor.getValueFromPropertyMappers(context, name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
package org.keycloak.quarkus.runtime.configuration.mappers;
|
||||
|
||||
import io.smallrye.config.ConfigSourceInterceptorContext;
|
||||
import io.smallrye.config.ConfigValue;
|
||||
import org.keycloak.config.ConfigKeystoreOptions;
|
||||
import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption;
|
||||
|
||||
final class ConfigKeystorePropertyMappers {
|
||||
public final class ConfigKeystorePropertyMappers {
|
||||
private static final String SMALLRYE_KEYSTORE_PATH = "smallrye.config.source.keystore.kc-default.path";
|
||||
private static final String SMALLRYE_KEYSTORE_PASSWORD = "smallrye.config.source.keystore.kc-default.password";
|
||||
|
||||
@@ -38,18 +38,16 @@ final class ConfigKeystorePropertyMappers {
|
||||
}
|
||||
|
||||
private static String validatePath(String option, ConfigSourceInterceptorContext context) {
|
||||
ConfigValue path = context.proceed(SMALLRYE_KEYSTORE_PATH);
|
||||
boolean isPasswordDefined = context.proceed(SMALLRYE_KEYSTORE_PASSWORD) != null;
|
||||
|
||||
if (path == null) {
|
||||
throw new IllegalArgumentException("config-keystore must be specified");
|
||||
if (option == null) {
|
||||
return null;
|
||||
}
|
||||
boolean isPasswordDefined = context.proceed(MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX + ConfigKeystoreOptions.CONFIG_KEYSTORE_PASSWORD.getKey()) != null;
|
||||
|
||||
if (!isPasswordDefined) {
|
||||
throw new IllegalArgumentException("config-keystore-password must be specified");
|
||||
}
|
||||
|
||||
final Path realPath = Path.of(path.getValue()).toAbsolutePath().normalize();
|
||||
final Path realPath = Path.of(option).toAbsolutePath().normalize();
|
||||
if (!Files.exists(realPath)) {
|
||||
throw new IllegalArgumentException("config-keystore path does not exist: " + realPath);
|
||||
}
|
||||
@@ -58,12 +56,10 @@ final class ConfigKeystorePropertyMappers {
|
||||
}
|
||||
|
||||
private static String validatePassword(String option, ConfigSourceInterceptorContext context) {
|
||||
boolean isPasswordDefined = context.proceed(SMALLRYE_KEYSTORE_PASSWORD).getValue() != null;
|
||||
boolean isPathDefined = context.proceed(SMALLRYE_KEYSTORE_PATH) != null;
|
||||
|
||||
if (!isPasswordDefined) {
|
||||
throw new IllegalArgumentException("config-keystore-password must be specified");
|
||||
if (option == null) {
|
||||
return null;
|
||||
}
|
||||
boolean isPathDefined = context.proceed(MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX + ConfigKeystoreOptions.CONFIG_KEYSTORE.getKey()) != null;
|
||||
|
||||
if (!isPathDefined) {
|
||||
throw new IllegalArgumentException("config-keystore must be specified");
|
||||
|
||||
@@ -127,7 +127,7 @@ public final class LoggingPropertyMappers {
|
||||
.to("quarkus.log.category.\"<categories>\".level")
|
||||
.validator(LoggingPropertyMappers::validateCategoryLogLevel)
|
||||
.wildcardKeysTransformer(LoggingPropertyMappers::getConfiguredLogCategories)
|
||||
.transformer((v,c) -> toLevel(v).getName())
|
||||
.transformer((v,c) -> v == null ? null : toLevel(v).getName())
|
||||
.wildcardMapFrom(LoggingOptions.LOG_LEVEL, LoggingPropertyMappers::resolveCategoryLogLevelFromParentLogLevelOption) // a fallback to log-level
|
||||
.paramLabel("level")
|
||||
.build(),
|
||||
@@ -267,8 +267,8 @@ public final class LoggingPropertyMappers {
|
||||
return DEFAULT_ROOT_LOG_LEVEL; // defaults are not resolved in the mapper if transformer is present, so doing it explicitly here
|
||||
}
|
||||
|
||||
private static Set<String> getConfiguredLogCategories(Set<String> categories) {
|
||||
for (CategoryLevel categoryLevel : parseRootLogLevel(null)) {
|
||||
private static Set<String> getConfiguredLogCategories(String value, Set<String> categories) {
|
||||
for (CategoryLevel categoryLevel : parseRootLogLevel(value)) {
|
||||
if (categoryLevel.category != null) {
|
||||
categories.add(categoryLevel.category);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ package org.keycloak.quarkus.runtime.configuration.mappers;
|
||||
|
||||
import static java.util.Optional.ofNullable;
|
||||
import static org.keycloak.config.Option.WILDCARD_PLACEHOLDER_PATTERN;
|
||||
import static org.keycloak.quarkus.runtime.Environment.isRebuild;
|
||||
import static org.keycloak.quarkus.runtime.configuration.Configuration.OPTION_PART_SEPARATOR;
|
||||
import static org.keycloak.quarkus.runtime.configuration.Configuration.OPTION_PART_SEPARATOR_CHAR;
|
||||
import static org.keycloak.quarkus.runtime.configuration.Configuration.toCliFormat;
|
||||
@@ -34,7 +33,6 @@ import java.util.function.BiConsumer;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import io.smallrye.config.ConfigSourceInterceptorContext;
|
||||
@@ -45,14 +43,11 @@ import io.smallrye.config.Expressions;
|
||||
import org.keycloak.config.DeprecatedMetadata;
|
||||
import org.keycloak.config.Option;
|
||||
import org.keycloak.config.OptionCategory;
|
||||
import org.keycloak.quarkus.runtime.Environment;
|
||||
import org.keycloak.quarkus.runtime.cli.PropertyException;
|
||||
import org.keycloak.quarkus.runtime.cli.ShortErrorMessageHandler;
|
||||
import org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource;
|
||||
import org.keycloak.quarkus.runtime.configuration.Configuration;
|
||||
import org.keycloak.quarkus.runtime.configuration.KcEnvConfigSource;
|
||||
import org.keycloak.quarkus.runtime.configuration.KeycloakConfigSourceProvider;
|
||||
import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider;
|
||||
import org.keycloak.utils.StringUtil;
|
||||
|
||||
public class PropertyMapper<T> {
|
||||
@@ -103,10 +98,6 @@ public class PropertyMapper<T> {
|
||||
this.parentMapper = parentMapper;
|
||||
}
|
||||
|
||||
ConfigValue getConfigValue(ConfigSourceInterceptorContext context) {
|
||||
return getConfigValue(to, context);
|
||||
}
|
||||
|
||||
ConfigValue getConfigValue(String name, ConfigSourceInterceptorContext context) {
|
||||
String from = getFrom();
|
||||
|
||||
@@ -123,7 +114,7 @@ public class PropertyMapper<T> {
|
||||
// if the property we want to map depends on another one, we use the value from the other property to call the mapper
|
||||
// not getting the value directly from SmallRye Config to avoid the risk of infinite recursion when Config is initializing
|
||||
String mapFromWithPrefix = NS_KEYCLOAK_PREFIX + mapFrom;
|
||||
config = PropertyMappers.getMapper(mapFromWithPrefix).getConfigValue(mapFromWithPrefix, context);
|
||||
config = context.restart(mapFromWithPrefix);
|
||||
parentValue = true;
|
||||
}
|
||||
|
||||
@@ -293,7 +284,7 @@ public class PropertyMapper<T> {
|
||||
String map(String name, String value, ConfigSourceInterceptorContext context);
|
||||
}
|
||||
|
||||
private final class ContextWrapper implements ConfigSourceInterceptorContext {
|
||||
private static final class ContextWrapper implements ConfigSourceInterceptorContext {
|
||||
private final ConfigSourceInterceptorContext context;
|
||||
private final ConfigValue value;
|
||||
|
||||
@@ -336,7 +327,7 @@ public class PropertyMapper<T> {
|
||||
private String description;
|
||||
private BooleanSupplier isRequired = () -> false;
|
||||
private String requiredWhen = "";
|
||||
private Function<Set<String>, Set<String>> wildcardKeysTransformer;
|
||||
private BiFunction<String, Set<String>, Set<String>> wildcardKeysTransformer;
|
||||
private ValueMapper wildcardMapFrom;
|
||||
|
||||
public Builder(Option<T> option) {
|
||||
@@ -462,7 +453,7 @@ public class PropertyMapper<T> {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder<T> wildcardKeysTransformer(Function<Set<String>, Set<String>> wildcardValuesTransformer) {
|
||||
public Builder<T> wildcardKeysTransformer(BiFunction<String, Set<String>, Set<String>> wildcardValuesTransformer) {
|
||||
this.wildcardKeysTransformer = wildcardValuesTransformer;
|
||||
return this;
|
||||
}
|
||||
@@ -567,25 +558,6 @@ public class PropertyMapper<T> {
|
||||
KeycloakConfigSourceProvider.getConfigSourceDisplayName(configValue.getConfigSourceName()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all Keycloak config values for the mapper. A multivalued config option is a config option that
|
||||
* has a wildcard in its name, e.g. log-level-<category>.
|
||||
*
|
||||
* @return a list of config values where the key is the resolved wildcard (e.g. category) and the value is the config value
|
||||
*/
|
||||
public List<ConfigValue> getKcConfigValues() {
|
||||
return List.of(Configuration.getConfigValue(getFrom()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new PropertyMapper tailored for the given env var key.
|
||||
* This is currently useful in {@link WildcardPropertyMapper} where "to" and "from" fields need to include a specific
|
||||
* wildcard key.
|
||||
*/
|
||||
public PropertyMapper<?> forEnvKey(String key) {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new PropertyMapper tailored for the given key.
|
||||
* This is currently useful in {@link WildcardPropertyMapper} where "to" and "from" fields need to include a specific
|
||||
@@ -595,4 +567,8 @@ public class PropertyMapper<T> {
|
||||
return this;
|
||||
}
|
||||
|
||||
String getMapFrom() {
|
||||
return mapFrom;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import jakarta.ws.rs.core.MultivaluedHashMap;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.util.CollectionUtil;
|
||||
import org.keycloak.config.ConfigSupportLevel;
|
||||
import org.keycloak.config.Option;
|
||||
import org.keycloak.config.OptionCategory;
|
||||
import org.keycloak.quarkus.runtime.Environment;
|
||||
import org.keycloak.quarkus.runtime.cli.PropertyException;
|
||||
@@ -15,6 +16,7 @@ import org.keycloak.quarkus.runtime.cli.command.Build;
|
||||
import org.keycloak.quarkus.runtime.cli.command.ShowConfig;
|
||||
import org.keycloak.quarkus.runtime.configuration.DisabledMappersInterceptor;
|
||||
import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider;
|
||||
import org.keycloak.quarkus.runtime.configuration.NestedPropertyMappingInterceptor;
|
||||
import org.keycloak.quarkus.runtime.configuration.PersistedConfigSource;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -24,6 +26,7 @@ import java.util.EnumMap;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@@ -79,12 +82,13 @@ public final class PropertyMappers {
|
||||
name = removeProfilePrefixIfNeeded(name);
|
||||
PropertyMapper<?> mapper = getMapper(name);
|
||||
|
||||
// During re-aug do not resolve the server runtime properties and avoid they included by quarkus in the default value config source.
|
||||
// During re-aug do not resolve server runtime properties and avoid they included by quarkus in the default value config source.
|
||||
//
|
||||
// The special handling of log properties is because some logging runtime properties are requested during build time
|
||||
// and we need to resolve them. That should be fine as they are generally not considered security sensitive.
|
||||
// See https://github.com/quarkusio/quarkus/pull/42157
|
||||
if ((isRebuild() || Environment.isRebuildCheck()) && isKeycloakRuntime(name, mapper) && !name.startsWith("quarkus.log.")) {
|
||||
if ((isRebuild() || Environment.isRebuildCheck()) && isKeycloakRuntime(name, mapper)
|
||||
&& !NestedPropertyMappingInterceptor.getResolvingRoot().orElse(name).startsWith("quarkus.log.")) {
|
||||
return ConfigValue.builder().withName(name).build();
|
||||
}
|
||||
|
||||
@@ -182,14 +186,21 @@ public final class PropertyMappers {
|
||||
return getMapper(property, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a mutable copy of all known mappers
|
||||
*/
|
||||
public static Set<PropertyMapper<?>> getMappers() {
|
||||
return MAPPERS.values().stream().flatMap(Collection::stream).collect(Collectors.toSet());
|
||||
return MAPPERS.values().stream().flatMap(Collection::stream).collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
}
|
||||
|
||||
public static Set<WildcardPropertyMapper<?>> getWildcardMappers() {
|
||||
return MAPPERS.getWildcardMappers();
|
||||
}
|
||||
|
||||
public static WildcardPropertyMapper<?> getWildcardMappedFrom(Option<?> from) {
|
||||
return MAPPERS.wildcardMapFrom.get(from.getKey());
|
||||
}
|
||||
|
||||
public static boolean isSupported(PropertyMapper<?> mapper) {
|
||||
ConfigSupportLevel supportLevel = mapper.getCategory().getSupportLevel();
|
||||
return supportLevel.equals(ConfigSupportLevel.SUPPORTED) || supportLevel.equals(ConfigSupportLevel.DEPRECATED);
|
||||
@@ -232,7 +243,9 @@ public final class PropertyMappers {
|
||||
|
||||
private final Map<String, PropertyMapper<?>> disabledBuildTimeMappers = new HashMap<>();
|
||||
private final Map<String, PropertyMapper<?>> disabledRuntimeMappers = new HashMap<>();
|
||||
|
||||
private final Set<WildcardPropertyMapper<?>> wildcardMappers = new HashSet<>();
|
||||
private final Map<String, WildcardPropertyMapper<?>> wildcardMapFrom = new HashMap<>();
|
||||
|
||||
public void addAll(PropertyMapper<?>[] mappers) {
|
||||
for (PropertyMapper<?> mapper : mappers) {
|
||||
@@ -252,13 +265,22 @@ public final class PropertyMappers {
|
||||
|
||||
public void addMapper(PropertyMapper<?> mapper) {
|
||||
if (mapper.hasWildcard()) {
|
||||
if (mapper.getMapFrom() != null) {
|
||||
wildcardMapFrom.put(mapper.getMapFrom(), (WildcardPropertyMapper<?>) mapper);
|
||||
}
|
||||
wildcardMappers.add((WildcardPropertyMapper<?>)mapper);
|
||||
} else {
|
||||
handleMapper(mapper, this::add);
|
||||
}
|
||||
handleMapper(mapper, this::add);
|
||||
}
|
||||
|
||||
public void removeMapper(PropertyMapper<?> mapper) {
|
||||
wildcardMappers.remove(mapper);
|
||||
if (mapper.hasWildcard()) {
|
||||
wildcardMappers.remove(mapper);
|
||||
if (mapper.getFrom() != null) {
|
||||
wildcardMapFrom.remove(mapper.getMapFrom());
|
||||
}
|
||||
}
|
||||
handleMapper(mapper, this::remove);
|
||||
}
|
||||
|
||||
@@ -272,17 +294,23 @@ public final class PropertyMappers {
|
||||
@Override
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
public List<PropertyMapper<?>> get(Object key) {
|
||||
// First check if the requested option matches any wildcard mappers
|
||||
// First check the base mappings
|
||||
String strKey = (String) key;
|
||||
List ret = wildcardMappers.stream()
|
||||
|
||||
List ret = super.get(key);
|
||||
if (ret != null) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
// TODO: we may want to introduce a prefix tree here as we add more wildcardMappers
|
||||
ret = wildcardMappers.stream()
|
||||
.filter(m -> m.matchesWildcardOptionName(strKey))
|
||||
.toList();
|
||||
if (!ret.isEmpty()) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
// If no wildcard mappers match, check for exact matches
|
||||
return super.get(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -295,7 +323,9 @@ public final class PropertyMappers {
|
||||
}
|
||||
|
||||
public void sanitizeDisabledMappers() {
|
||||
if (Environment.getParsedCommand().isEmpty()) return; // do not sanitize when no command is present
|
||||
if (Environment.getParsedCommand().isEmpty()) {
|
||||
return; // do not sanitize when no command is present
|
||||
}
|
||||
|
||||
DisabledMappersInterceptor.runWithDisabled(() -> { // We need to have the whole configuration available
|
||||
|
||||
@@ -377,4 +407,5 @@ public final class PropertyMappers {
|
||||
operation.accept(mapper.getEnvVarFormat(), mapper);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,20 +3,18 @@ package org.keycloak.quarkus.runtime.configuration.mappers;
|
||||
import static org.keycloak.config.Option.WILDCARD_PLACEHOLDER_PATTERN;
|
||||
import static org.keycloak.quarkus.runtime.cli.Picocli.ARG_PREFIX;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.HashSet;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.StreamSupport;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.keycloak.config.Option;
|
||||
import org.keycloak.quarkus.runtime.configuration.Configuration;
|
||||
import org.keycloak.quarkus.runtime.cli.Picocli;
|
||||
import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider;
|
||||
|
||||
import io.smallrye.config.ConfigSourceInterceptorContext;
|
||||
@@ -29,14 +27,14 @@ public class WildcardPropertyMapper<T> extends PropertyMapper<T> {
|
||||
private final Pattern envVarNameWildcardPattern;
|
||||
private Matcher toWildcardMatcher;
|
||||
private Pattern toWildcardPattern;
|
||||
private final Function<Set<String>, Set<String>> wildcardKeysTransformer;
|
||||
private final BiFunction<String, Set<String>, Set<String>> wildcardKeysTransformer;
|
||||
private final ValueMapper wildcardMapFrom;
|
||||
|
||||
public WildcardPropertyMapper(Option<T> option, String to, BooleanSupplier enabled, String enabledWhen,
|
||||
BiFunction<String, ConfigSourceInterceptorContext, String> mapper,
|
||||
String mapFrom, BiFunction<String, ConfigSourceInterceptorContext, String> parentMapper,
|
||||
String paramLabel, boolean mask, BiConsumer<PropertyMapper<T>, ConfigValue> validator,
|
||||
String description, BooleanSupplier required, String requiredWhen, Matcher fromWildcardMatcher, Function<Set<String>, Set<String>> wildcardKeysTransformer, ValueMapper wildcardMapFrom) {
|
||||
String description, BooleanSupplier required, String requiredWhen, Matcher fromWildcardMatcher, BiFunction<String, Set<String>, Set<String>> wildcardKeysTransformer, ValueMapper wildcardMapFrom) {
|
||||
super(option, to, enabled, enabledWhen, mapper, mapFrom, parentMapper, paramLabel, mask, validator, description, required, requiredWhen, null);
|
||||
this.wildcardMapFrom = wildcardMapFrom;
|
||||
this.fromWildcardMatcher = fromWildcardMatcher;
|
||||
@@ -72,25 +70,11 @@ public class WildcardPropertyMapper<T> extends PropertyMapper<T> {
|
||||
return MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX + fromWildcardMatcher.replaceFirst(wildcardKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ConfigValue> getKcConfigValues() {
|
||||
return this.getWildcardKeys().stream().map(v -> Configuration.getConfigValue(getFrom(v))).toList();
|
||||
}
|
||||
|
||||
public Set<String> getWildcardKeys() {
|
||||
// this is not optimal
|
||||
// TODO find an efficient way to get all values that match the wildcard
|
||||
Set<String> values = StreamSupport.stream(Configuration.getPropertyNames().spliterator(), false)
|
||||
.map(n -> getMappedKey(n, false))
|
||||
.filter(Optional::isPresent)
|
||||
.map(Optional::get)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
if (wildcardKeysTransformer != null) {
|
||||
return wildcardKeysTransformer.apply(values);
|
||||
public Stream<String> getToFromWildcardTransformer(String value) {
|
||||
if (wildcardKeysTransformer == null) {
|
||||
return Stream.empty();
|
||||
}
|
||||
|
||||
return values;
|
||||
return wildcardKeysTransformer.apply(value, new HashSet<String>()).stream().map(toWildcardMatcher::replaceFirst);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,50 +83,39 @@ public class WildcardPropertyMapper<T> extends PropertyMapper<T> {
|
||||
* E.g. for the option "log-level-<category>" and the option name "log-level-io.quarkus",
|
||||
* the wildcard value would be "io.quarkus".
|
||||
*/
|
||||
private Optional<String> getMappedKey(String originalKey, boolean tryTo) {
|
||||
Matcher matcher = fromWildcardPattern.matcher(originalKey);
|
||||
if (matcher.matches()) {
|
||||
return Optional.of(matcher.group(1));
|
||||
private Optional<String> getMappedKey(String originalKey) {
|
||||
Matcher matcher = null;
|
||||
if (originalKey.startsWith(Picocli.ARG_PREFIX) || originalKey.startsWith(MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX)) {
|
||||
matcher = fromWildcardPattern.matcher(originalKey);
|
||||
if (matcher.matches()) {
|
||||
return Optional.of(matcher).map(m -> m.group(1));
|
||||
}
|
||||
}
|
||||
|
||||
if (tryTo && toWildcardPattern != null) {
|
||||
if (toWildcardPattern != null) {
|
||||
matcher = toWildcardPattern.matcher(originalKey);
|
||||
if (matcher.matches()) {
|
||||
return Optional.of(matcher.group(1));
|
||||
return Optional.of(matcher).map(m -> m.group(1));
|
||||
}
|
||||
}
|
||||
|
||||
if (originalKey.startsWith("KC_")) {
|
||||
matcher = envVarNameWildcardPattern.matcher(originalKey);
|
||||
if (matcher.matches()) {
|
||||
// we opiniotatedly convert env var names to CLI format with dots
|
||||
return Optional.of(matcher).map(m -> m.group(1).toLowerCase().replace("_", "."));
|
||||
}
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public Set<String> getToWithWildcards() {
|
||||
if (toWildcardMatcher == null) {
|
||||
return Set.of();
|
||||
}
|
||||
|
||||
return getWildcardKeys().stream()
|
||||
.map(v -> toWildcardMatcher.replaceFirst(v))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the given option name matches the wildcard pattern of this option.
|
||||
* E.g. check if "log-level-io.quarkus" matches the wildcard pattern "log-level-<category>".
|
||||
*/
|
||||
public boolean matchesWildcardOptionName(String name) {
|
||||
return fromWildcardPattern.matcher(name).matches() || envVarNameWildcardPattern.matcher(name).matches()
|
||||
|| (toWildcardPattern != null && toWildcardPattern.matcher(name).matches());
|
||||
}
|
||||
|
||||
@Override
|
||||
public PropertyMapper<?> forEnvKey(String key) {
|
||||
Matcher matcher = envVarNameWildcardPattern.matcher(key);
|
||||
if (!matcher.matches()) {
|
||||
throw new IllegalStateException("Env var '" + key + "' does not match the expected pattern '" + envVarNameWildcardPattern + "'");
|
||||
}
|
||||
String value = matcher.group(1);
|
||||
final String wildcardValue = value.toLowerCase().replace("_", "."); // we opiniotatedly convert env var names to CLI format with dots
|
||||
return forWildcardValue(wildcardValue);
|
||||
return getMappedKey(name).isPresent();
|
||||
}
|
||||
|
||||
private PropertyMapper<?> forWildcardValue(final String wildcardValue) {
|
||||
@@ -153,8 +126,7 @@ public class WildcardPropertyMapper<T> extends PropertyMapper<T> {
|
||||
|
||||
@Override
|
||||
public PropertyMapper<?> forKey(String key) {
|
||||
final String wildcardValue = getMappedKey(key, true).orElseThrow();
|
||||
return forWildcardValue(wildcardValue);
|
||||
return getMappedKey(key).map(this::forWildcardValue).orElseThrow();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,4 +16,5 @@
|
||||
#
|
||||
|
||||
org.keycloak.quarkus.runtime.configuration.PropertyMappingInterceptor
|
||||
org.keycloak.quarkus.runtime.configuration.NestedPropertyMappingInterceptor
|
||||
org.keycloak.quarkus.runtime.configuration.DisabledMappersInterceptor
|
||||
@@ -55,7 +55,11 @@ public abstract class AbstractConfigurationTest {
|
||||
try {
|
||||
field = env.getClass().getDeclaredField("m");
|
||||
field.setAccessible(true);
|
||||
((Map<String, String>) field.get(env)).put(name, value);
|
||||
if (value == null) {
|
||||
((Map<String, String>) field.get(env)).remove(name);
|
||||
} else {
|
||||
((Map<String, String>) field.get(env)).put(name, value);
|
||||
}
|
||||
} catch (Exception cause) {
|
||||
throw new RuntimeException("Failed to update environment variables", cause);
|
||||
} finally {
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package org.keycloak.quarkus.runtime.configuration.test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.keycloak.quarkus.runtime.Environment.isWindows;
|
||||
@@ -149,6 +150,28 @@ public class ConfigurationTest extends AbstractConfigurationTest {
|
||||
assertEquals("http://c.jwk.url", initConfig("client-registration", "openid-connect").get("static-jwk-url"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExpressionEnvValue() {
|
||||
putEnvVar("KC_HOSTNAME_STRICT", "false");
|
||||
putEnvVar("MY_EXPRESSION", "${KC_HOSTNAME_STRICT}");
|
||||
ConfigArgsConfigSource.setCliArgs("");
|
||||
var config = createConfig();
|
||||
// with the env variable set, we should get the same value either way
|
||||
assertEquals("false", config.getConfigValue("KC_HOSTNAME_STRICT").getValue());
|
||||
assertEquals("false", config.getConfigValue("MY_EXPRESSION").getValue());
|
||||
|
||||
// without the env variable set, the expression should use the missing env variable
|
||||
putEnvVar("KC_HOSTNAME_STRICT", null);
|
||||
ConfigArgsConfigSource.setCliArgs("");
|
||||
config = createConfig();
|
||||
// check that we get the mapped default value
|
||||
assertEquals("true", config.getConfigValue("kc.hostname-strict").getValue());
|
||||
// check that we don't get the mapped value
|
||||
assertNull(config.getConfigValue("MY_EXPRESSION").getValue());
|
||||
// could change after https://github.com/keycloak/keycloak/issues/38072
|
||||
assertEquals("true", config.getConfigValue("KC_HOSTNAME_STRICT").getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testResolveTransformedValue() {
|
||||
ConfigArgsConfigSource.setCliArgs("");
|
||||
@@ -478,11 +501,7 @@ public class ConfigurationTest extends AbstractConfigurationTest {
|
||||
|
||||
@Test
|
||||
public void testKeystoreConfigSourcePropertyMapping() {
|
||||
SmallRyeConfig config = new SmallRyeConfigBuilder()
|
||||
.addDefaultInterceptors()
|
||||
.addDiscoveredSources()
|
||||
.build();
|
||||
|
||||
SmallRyeConfig config = createConfig();
|
||||
assertEquals(config.getConfigValue("smallrye.config.source.keystore.kc-default.password").getValue(),config.getConfigValue("kc.config-keystore-password").getValue());
|
||||
// Properties are loaded from the file - secret can be obtained only if the mapping works correctly
|
||||
ConfigValue secret = config.getConfigValue("my.secret");
|
||||
@@ -540,4 +559,28 @@ public class ConfigurationTest extends AbstractConfigurationTest {
|
||||
assertEquals(Integer.toString(maxCount), config.getConfigValue(prop).getValue());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDirectWildcardTo() {
|
||||
// the mapping to for a wildcard property shouldn't be to anything
|
||||
ConfigArgsConfigSource.setCliArgs("");
|
||||
SmallRyeConfig config = createConfig();
|
||||
assertNull(config.getConfigValue("quarkus.log.category.\"<categories>\".level").getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testKeycloakConfQuarkusPropertyNotUsed() {
|
||||
ConfigArgsConfigSource.setCliArgs("");
|
||||
SmallRyeConfig config = createConfig();
|
||||
assertNull(config.getConfigValue("quarkus.management.ssl.cipher-suites").getValue());
|
||||
assertNotNull(config.getConfigValue("kc.quarkus.management.ssl.cipher-suites").getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testQuarkusLogPropDependentUponKeycloak() {
|
||||
Environment.setRebuildCheck(); // will be reset by the system properties logic
|
||||
ConfigArgsConfigSource.setCliArgs("--log-level=debug");
|
||||
SmallRyeConfig config = createConfig();
|
||||
assertEquals("DEBUG", config.getConfigValue("quarkus.log.category.\"something\".level").getValue());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,4 +6,5 @@ KC_LOG_LEVEL_IO_QUARKUS=trace
|
||||
config-keystore=src/test/resources/keystore
|
||||
config-keystore-password=secret
|
||||
|
||||
quarkus.log.file.path=random/path
|
||||
#quarkus option should not be used
|
||||
quarkus.management.ssl.cipher-suites=foo
|
||||
@@ -29,7 +29,6 @@ import org.keycloak.it.utils.KeycloakDistribution;
|
||||
import org.keycloak.it.utils.RawKeycloakDistribution;
|
||||
|
||||
import io.quarkus.test.junit.main.Launch;
|
||||
import io.quarkus.test.junit.main.LaunchResult;
|
||||
|
||||
@DistributionTest(keepAlive = true, defaultOptions = { "--db=dev-file", "--features=fips", "--http-enabled=true", "--hostname-strict=false", "--log-level=org.keycloak.common.crypto.CryptoIntegration:trace" })
|
||||
@RawDistOnly(reason = "Containers are immutable")
|
||||
|
||||
@@ -22,7 +22,7 @@ import org.junit.jupiter.api.MethodOrderer;
|
||||
import org.junit.jupiter.api.Tag;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
import org.junit.jupiter.api.condition.DisabledOnOs;
|
||||
import org.junit.jupiter.api.condition.EnabledOnOs;
|
||||
import org.junit.jupiter.api.condition.OS;
|
||||
import org.keycloak.it.junit5.extension.CLIResult;
|
||||
import org.keycloak.it.junit5.extension.DistributionTest;
|
||||
@@ -62,6 +62,8 @@ public class StartDevCommandDistTest {
|
||||
cliResult.assertMessageWasShownExactlyNumberOfTimes("Listening for transport dt_socket at address:", 2);
|
||||
cliResult.assertStartedDevMode();
|
||||
cliResult.assertMessage("passkeys");
|
||||
// ensure consistency with build-time properties
|
||||
cliResult.assertNoMessage("Build time property cannot");
|
||||
}
|
||||
|
||||
@DryRun
|
||||
@@ -80,7 +82,6 @@ public class StartDevCommandDistTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisabledOnOs(value = { OS.LINUX, OS.MAC }, disabledReason = "A drive letter in URI can cause a problem.")
|
||||
void testConfigKeystoreAbsolutePath(KeycloakDistribution dist) {
|
||||
CLIResult cliResult = dist.run("start-dev", "--config-keystore=" + Paths.get("src/test/resources/keystore").toAbsolutePath().normalize(),
|
||||
"--config-keystore-password=secret");
|
||||
|
||||
Reference in New Issue
Block a user