[KEYCLOAK-18255] - Vault Support in Dist.X

This commit is contained in:
Pedro Igor 2021-11-01 08:35:43 -03:00
parent 62482eb313
commit eaa96f6147
38 changed files with 537 additions and 69 deletions

View file

@ -17,6 +17,8 @@
package org.keycloak;
import java.util.Set;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@ -154,6 +156,11 @@ public class Config {
return new SystemPropertiesScope(sb.toString());
}
@Override
public Set<String> getPropertyNames() {
throw new UnsupportedOperationException("Not implemented");
}
}
/**
@ -181,5 +188,6 @@ public class Config {
Scope scope(String... scope);
Set<String> getPropertyNames();
}
}

View file

@ -70,6 +70,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-metrics-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-vault-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-internal</artifactId>

View file

@ -556,7 +556,7 @@ class KeycloakProcessor {
return false;
}
if (factory instanceof EnvironmentDependentProviderFactory) {
return ((EnvironmentDependentProviderFactory) factory).isSupported();
return ((EnvironmentDependentProviderFactory) factory).isSupported(scope);
}
return true;
}

View file

@ -4,3 +4,6 @@ quarkus.banner.enabled=false
quarkus.resteasy.ignore-application-classes=true
quarkus.arc.ignored-split-packages=org.keycloak.*
# we do not want running testcontainers when running tests in this module
quarkus.devservices.enabled=false

View file

@ -71,6 +71,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-metrics</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-vault</artifactId>
</dependency>
<!-- CLI -->
<dependency>

View file

@ -17,6 +17,8 @@
package org.keycloak.quarkus.runtime;
import static org.keycloak.quarkus.runtime.configuration.PropertyMappers.getBuiltTimeProperty;
import java.io.File;
import java.io.FilenameFilter;
import java.nio.file.Path;
@ -24,14 +26,12 @@ import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import io.quarkus.runtime.LaunchMode;
import io.quarkus.runtime.configuration.ProfileManager;
import org.apache.commons.lang3.SystemUtils;
import org.keycloak.quarkus.runtime.configuration.Configuration;
public final class Environment {
@ -112,16 +112,6 @@ public final class Environment {
return profile;
}
public static Optional<String> getBuiltTimeProperty(String name) {
String value = Configuration.getBuiltTimeProperty(name);
if (value == null) {
return Optional.empty();
}
return Optional.of(value);
}
public static boolean isDevMode() {
if ("dev".equalsIgnoreCase(getProfile())) {
return true;

View file

@ -20,8 +20,11 @@ package org.keycloak.quarkus.runtime.cli;
import static java.util.Arrays.asList;
import static org.keycloak.quarkus.runtime.cli.command.AbstractStartCommand.AUTO_BUILD_OPTION;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getConfig;
import static org.keycloak.quarkus.runtime.configuration.PropertyMappers.getBuiltTimeProperty;
import static org.keycloak.quarkus.runtime.configuration.PropertyMappers.getRuntimeProperty;
import static org.keycloak.quarkus.runtime.configuration.PropertyMappers.isBuildTimeProperty;
import static org.keycloak.quarkus.runtime.Environment.isDevMode;
import static org.keycloak.utils.StringUtil.isNotBlank;
import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST;
import java.io.File;
@ -70,6 +73,7 @@ public final class Picocli {
private static final String ARG_SEPARATOR = ";;";
public static final String ARG_PREFIX = "--";
public static final String ARG_PART_SEPARATOR = "-";
public static final char ARG_KEY_VALUE_SEPARATOR = '=';
public static final Pattern ARG_SPLIT = Pattern.compile(";;");
public static final Pattern ARG_KEY_VALUE_SPLIT = Pattern.compile("=");
@ -206,7 +210,7 @@ public final class Picocli {
private static boolean hasConfigChanges() {
Optional<String> currentProfile = Optional.ofNullable(Environment.getProfile());
Optional<String> persistedProfile = Environment.getBuiltTimeProperty("kc.profile");
Optional<String> persistedProfile = getBuiltTimeProperty("kc.profile");
if (!persistedProfile.orElse("").equals(currentProfile.orElse(""))) {
return true;
@ -218,16 +222,27 @@ public final class Picocli {
continue;
}
ConfigValue configValue = getConfig().getConfigValue(propertyName);
if (configValue == null || configValue.getConfigSourceName() == null) {
continue;
}
// try to resolve any property set using profiles
if (propertyName.startsWith("%")) {
propertyName = propertyName.substring(propertyName.indexOf('.') + 1);
}
String currentValue = Environment.getBuiltTimeProperty(propertyName).orElse(null);
String newValue = getConfig().getConfigValue(propertyName).getValue();
String persistedValue = getBuiltTimeProperty(propertyName).orElse("");
String runtimeValue = getRuntimeProperty(propertyName).orElse(null);
if (newValue != null && !newValue.equalsIgnoreCase(currentValue)) {
// changes to a single property are enough to indicate changes to configuration
if (runtimeValue == null && isNotBlank(persistedValue)) {
// probably because it was unset
return true;
}
// changes to a single property is enough to indicate changes to configuration
if (!persistedValue.equals(runtimeValue)) {
return true;
}
}
@ -315,7 +330,8 @@ public final class Picocli {
String name = ARG_PREFIX + PropertyMappers.toCLIFormat(mapper.getFrom()).substring(3);
String description = mapper.getDescription();
if (description == null || commandSpec.optionsMap().containsKey(name)) {
if (description == null || commandSpec.optionsMap().containsKey(name)
|| name.endsWith(ARG_PART_SEPARATOR)) {
continue;
}
@ -443,4 +459,8 @@ public final class Picocli {
}
}
}
public static String normalizeKey(String key) {
return key.replace('-', '.');
}
}

View file

@ -22,7 +22,7 @@ import static org.keycloak.quarkus.runtime.configuration.Configuration.getConfig
import static org.keycloak.quarkus.runtime.configuration.Configuration.getPropertyNames;
import static org.keycloak.quarkus.runtime.configuration.PropertyMappers.canonicalFormat;
import static org.keycloak.quarkus.runtime.configuration.PropertyMappers.formatValue;
import static org.keycloak.quarkus.runtime.Environment.getBuiltTimeProperty;
import static org.keycloak.quarkus.runtime.configuration.PropertyMappers.getBuiltTimeProperty;
import java.util.HashSet;
import java.util.Map;
@ -171,6 +171,10 @@ public final class ShowConfig extends AbstractCommand implements Runnable {
return;
}
if (configValue.getSourceName() == null) {
return;
}
spec.commandLine().getOut().printf("\t%s = %s (%s)%n", configValue.getName(), formatValue(configValue.getName(), configValue.getValue()), configValue.getConfigSourceName());
}

View file

@ -22,6 +22,7 @@ import static org.keycloak.quarkus.runtime.cli.Picocli.ARG_PREFIX;
import static org.keycloak.quarkus.runtime.cli.Picocli.ARG_SPLIT;
import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX;
import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_QUARKUS_PREFIX;
import static org.keycloak.quarkus.runtime.configuration.PropertyMappers.getMappedPropertyName;
import java.util.Collections;
import java.util.HashMap;
@ -32,6 +33,7 @@ import org.jboss.logging.Logger;
import io.smallrye.config.PropertiesConfigSource;
import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.quarkus.runtime.cli.Picocli;
/**
* <p>A configuration source for mapping configuration arguments to their corresponding properties so that they can be recognized
@ -55,23 +57,13 @@ public class ConfigArgsConfigSource extends PropertiesConfigSource {
@Override
public String getValue(String propertyName) {
String prefix = null;
String value = super.getValue(propertyName.replace('-', '.'));
// we only care about runtime args passed when executing the CLI, no need to check if the property is prefixed with a profile
if (propertyName.startsWith(NS_KEYCLOAK_PREFIX)) {
prefix = NS_KEYCLOAK_PREFIX;
} else if (propertyName.startsWith(NS_QUARKUS_PREFIX)) {
prefix = NS_QUARKUS_PREFIX;
if (value != null) {
return value;
}
// we only recognize properties within keycloak and quarkus namespaces
if (prefix == null) {
return null;
}
String[] parts = DOT_SPLIT.split(propertyName.substring(propertyName.indexOf(prefix) + prefix.length()));
return super.getValue(prefix + String.join("-", parts));
return null;
}
private static Map<String, String> parseArgument() {
@ -111,8 +103,10 @@ public class ConfigArgsConfigSource extends PropertiesConfigSource {
key = NS_KEYCLOAK_PREFIX + key.substring(2);
log.tracef("Adding property [%s=%s] from command-line", key, value);
properties.put(key, value);
properties.put(getMappedPropertyName(key), value);
// to make lookup easier, we normalize the key
properties.put(Picocli.normalizeKey(key), value);
}
return properties;

View file

@ -42,6 +42,10 @@ public final class Configuration {
public static String getBuiltTimeProperty(String name) {
String value = KeycloakConfigSourceProvider.PERSISTED_CONFIG_SOURCE.getValue(name);
if (value == null) {
value = KeycloakConfigSourceProvider.PERSISTED_CONFIG_SOURCE.getValue(PropertyMappers.getMappedPropertyName(name));
}
if (value == null) {
String profile = Environment.getProfile();

View file

@ -0,0 +1,59 @@
/*
* 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.Map;
import java.util.Set;
import java.util.TreeMap;
import org.eclipse.microprofile.config.spi.ConfigSource;
public class EnvConfigSource implements ConfigSource {
private final Map<String, String> properties = new TreeMap<>();
public EnvConfigSource() {
for (Map.Entry<String, String> entry : System.getenv().entrySet()) {
String key = entry.getKey();
if (key.startsWith(MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX.toUpperCase().replace('.', '_'))) {
properties.put(PropertyMappers.getMappedPropertyName(key), entry.getValue());
}
}
}
@Override
public Map<String, String> getProperties() {
return properties;
}
@Override
public Set<String> getPropertyNames() {
return properties.keySet();
}
public String getValue(final String propertyName) {
return System.getProperty(propertyName);
}
public String getName() {
return "KcEnvVarConfigSource";
}
public int getOrdinal() {
return 350;
}
}

View file

@ -52,9 +52,10 @@ public class KeycloakConfigSourceProvider implements ConfigSourceProvider {
}
CONFIG_SOURCES.add(new ConfigArgsConfigSource());
CONFIG_SOURCES.add(new SysPropConfigSource());
CONFIG_SOURCES.add(new EnvConfigSource());
PERSISTED_CONFIG_SOURCE = new PersistedConfigSource(getPersistedConfigFile());
CONFIG_SOURCES.add(PERSISTED_CONFIG_SOURCE);
CONFIG_SOURCES.add(new SysPropConfigSource());
Path configFile = getConfigurationFile();

View file

@ -38,6 +38,7 @@ import io.smallrye.config.PropertiesConfigSource;
import static org.keycloak.common.util.StringPropertyReplacer.replaceProperties;
import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_KEYCLOAK;
import static org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider.NS_QUARKUS;
import static org.keycloak.quarkus.runtime.configuration.PropertyMappers.getMappedPropertyName;
/**
* A configuration source for {@code keycloak.properties}.
@ -68,7 +69,7 @@ public abstract class KeycloakPropertiesConfigSource extends PropertiesConfigSou
public static final class InJar extends KeycloakPropertiesConfigSource {
public InJar() {
super(openStream(), 245);
super(openStream(), 250);
}
private static InputStream openStream() {
@ -93,7 +94,7 @@ public abstract class KeycloakPropertiesConfigSource extends PropertiesConfigSou
public static final class InFileSystem extends KeycloakPropertiesConfigSource {
public InFileSystem(Path path) {
super(openStream(path), 255);
super(openStream(path), 250);
}
private static InputStream openStream(Path path) {
@ -113,7 +114,13 @@ public abstract class KeycloakPropertiesConfigSource extends PropertiesConfigSou
private static Map<String, String> transform(Map<String, String> properties) {
Map<String, String> result = new HashMap<>(properties.size());
properties.keySet().forEach(k -> result.put(transformKey(k), replaceProperties(properties.get(k))));
properties.keySet().forEach(k -> {
String key = transformKey(k);
String value = replaceProperties(properties.get(k));
result.put(key, value);
result.put(getMappedPropertyName(key), value);
});
return result;
}

View file

@ -17,10 +17,15 @@
package org.keycloak.quarkus.runtime.configuration;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.apache.commons.lang3.ArrayUtils;
import org.eclipse.microprofile.config.ConfigProvider;
import org.keycloak.Config;
import org.keycloak.quarkus.runtime.cli.Picocli;
public class MicroProfileConfigProvider implements Config.ConfigProvider {
@ -109,6 +114,18 @@ public class MicroProfileConfigProvider implements Config.ConfigProvider {
return new MicroProfileScope(ArrayUtils.addAll(this.scope, scope));
}
@Override
public Set<String> getPropertyNames() {
return StreamSupport.stream(config.getPropertyNames().spliterator(), false)
.filter(new Predicate<String>() {
@Override
public boolean test(String key) {
return key.startsWith(prefix) || key.startsWith(Picocli.normalizeKey(prefix));
}
})
.collect(Collectors.toSet());
}
private <T> T getValue(String key, Class<T> clazz, T defaultValue) {
return config.getOptionalValue(toDashCase(prefix.concat(".").concat(key)), clazz).orElse(defaultValue);
}

View file

@ -58,10 +58,6 @@ public class PersistedConfigSource extends PropertiesConfigSource {
return value;
}
if (propertyName.startsWith(MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX)) {
return super.getValue(PropertyMappers.toCLIFormat(propertyName));
}
return null;
}

View file

@ -143,6 +143,13 @@ public class PropertyMapper {
}
ConfigValue getOrDefault(String name, ConfigSourceInterceptorContext context, ConfigValue current) {
String from = this.from;
if (to != null && to.endsWith(".")) {
// in case mapping is based on prefixes instead of full property names
from = name.replace(to.substring(0, to.lastIndexOf('.')), from.substring(0, from.lastIndexOf('.')));
}
// try to obtain the value for the property we want to map
ConfigValue config = context.proceed(from);

View file

@ -16,6 +16,8 @@
*/
package org.keycloak.quarkus.runtime.configuration;
import static org.keycloak.quarkus.runtime.Environment.getProfileOrDefault;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getConfig;
import static org.keycloak.quarkus.runtime.configuration.Messages.invalidDatabaseVendor;
import static org.keycloak.quarkus.runtime.configuration.PropertyMapper.MAPPERS;
import static org.keycloak.quarkus.runtime.configuration.PropertyMapper.create;
@ -26,7 +28,9 @@ import static org.keycloak.quarkus.runtime.integration.QuarkusPlatform.addInitia
import java.io.File;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@ -34,6 +38,7 @@ import java.util.stream.Collectors;
import io.smallrye.config.ConfigSourceInterceptorContext;
import io.smallrye.config.ConfigValue;
import org.eclipse.microprofile.config.spi.ConfigSource;
import org.keycloak.quarkus.runtime.storage.database.Database;
import org.keycloak.quarkus.runtime.Environment;
@ -50,6 +55,7 @@ public final class PropertyMappers {
configureClustering();
configureHostnameProviderMappers();
configureMetrics();
configureVault();
}
private static void configureHttpPropertyMappers() {
@ -164,9 +170,45 @@ public final class PropertyMappers {
Arrays.asList(Boolean.TRUE.toString(), Boolean.FALSE.toString()));
}
private static void configureVault() {
createBuildTimeProperty("vault.file.path", "kc.spi.vault.files-plaintext.dir", "If set, secrets can be obtained by reading the content of files within the given path.");
createBuildTimeProperty("vault.hashicorp.", "quarkus.vault.", "If set, secrets can be obtained from Hashicorp Vault.");
createBuildTimeProperty("vault.hashicorp.paths", "kc.spi.vault.hashicorp.paths", "A set of one or more paths that should be used when looking up secrets.");
}
static ConfigValue getValue(ConfigSourceInterceptorContext context, String name) {
return PropertyMapper.MAPPERS.getOrDefault(name, PropertyMapper.IDENTITY)
PropertyMapper mapper = MAPPERS.getOrDefault(name, PropertyMapper.IDENTITY);
ConfigValue configValue = mapper
.getOrDefault(name, context, context.proceed(name));
if (configValue == null) {
Optional<String> prefixedMapper = getPrefixedMapper(name);
if (prefixedMapper.isPresent()) {
return MAPPERS.get(prefixedMapper.get()).getOrDefault(name, context, configValue);
}
} else {
configValue.withName(mapper.getTo());
}
return configValue;
}
private static Optional<String> getPrefixedMapper(String name) {
Optional<String> prefixedMapper = MAPPERS.keySet().stream().filter(new Predicate<String>() {
@Override
public boolean test(String key) {
if (!key.endsWith(".")) {
return false;
}
String prefix = key.substring(0, key.lastIndexOf('.') - 1);
return name.startsWith(prefix);
}
}).findAny();
return prefixedMapper;
}
public static boolean isBuildTimeProperty(String name) {
@ -176,8 +218,9 @@ public final class PropertyMappers {
}
return name.startsWith(MicroProfileConfigProvider.NS_KEYCLOAK_PREFIX)
&& PropertyMapper.MAPPERS.entrySet().stream()
.anyMatch(entry -> entry.getValue().getFrom().equals(name) && entry.getValue().isBuildTime())
&& PropertyMapper.MAPPERS.values().stream()
.filter(PropertyMapper::isBuildTime)
.anyMatch(mapper -> mapper.getFrom().equals(name) || mapper.getTo().equals(name))
&& !"kc.version".equals(name)
&& !Environment.CLI_ARGS.equals(name)
&& !"kc.home.dir".equals(name)
@ -215,6 +258,10 @@ public final class PropertyMappers {
.filter(entry -> entry.isBuildTime()).collect(Collectors.toList());
}
public static Collection<PropertyMapper> getMappers() {
return MAPPERS.values();
}
public static String canonicalFormat(String name) {
return name.replaceAll("-", "\\.");
}
@ -237,4 +284,58 @@ public final class PropertyMappers {
}
}).findFirst().orElse(null);
}
public static String getMappedPropertyName(String key) {
for (PropertyMapper mapper : PropertyMappers.getMappers()) {
String mappedProperty = mapper.getFrom();
List<String> expectedFormats = Arrays.asList(mappedProperty, toCLIFormat(mappedProperty), mappedProperty.toUpperCase().replace('.', '_').replace('-', '_'));
if (expectedFormats.contains(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 ? mappedProperty : mapper.getTo();
}
}
return key;
}
public static Optional<String> getBuiltTimeProperty(String name) {
String value = Configuration.getBuiltTimeProperty(name);
if (value == null) {
return Optional.empty();
}
return Optional.of(value);
}
public static Optional<String> getRuntimeProperty(String name) {
for (ConfigSource configSource : getConfig().getConfigSources()) {
if (PersistedConfigSource.NAME.equals(configSource.getName())) {
continue;
}
String value = getValue(configSource, name);
if (value == null) {
value = getValue(configSource, PropertyMappers.getMappedPropertyName(name));
}
if (value != null) {
return Optional.of(value);
}
}
return Optional.empty();
}
private static String getValue(ConfigSource configSource, String name) {
String value = configSource.getValue(name);
if (value == null) {
value = configSource.getValue("%".concat(getProfileOrDefault("prod").concat(".").concat(name)));
}
return value;
}
}

View file

@ -0,0 +1,66 @@
/*
* 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.vault;
import static org.keycloak.vault.DefaultVaultRawSecret.forBuffer;
import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.keycloak.vault.AbstractVaultProvider;
import org.keycloak.vault.VaultKeyResolver;
import org.keycloak.vault.VaultRawSecret;
import io.quarkus.vault.VaultKVSecretEngine;
public class QuarkusVaultProvider extends AbstractVaultProvider {
private VaultKVSecretEngine secretEngine;
private String[] kvPaths;
public QuarkusVaultProvider(VaultKVSecretEngine secretEngine, String[] kvPaths, String realm, List<VaultKeyResolver> keyResolvers) {
super(realm, keyResolvers);
this.secretEngine = secretEngine;
this.kvPaths = kvPaths;
}
@Override
protected VaultRawSecret obtainSecretInternal(String key) {
if (kvPaths == null) {
return forBuffer(Optional.empty());
}
for (String path : kvPaths) {
Map<String, String> secrets = secretEngine.readSecret(path);
String secret = secrets.get(key);
if (secret != null) {
return forBuffer(Optional.of(StandardCharsets.UTF_8.encode(CharBuffer.wrap(secret))));
}
}
return forBuffer(Optional.empty());
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,88 @@
/*
* 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.vault;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.vault.AbstractVaultProviderFactory;
import org.keycloak.vault.VaultProvider;
import io.quarkus.arc.Arc;
import io.quarkus.arc.InstanceHandle;
import io.quarkus.vault.VaultKVSecretEngine;
import io.quarkus.vault.runtime.VaultConfigHolder;
public class QuarkusVaultProviderFactory extends AbstractVaultProviderFactory implements EnvironmentDependentProviderFactory {
private String[] kvPaths;
private VaultKVSecretEngine secretEngine;
@Override
public VaultProvider create(KeycloakSession session) {
return new QuarkusVaultProvider(secretEngine, kvPaths, getRealmName(session), super.keyResolvers);
}
@Override
public void init(Config.Scope config) {
super.init(config);
kvPaths = config.getArray("paths");
}
@Override
public void postInit(KeycloakSessionFactory factory) {
InstanceHandle<VaultKVSecretEngine> engineInstance = Arc.container().instance(VaultKVSecretEngine.class);
if (engineInstance.isAvailable()) {
secretEngine = engineInstance.get();
}
InstanceHandle<VaultConfigHolder> configInstance = Arc.container().instance(VaultConfigHolder.class);
if (!configInstance.isAvailable() || configInstance.get().getVaultBootstrapConfig() == null) {
throw new RuntimeException("No configuration defined for hashicorp provider.");
}
}
@Override
public void close() {
}
@Override
public String getId() {
return "hashicorp";
}
@Override
public int order() {
return 10;
}
@Override
public boolean isSupported(Config.Scope config) {
return !config.getPropertyNames().isEmpty();
}
@Override
public boolean isSupported() {
// in quarkus we do not use this method when installing providers
return false;
}
}

View file

@ -0,0 +1 @@
org.keycloak.quarkus.runtime.vault.QuarkusVaultProviderFactory

View file

@ -42,6 +42,7 @@ import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider;
import io.quarkus.runtime.configuration.ConfigUtils;
import io.smallrye.config.SmallRyeConfigProviderResolver;
import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.vault.FilesPlainTextVaultProviderFactory;
public class ConfigurationTest {
@ -175,6 +176,33 @@ public class ConfigurationTest {
assertEquals("http://c.jwk.url", initConfig("client-registration", "openid-connect").get("static-jwk-url"));
}
@Test
public void testPropertyNamesFromConfig() {
System.setProperty(CLI_ARGS, "--spi-client-registration-openid-connect-static-jwk-url=http://c.jwk.url");
Config.Scope config = initConfig("client-registration", "openid-connect");
assertEquals(1, config.getPropertyNames().size());
assertEquals("http://c.jwk.url", config.get("static-jwk-url"));
System.setProperty(CLI_ARGS, "--vault-file-path=secrets");
config = initConfig("vault", FilesPlainTextVaultProviderFactory.PROVIDER_ID);
assertEquals(1, config.getPropertyNames().size());
assertEquals("secrets", config.get("dir"));
System.getProperties().remove(CLI_ARGS);
System.setProperty("kc.spi.client-registration.openid-connect.static-jwk-url", "http://c.jwk.url");
config = initConfig("client-registration", "openid-connect");
assertEquals(1, config.getPropertyNames().size());
assertEquals("http://c.jwk.url", config.get("static-jwk-url"));
System.getProperties().remove(CLI_ARGS);
System.getProperties().remove("kc.spi.client-registration.openid-connect.static-jwk-url");
putEnvVar("KC_SPI_CLIENT_REGISTRATION_OPENID_CONNECT_STATIC_JWK_URL", "http://c.jwk.url/from-env");
config = initConfig("client-registration", "openid-connect");
assertEquals(1, config.getPropertyNames().size());
assertEquals("http://c.jwk.url/from-env", config.get("static-jwk-url"));
}
@Test
public void testPropertyMapping() {
System.setProperty(CLI_ARGS, "--db=mariadb" + ARG_SEPARATOR + "--db-url=jdbc:mariadb://localhost/keycloak");

View file

@ -16,8 +16,8 @@
*/
package org.keycloak.component;
import java.util.Set;
import org.keycloak.Config.Scope;
import org.keycloak.component.ComponentModel;
/**
*
@ -114,4 +114,9 @@ public class ComponentModelScope implements Scope {
return new ComponentModelScope(origScope.scope(scope), componentConfig, String.join(".", scope) + ".");
}
@Override
public Set<String> getPropertyNames() {
throw new UnsupportedOperationException("Not implemented");
}
}

View file

@ -17,6 +17,8 @@
package org.keycloak.provider;
import org.keycloak.Config;
/**
* Providers that are only supported in some environments can implement this interface to be able to determine if they
* should be available or not.
@ -27,7 +29,18 @@ public interface EnvironmentDependentProviderFactory {
/**
* @return <code>true</code> if the provider is supported and should be available, <code>false</code> otherwise
* @deprecated Prefer overriding/using the {@link #isSupported(Config.Scope)} method.
*/
boolean isSupported();
/**
* An alternative to {@link #isSupported()} method to check if the provider is supported based on the
* provider configuration.
*
* @param config the provider configuration
* @return {@code true} if the provider is supported. Otherwise, {@code false}.
*/
default boolean isSupported(Config.Scope config) {
return isSupported();
}
}

View file

@ -22,6 +22,7 @@ import org.keycloak.Config;
import org.keycloak.common.util.StringPropertyReplacer;
import java.util.Properties;
import java.util.Set;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -183,6 +184,11 @@ public class JsonConfigProvider implements Config.ConfigProvider {
return new JsonScope(getNode(config, path));
}
@Override
public Set<String> getPropertyNames() {
throw new UnsupportedOperationException("Not implemented");
}
}
}

View file

@ -11,6 +11,8 @@ import org.keycloak.services.DefaultKeycloakSessionFactory;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import java.util.Set;
/**
* Tests for {@link FilesPlainTextVaultProviderFactory}.
*
@ -129,6 +131,11 @@ public class PlainTextVaultProviderFactoryTest {
public Config.Scope scope(String... scope) {
throw new UnsupportedOperationException("not implemented");
}
@Override
public Set<String> getPropertyNames() {
throw new UnsupportedOperationException("not implemented");
}
}
}

View file

@ -113,6 +113,25 @@
<overwrite>true</overwrite>
</configuration>
</execution>
<execution>
<id>copy-vault-secrets</id>
<phase>process-resources</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${auth.server.home}/secrets</outputDirectory>
<resources>
<resource>
<directory>${common.resources}/vault</directory>
<includes>
<include>**</include>
</includes>
</resource>
</resources>
<overwrite>true</overwrite>
</configuration>
</execution>
<execution>
<id>add-extending-theme</id>
<phase>process-resources</phase>

View file

@ -41,3 +41,6 @@ spi.events-store.jpa.max-detail-length=2000
# set known protocol ports for basicsamltest
spi.login-protocol.saml.known-protocols=http=8180,https=8543
# File-Based Vault
vault.file.path=${kc.home.dir}secrets

View file

@ -568,7 +568,7 @@ public class AuthServerTestEnricher {
}
}
if (!isAuthServerQuarkus() && event.getTestClass().isAnnotationPresent(EnableVault.class)) {
if (event.getTestClass().isAnnotationPresent(EnableVault.class)) {
VaultUtils.enableVault(suiteContext, event.getTestClass().getAnnotation(EnableVault.class).providerId());
wasUpdated = true;
}

View file

@ -48,6 +48,9 @@ public class VaultTestExecutionDecider implements TestExecutionDecider {
if (suiteContext != null && suiteContext.getAuthServerInfo() != null && suiteContext.getAuthServerInfo().isUndertow()) {
return ExecutionDecision.dontExecute("@EnableVault with Elytron credential store provider not supported on Undertow, skipping");
}
if (suiteContext != null && suiteContext.getAuthServerInfo() != null && suiteContext.getAuthServerInfo().isQuarkus()) {
return ExecutionDecision.dontExecute("@EnableVault with Elytron credential store provider not supported on Quarkus, skipping");
}
}
}
return ExecutionDecision.execute();

View file

@ -19,6 +19,7 @@ package org.keycloak.testsuite.util;
import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
import org.keycloak.testsuite.arquillian.ContainerInfo;
import org.keycloak.testsuite.arquillian.SuiteContext;
import org.keycloak.testsuite.arquillian.annotation.EnableVault;
import org.wildfly.extras.creaper.core.online.CliException;
@ -34,9 +35,11 @@ import java.util.concurrent.TimeoutException;
public class VaultUtils {
public static void enableVault(SuiteContext suiteContext, EnableVault.PROVIDER_ID provider) throws IOException, CliException, TimeoutException, InterruptedException {
if (suiteContext.getAuthServerInfo().isUndertow()) {
ContainerInfo serverInfo = suiteContext.getAuthServerInfo();
if (serverInfo.isUndertow()) {
System.setProperty("keycloak.vault." + provider.getName() + ".provider.enabled", "true");
} else {
} else if (serverInfo.isJBossBased()) {
OnlineManagementClient client = AuthServerTestEnricher.getManagementClient();
// configure the selected provider and set it as the default vault provider.
client.execute("/subsystem=keycloak-server/spi=vault/:add(default-provider=" + provider.getName() + ")");
@ -48,9 +51,11 @@ public class VaultUtils {
}
public static void disableVault(SuiteContext suiteContext, EnableVault.PROVIDER_ID provider) throws IOException, CliException, TimeoutException, InterruptedException {
if (suiteContext.getAuthServerInfo().isUndertow() || suiteContext.getAuthServerInfo().isQuarkus()) {
ContainerInfo serverInfo = suiteContext.getAuthServerInfo();
if (serverInfo.isUndertow()) {
System.setProperty("keycloak.vault." + provider.getName() + ".provider.enabled", "false");
} else {
} else if (serverInfo.isJBossBased()) {
OnlineManagementClient client = AuthServerTestEnricher.getManagementClient();
for (String command : provider.getCliRemovalCommands()) {
client.execute(command);

View file

@ -40,7 +40,7 @@ import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.A
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@EnableVault
@AuthServerContainerExclude({AuthServer.REMOTE, AuthServer.QUARKUS})
@AuthServerContainerExclude(AuthServer.REMOTE)
public class UserFederationLdapConnectionTest extends AbstractAdminTest {
@ClassRule

View file

@ -39,13 +39,12 @@ import java.util.stream.Collectors;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.QUARKUS;
import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.REMOTE;
/**
* @author Vaclav Muzikar <vmuzikar@redhat.com>
*/
@AuthServerContainerExclude({REMOTE})
@AuthServerContainerExclude(REMOTE)
public class ClientSearchTest extends AbstractClientTest {
@ArquillianResource
protected ContainerController controller;

View file

@ -1,5 +1,7 @@
package org.keycloak.testsuite.broker;
import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.REMOTE;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
import org.keycloak.testsuite.arquillian.annotation.EnableVault;
@ -7,7 +9,7 @@ import org.keycloak.testsuite.arquillian.annotation.EnableVault;
* @author Martin Kanis <mkanis@redhat.com>
*/
@EnableVault
@AuthServerContainerExclude({AuthServerContainerExclude.AuthServer.QUARKUS, AuthServerContainerExclude.AuthServer.REMOTE})
@AuthServerContainerExclude(REMOTE)
public class KcOidcBrokerVaultTest extends AbstractBrokerTest {
@Override

View file

@ -50,8 +50,7 @@ import java.util.List;
import java.util.Objects;
import org.junit.Assume;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.REMOTE;
/**
* Test user logins utilizing various LDAP authentication methods and different LDAP connection encryption mechanisms.
@ -227,7 +226,7 @@ public class LDAPUserLoginTest extends AbstractLDAPTest {
// Test variant: Bind credential set to vault
@Test
@LDAPConnectionParameters(bindCredential=LDAPConnectionParameters.BindCredential.VAULT, bindType=LDAPConnectionParameters.BindType.SIMPLE, encryption=LDAPConnectionParameters.Encryption.NONE)
@AuthServerContainerExclude(value = {AuthServerContainerExclude.AuthServer.QUARKUS, AuthServerContainerExclude.AuthServer.REMOTE}, details =
@AuthServerContainerExclude(value = REMOTE, details =
"java.io.NotSerializableException: com.sun.jndi.ldap.LdapCtx")
public void loginLDAPUserCredentialVaultAuthenticationSimpleEncryptionNone() {
verifyConnectionUrlProtocolPrefix("ldap://");
@ -247,7 +246,7 @@ public class LDAPUserLoginTest extends AbstractLDAPTest {
// Test variant: Bind credential set to vault
@Test
@LDAPConnectionParameters(bindCredential=LDAPConnectionParameters.BindCredential.VAULT, bindType=LDAPConnectionParameters.BindType.SIMPLE, encryption=LDAPConnectionParameters.Encryption.SSL)
@AuthServerContainerExclude(value = {AuthServerContainerExclude.AuthServer.QUARKUS, AuthServerContainerExclude.AuthServer.REMOTE}, details =
@AuthServerContainerExclude(value = REMOTE, details =
"java.io.NotSerializableException: com.sun.jndi.ldap.LdapCtx")
public void loginLDAPUserCredentialVaultAuthenticationSimpleEncryptionSSL() {
verifyConnectionUrlProtocolPrefix("ldaps://");
@ -267,7 +266,7 @@ public class LDAPUserLoginTest extends AbstractLDAPTest {
// Test variant: Bind credential set to vault
@Test
@LDAPConnectionParameters(bindCredential=LDAPConnectionParameters.BindCredential.VAULT, bindType=LDAPConnectionParameters.BindType.SIMPLE, encryption=LDAPConnectionParameters.Encryption.STARTTLS)
@AuthServerContainerExclude(value = {AuthServerContainerExclude.AuthServer.QUARKUS, AuthServerContainerExclude.AuthServer.REMOTE}, details =
@AuthServerContainerExclude(value = REMOTE, details =
"java.io.NotSerializableException: com.sun.jndi.ldap.LdapCtx")
public void loginLDAPUserCredentialVaultAuthenticationSimpleEncryptionStartTLS() {
verifyConnectionUrlProtocolPrefix("ldap://");

View file

@ -9,12 +9,13 @@ import org.keycloak.testsuite.util.LDAPTestConfiguration;
import java.util.Map;
import static org.keycloak.models.LDAPConstants.BIND_CREDENTIAL;
import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.REMOTE;
/**
* @author mhajas
*/
@EnableVault
@AuthServerContainerExclude(value = {AuthServerContainerExclude.AuthServer.QUARKUS, AuthServerContainerExclude.AuthServer.REMOTE}, details = "java.io.NotSerializableException: com.sun.jndi.ldap.LdapCtx")
@AuthServerContainerExclude(value = REMOTE, details = "java.io.NotSerializableException: com.sun.jndi.ldap.LdapCtx")
public class LDAPVaultCredentialsTest extends LDAPSyncTest {
private static final String VAULT_EXPRESSION = "${vault.ldap_bindCredential}";

View file

@ -60,7 +60,6 @@ import static org.junit.Assert.assertTrue;
import static org.keycloak.testsuite.util.ServerURLs.AUTH_SERVER_PORT;
import static org.keycloak.testsuite.util.ServerURLs.AUTH_SERVER_SCHEME;
import static org.keycloak.testsuite.util.ServerURLs.getAuthServerContextRoot;
import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.QUARKUS;
import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.REMOTE;
@AuthServerContainerExclude({REMOTE})

View file

@ -32,7 +32,6 @@ import org.keycloak.vault.VaultTranscriber;
import java.util.List;
import java.util.Optional;
import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.QUARKUS;
import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer.REMOTE;
/**
@ -42,7 +41,7 @@ import static org.keycloak.testsuite.arquillian.annotation.AuthServerContainerEx
* @author <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/
@EnableVault
@AuthServerContainerExclude({REMOTE, QUARKUS})
@AuthServerContainerExclude(REMOTE)
public class KeycloakVaultTest extends AbstractKeycloakTest {
@Override

View file

@ -21,6 +21,7 @@ import java.nio.charset.StandardCharsets;
import java.security.Security;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
@ -349,6 +350,11 @@ public class ElytronCSKeyStoreProviderTest {
public Config.Scope scope(String... scope) {
throw new UnsupportedOperationException("not implemented");
}
@Override
public Set<String> getPropertyNames() {
throw new UnsupportedOperationException("not implemented");
}
}
static class SecretContains extends TypeSafeMatcher<VaultRawSecret> {