enhance: supporting versioned features (#24811)

also adding a common PropertyMapper validation method

closes #24668

Co-authored-by: Václav Muzikář <vaclav@muzikari.cz>
Co-authored-by: Martin Bartoš <mabartos@redhat.com>
This commit is contained in:
Steven Hawkins 2024-01-03 11:56:31 -05:00 committed by GitHub
parent 15b10f58fc
commit 667ce4be9e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
41 changed files with 805 additions and 417 deletions

View file

@ -18,19 +18,21 @@
package org.keycloak.common;
import org.jboss.logging.Logger;
import org.keycloak.common.Profile.Feature.Type;
import org.keycloak.common.profile.ProfileConfigResolver;
import org.keycloak.common.profile.ProfileConfigResolver.FeatureConfig;
import org.keycloak.common.profile.ProfileException;
import org.keycloak.common.profile.PropertiesFileProfileConfigResolver;
import org.keycloak.common.profile.PropertiesProfileConfigResolver;
import org.keycloak.common.util.KerberosJdkProvider;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -40,12 +42,14 @@ import java.util.stream.Stream;
*/
public class Profile {
private static volatile Map<String, TreeSet<Feature>> FEATURES;
public enum Feature {
AUTHORIZATION("Authorization Service", Type.DEFAULT),
ACCOUNT_API("Account Management REST API", Type.DEFAULT),
ACCOUNT2("Account Management Console version 2", Type.DEFAULT, Feature.ACCOUNT_API),
ACCOUNT3("Account Management Console version 3", Type.PREVIEW, Feature.ACCOUNT_API),
ACCOUNT2("Account Management Console 2", Type.DEFAULT, Feature.ACCOUNT_API),
ACCOUNT3("Account Management Console 3", Type.PREVIEW, Feature.ACCOUNT_API),
ADMIN_FINE_GRAINED_AUTHZ("Fine-Grained Admin Permissions", Type.PREVIEW),
@ -103,21 +107,54 @@ public class Profile {
private final String label;
private Set<Feature> dependencies;
Feature(String label, Type type) {
this.label = label;
this.type = type;
}
private int version;
Feature(String label, Type type, Feature... dependencies) {
this(label, type, 1, dependencies);
}
/**
* allowNameKey should be false for new versioned features to disallow using a legacy name, like account2
*/
Feature(String label, Type type, int version, Feature... dependencies) {
this.label = label;
this.type = type;
this.version = version;
if (this.version > 1 && !this.name().endsWith("_V" + version)) {
throw new IllegalStateException("It is expected that the enum name ends with the version");
}
this.dependencies = Arrays.stream(dependencies).collect(Collectors.toSet());
}
/**
* Get the key that uniquely identifies this feature, may be used by users if
* allowNameKey is true.
* <p>
* {@link #getVersionedKey()} should instead be shown to users where possible.
*/
public String getKey() {
return name().toLowerCase().replaceAll("_", "-");
}
/**
* Return the key without any versioning. All features of the same type
* will share this key.
*/
public String getUnversionedKey() {
String key = getKey();
if (version == 1) {
return key;
}
return key.substring(0, key.length() - (String.valueOf(version).length() + 2));
}
/**
* Return the key in the form key:v{version}
*/
public String getVersionedKey() {
return getUnversionedKey() + ":v" + version;
}
public String getLabel() {
return label;
}
@ -130,13 +167,18 @@ public class Profile {
return dependencies;
}
public int getVersion() {
return version;
}
public enum Type {
// in priority order
DEFAULT("Default"),
DISABLED_BY_DEFAULT("Disabled by default"),
DEPRECATED("Deprecated"),
PREVIEW("Preview"),
PREVIEW_DISABLED_BY_DEFAULT("Preview disabled by default"), // Preview features, which are not automatically enabled even with enabled preview profile (Needs to be enabled explicitly)
EXPERIMENTAL("Experimental"),
DEPRECATED("Deprecated");
EXPERIMENTAL("Experimental");
private final String label;
@ -152,12 +194,6 @@ public class Profile {
private static final Logger logger = Logger.getLogger(Profile.class);
private static final List<ProfileConfigResolver> DEFAULT_RESOLVERS = new LinkedList<>();
static {
DEFAULT_RESOLVERS.add(new PropertiesProfileConfigResolver(System.getProperties()));
DEFAULT_RESOLVERS.add(new PropertiesFileProfileConfigResolver());
};
private static Profile CURRENT;
private final ProfileName profileName;
@ -170,13 +206,127 @@ public class Profile {
public static Profile configure(ProfileConfigResolver... resolvers) {
ProfileName profile = Arrays.stream(resolvers).map(ProfileConfigResolver::getProfileName).filter(Objects::nonNull).findFirst().orElse(ProfileName.DEFAULT);
Map<Feature, Boolean> features = Arrays.stream(Feature.values()).collect(Collectors.toMap(f -> f, f -> isFeatureEnabled(profile, f, resolvers)));
Map<Feature, Boolean> features = new LinkedHashMap<>();
for (Map.Entry<String, TreeSet<Feature>> entry : getOrderedFeatures().entrySet()) {
// first check by unversioned key - if enabled, choose the highest priority feature
String unversionedFeature = entry.getKey();
ProfileConfigResolver.FeatureConfig unversionedConfig = getFeatureConfig(unversionedFeature, resolvers);
Feature enabledFeature = null;
if (unversionedConfig == FeatureConfig.ENABLED) {
enabledFeature = entry.getValue().iterator().next();
}
// now check each feature version to ensure consistency and select any features enabled by default
boolean isExplicitlyEnabledFeature = false;
for (Feature f : entry.getValue()) {
ProfileConfigResolver.FeatureConfig configuration = getFeatureConfig(f.getVersionedKey(), resolvers);
if (configuration != FeatureConfig.UNCONFIGURED && unversionedConfig != FeatureConfig.UNCONFIGURED) {
throw new ProfileException("Versioned feature " + f.getVersionedKey() + " is not expected as " + unversionedFeature + " is already " + unversionedConfig.name().toLowerCase());
}
switch (configuration) {
case ENABLED:
if (isExplicitlyEnabledFeature) {
throw new ProfileException(
String.format("Multiple versions of the same feature %s, %s should not be enabled.",
enabledFeature.getVersionedKey(), f.getVersionedKey()));
}
// even if something else was enabled by default, explicitly enabling a lower priority feature takes precedence
enabledFeature = f;
isExplicitlyEnabledFeature = true;
break;
case DISABLED:
throw new ProfileException("Feature " + f.getVersionedKey() + " should not be disabled using a versioned key.");
default:
if (unversionedConfig == FeatureConfig.UNCONFIGURED && enabledFeature == null && isEnabledByDefault(profile, f)) {
enabledFeature = f;
}
break;
}
}
for (Feature f : entry.getValue()) {
features.put(f, f == enabledFeature);
}
}
verifyConfig(features);
CURRENT = new Profile(profile, features);
return CURRENT;
}
private static boolean isEnabledByDefault(ProfileName profile, Feature f) {
switch (f.getType()) {
case DEFAULT:
return true;
case PREVIEW:
return profile.equals(ProfileName.PREVIEW);
default:
return false;
}
}
private static ProfileConfigResolver.FeatureConfig getFeatureConfig(String unversionedFeature,
ProfileConfigResolver... resolvers) {
ProfileConfigResolver.FeatureConfig configuration = Arrays.stream(resolvers).map(r -> r.getFeatureConfig(unversionedFeature))
.filter(r -> !r.equals(ProfileConfigResolver.FeatureConfig.UNCONFIGURED))
.findFirst()
.orElse(ProfileConfigResolver.FeatureConfig.UNCONFIGURED);
return configuration;
}
/**
* Compute a map of unversioned feature keys to ordered sets (highest first) of features. The priority order for features is:
* <p>
* <ul>
* <li>The highest default supported version
* <li>The highest non-default supported version
* <li>The highest deprecated version
* <li>The highest preview version
* <li>The highest experimental version
* <ul>
* <p>
* Note the {@link Type} enum is ordered based upon priority.
*/
private static Map<String, TreeSet<Feature>> getOrderedFeatures() {
if (FEATURES == null) {
// "natural" ordering low to high between two features
Comparator<Feature> comparator = Comparator.comparing(Feature::getType).thenComparingInt(Feature::getVersion);
// aggregate the features by unversioned key
HashMap<String, TreeSet<Feature>> features = new HashMap<>();
Stream.of(Feature.values()).forEach(f -> features.compute(f.getUnversionedKey(), (k, v) -> {
if (v == null) {
v = new TreeSet<>(comparator.reversed()); // we want the highest priority first
}
v.add(f);
return v;
}));
FEATURES = features;
}
return FEATURES;
}
public static Set<String> getAllUnversionedFeatureNames() {
return Collections.unmodifiableSet(getOrderedFeatures().keySet());
}
/**
* Get all of the feature versions for the given feature. They will be ordered by priority.
* <p>
* If the feature does not exist an empty collection will be returned.
*/
public static Set<Feature> getFeatureVersions(String feature) {
TreeSet<Feature> versions = getOrderedFeatures().get(feature);
if (versions == null) {
return Collections.emptySet();
}
return Collections.unmodifiableSet(versions);
}
public static Profile init(ProfileName profileName, Map<Feature, Boolean> features) {
CURRENT = new Profile(profileName, features);
return CURRENT;
@ -238,28 +388,6 @@ public class Profile {
PREVIEW
}
private static Boolean isFeatureEnabled(ProfileName profile, Feature feature, ProfileConfigResolver... resolvers) {
ProfileConfigResolver.FeatureConfig configuration = Arrays.stream(resolvers).map(r -> r.getFeatureConfig(feature))
.filter(r -> !r.equals(ProfileConfigResolver.FeatureConfig.UNCONFIGURED))
.findFirst()
.orElse(ProfileConfigResolver.FeatureConfig.UNCONFIGURED);
switch (configuration) {
case ENABLED:
return true;
case DISABLED:
return false;
default:
switch (feature.getType()) {
case DEFAULT:
return true;
case PREVIEW:
return profile.equals(ProfileName.PREVIEW);
default:
return false;
}
}
}
private static void verifyConfig(Map<Feature, Boolean> features) {
for (Feature f : features.keySet()) {
if (features.get(f) && f.getDependencies() != null) {
@ -285,7 +413,7 @@ public class Profile {
String enabledFeaturesOfType = features.entrySet().stream()
.filter(e -> e.getValue() && checkedFeatureTypes.contains(e.getKey().getType()))
.map(e -> e.getKey().getKey()).sorted().collect(Collectors.joining(", "));
.map(e -> e.getKey().getVersionedKey()).sorted().collect(Collectors.joining(", "));
if (!enabledFeaturesOfType.isEmpty()) {
logger.logv(level, "{0} features enabled: {1}", type.getLabel(), enabledFeaturesOfType);

View file

@ -3,8 +3,8 @@ package org.keycloak.common.profile;
import org.keycloak.common.Profile;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;
public class CommaSeparatedListProfileConfigResolver implements ProfileConfigResolver {
@ -13,10 +13,10 @@ public class CommaSeparatedListProfileConfigResolver implements ProfileConfigRes
public CommaSeparatedListProfileConfigResolver(String enabledFeatures, String disabledFeatures) {
if (enabledFeatures != null) {
this.enabledFeatures = Arrays.stream(enabledFeatures.split(",")).collect(Collectors.toSet());
this.enabledFeatures = new HashSet<>(Arrays.asList(enabledFeatures.split(",")));
}
if (disabledFeatures != null) {
this.disabledFeatures = Arrays.stream(disabledFeatures.split(",")).collect(Collectors.toSet());
this.disabledFeatures = new HashSet<>(Arrays.asList(disabledFeatures.split(",")));
}
}
@ -29,11 +29,14 @@ public class CommaSeparatedListProfileConfigResolver implements ProfileConfigRes
}
@Override
public FeatureConfig getFeatureConfig(Profile.Feature feature) {
String key = feature.getKey();
if (enabledFeatures != null && enabledFeatures.contains(key)) {
public FeatureConfig getFeatureConfig(String feature) {
if (enabledFeatures != null && enabledFeatures.contains(feature)) {
if (disabledFeatures != null && disabledFeatures.contains(feature)) {
throw new ProfileException(feature + " is in both the enabled and disabled feature lists.");
}
return FeatureConfig.ENABLED;
} else if (disabledFeatures != null && disabledFeatures.contains(key)) {
}
if (disabledFeatures != null && disabledFeatures.contains(feature)) {
return FeatureConfig.DISABLED;
}
return FeatureConfig.UNCONFIGURED;

View file

@ -6,7 +6,7 @@ public interface ProfileConfigResolver {
Profile.ProfileName getProfileName();
FeatureConfig getFeatureConfig(Profile.Feature feature);
FeatureConfig getFeatureConfig(String feature);
public enum FeatureConfig {
ENABLED,

View file

@ -1,24 +1,24 @@
package org.keycloak.common.profile;
import org.keycloak.common.Profile;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;
public class PropertiesFileProfileConfigResolver implements ProfileConfigResolver {
private Properties properties;
public class PropertiesFileProfileConfigResolver extends PropertiesProfileConfigResolver {
public PropertiesFileProfileConfigResolver() {
super(loadProperties());
}
private static Properties loadProperties() {
Properties properties = new Properties();
try {
String jbossServerConfigDir = System.getProperty("jboss.server.config.dir");
if (jbossServerConfigDir != null) {
File file = new File(jbossServerConfigDir, "profile.properties");
if (file.isFile()) {
try (FileInputStream is = new FileInputStream(file)) {
properties = new Properties();
properties.load(is);
}
}
@ -26,34 +26,7 @@ public class PropertiesFileProfileConfigResolver implements ProfileConfigResolve
} catch (IOException e) {
throw new ProfileException("Failed to load profile propeties file", e);
}
return properties;
}
@Override
public Profile.ProfileName getProfileName() {
if (properties != null) {
String profile = properties.getProperty("profile");
if (profile != null) {
return Profile.ProfileName.valueOf(profile.toUpperCase());
}
}
return null;
}
@Override
public FeatureConfig getFeatureConfig(Profile.Feature feature) {
if (properties != null) {
String config = properties.getProperty("feature." + feature.name().toLowerCase());
if (config != null) {
switch (config) {
case "enabled":
return FeatureConfig.ENABLED;
case "disabled":
return FeatureConfig.DISABLED;
default:
throw new ProfileException("Invalid config value '" + config + "' for feature " + feature.getKey());
}
}
}
return FeatureConfig.UNCONFIGURED;
}
}

View file

@ -1,26 +1,33 @@
package org.keycloak.common.profile;
import org.keycloak.common.Profile;
import org.keycloak.common.Profile.Feature;
import java.util.Properties;
import java.util.function.UnaryOperator;
public class PropertiesProfileConfigResolver implements ProfileConfigResolver {
private Properties properties;
private UnaryOperator<String> getter;
public PropertiesProfileConfigResolver(Properties properties) {
this.properties = properties;
this(properties::getProperty);
}
public PropertiesProfileConfigResolver(UnaryOperator<String> getter) {
this.getter = getter;
}
@Override
public Profile.ProfileName getProfileName() {
String profile = properties.getProperty("keycloak.profile");
String profile = getter.apply("keycloak.profile");
return profile != null ? Profile.ProfileName.valueOf(profile.toUpperCase()) : null;
}
@Override
public FeatureConfig getFeatureConfig(Profile.Feature feature) {
String config = properties.getProperty("keycloak.profile.feature." + feature.name().toLowerCase());
public FeatureConfig getFeatureConfig(String feature) {
String key = getPropertyKey(feature);
String config = getter.apply(key);
if (config != null) {
switch (config) {
case "enabled":
@ -28,9 +35,17 @@ public class PropertiesProfileConfigResolver implements ProfileConfigResolver {
case "disabled":
return FeatureConfig.DISABLED;
default:
throw new ProfileException("Invalid config value '" + config + "' for feature " + feature.getKey());
throw new ProfileException("Invalid config value '" + config + "' for feature key " + key);
}
}
return FeatureConfig.UNCONFIGURED;
}
public static String getPropertyKey(Feature feature) {
return getPropertyKey(feature.getKey());
}
public static String getPropertyKey(String feature) {
return "keycloak.profile.feature." + feature.replaceAll("-", "_");
}
}

View file

@ -8,13 +8,8 @@ import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.keycloak.common.profile.CommaSeparatedListProfileConfigResolver;
import org.keycloak.common.profile.ProfileException;
import org.keycloak.common.profile.PropertiesFileProfileConfigResolver;
import org.keycloak.common.profile.PropertiesProfileConfigResolver;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
@ -22,6 +17,8 @@ import java.util.HashSet;
import java.util.Properties;
import java.util.Set;
import static org.junit.Assert.assertThrows;
public class ProfileTest {
private static final Profile.Feature DEFAULT_FEATURE = Profile.Feature.AUTHORIZATION;
@ -126,7 +123,7 @@ public class ProfileTest {
try {
Profile.configure(new PropertiesProfileConfigResolver(properties));
} catch (ProfileException e) {
Assert.assertEquals("Invalid config value 'invalid' for feature account-api", e.getMessage());
Assert.assertEquals("Invalid config value 'invalid' for feature key keycloak.profile.feature.account_api", e.getMessage());
}
}
@ -148,30 +145,6 @@ public class ProfileTest {
Assert.assertTrue(Profile.isFeatureEnabled(PREVIEW_FEATURE));
}
@Test
public void enablePreviewWithPropertiesFile() throws IOException {
Properties properties = new Properties();
properties.setProperty("profile", "preview");
Path tempDirectory = Files.createTempDirectory("jboss-config");
System.setProperty("jboss.server.config.dir", tempDirectory.toString());
Path profileProperties = tempDirectory.resolve("profile.properties");
try(OutputStream out = Files.newOutputStream(profileProperties.toFile().toPath())) {
properties.store(out, "");
}
Profile.configure(new PropertiesFileProfileConfigResolver());
Assert.assertEquals(Profile.ProfileName.PREVIEW, Profile.getInstance().getName());
Assert.assertTrue(Profile.isFeatureEnabled(PREVIEW_FEATURE));
Files.delete(profileProperties);
Files.delete(tempDirectory);
System.getProperties().remove("jboss.server.config.dir");
}
@Test
public void configWithCommaSeparatedList() {
String enabledFeatures = DISABLED_BY_DEFAULT_FEATURE.getKey() + "," + PREVIEW_FEATURE.getKey() + "," + EXPERIMENTAL_FEATURE.getKey();
@ -191,15 +164,63 @@ public class ProfileTest {
}
}
@Test
public void testKeys() {
Assert.assertEquals("account2", Profile.Feature.ACCOUNT2.getKey());
Assert.assertEquals("account2", Profile.Feature.ACCOUNT2.getUnversionedKey());
Assert.assertEquals("account2:v1", Profile.Feature.ACCOUNT2.getVersionedKey());
}
@Test
public void configWithCommaSeparatedVersionedList() {
String enabledFeatures = DISABLED_BY_DEFAULT_FEATURE.getVersionedKey() + "," + PREVIEW_FEATURE.getVersionedKey() + "," + EXPERIMENTAL_FEATURE.getVersionedKey();
if (DEPRECATED_FEATURE != null) {
enabledFeatures += "," + DEPRECATED_FEATURE.getVersionedKey();
}
String disabledFeatures = DEFAULT_FEATURE.getUnversionedKey();
Profile.configure(new CommaSeparatedListProfileConfigResolver(enabledFeatures, disabledFeatures));
Assert.assertFalse(Profile.isFeatureEnabled(DEFAULT_FEATURE));
Assert.assertTrue(Profile.isFeatureEnabled(DISABLED_BY_DEFAULT_FEATURE));
Assert.assertTrue(Profile.isFeatureEnabled(PREVIEW_FEATURE));
Assert.assertTrue(Profile.isFeatureEnabled(EXPERIMENTAL_FEATURE));
if (DEPRECATED_FEATURE != null) {
Assert.assertTrue(Profile.isFeatureEnabled(DEPRECATED_FEATURE));
}
}
@Test
public void configWithCommaSeparatedInvalidDisabled() {
String disabledFeatures = DEFAULT_FEATURE.getVersionedKey();
CommaSeparatedListProfileConfigResolver resolver = new CommaSeparatedListProfileConfigResolver(null, disabledFeatures);
assertThrows(ProfileException.class, () -> Profile.configure(resolver));
}
@Test
public void commaSeparatedVersionedConflict() {
String enabledFeatures = DEFAULT_FEATURE.getVersionedKey();
String disabledFeatures = DEFAULT_FEATURE.getVersionedKey();
CommaSeparatedListProfileConfigResolver resolver = new CommaSeparatedListProfileConfigResolver(enabledFeatures, disabledFeatures);
assertThrows(ProfileException.class, () -> Profile.configure(resolver));
}
@Test
public void commaSeparatedDuplicateEnabled() {
String enabledFeatures = DEFAULT_FEATURE.getVersionedKey() + "," + DEFAULT_FEATURE.getUnversionedKey();
CommaSeparatedListProfileConfigResolver resolver = new CommaSeparatedListProfileConfigResolver(enabledFeatures, null);
assertThrows(ProfileException.class, () -> Profile.configure(resolver));
}
@Test
public void configWithProperties() {
Properties properties = new Properties();
properties.setProperty("keycloak.profile.feature." + DEFAULT_FEATURE.name().toLowerCase(), "disabled");
properties.setProperty("keycloak.profile.feature." + DISABLED_BY_DEFAULT_FEATURE.name().toLowerCase(), "enabled");
properties.setProperty("keycloak.profile.feature." + PREVIEW_FEATURE.name().toLowerCase(), "enabled");
properties.setProperty("keycloak.profile.feature." + EXPERIMENTAL_FEATURE.name().toLowerCase(), "enabled");
properties.setProperty(PropertiesProfileConfigResolver.getPropertyKey(DEFAULT_FEATURE), "disabled");
properties.setProperty(PropertiesProfileConfigResolver.getPropertyKey(DISABLED_BY_DEFAULT_FEATURE), "enabled");
properties.setProperty(PropertiesProfileConfigResolver.getPropertyKey(PREVIEW_FEATURE), "enabled");
properties.setProperty(PropertiesProfileConfigResolver.getPropertyKey(EXPERIMENTAL_FEATURE), "enabled");
if (DEPRECATED_FEATURE != null) {
properties.setProperty("keycloak.profile.feature." + DEPRECATED_FEATURE.name().toLowerCase(), "enabled");
properties.setProperty(PropertiesProfileConfigResolver.getPropertyKey(DEPRECATED_FEATURE), "enabled");
}
Profile.configure(new PropertiesProfileConfigResolver(properties));
@ -213,45 +234,10 @@ public class ProfileTest {
}
}
@Test
public void configWithPropertiesFile() throws IOException {
Properties properties = new Properties();
properties.setProperty("feature." + DEFAULT_FEATURE.name().toLowerCase(), "disabled");
properties.setProperty("feature." + DISABLED_BY_DEFAULT_FEATURE.name().toLowerCase(), "enabled");
properties.setProperty("feature." + PREVIEW_FEATURE.name().toLowerCase(), "enabled");
properties.setProperty("feature." + EXPERIMENTAL_FEATURE.name().toLowerCase(), "enabled");
if (DEPRECATED_FEATURE != null) {
properties.setProperty("feature." + DEPRECATED_FEATURE.name().toLowerCase(), "enabled");
}
Path tempDirectory = Files.createTempDirectory("jboss-config");
System.setProperty("jboss.server.config.dir", tempDirectory.toString());
Path profileProperties = tempDirectory.resolve("profile.properties");
try(OutputStream out = Files.newOutputStream(profileProperties.toFile().toPath())) {
properties.store(out, "");
}
Profile.configure(new PropertiesFileProfileConfigResolver());
Assert.assertFalse(Profile.isFeatureEnabled(DEFAULT_FEATURE));
Assert.assertTrue(Profile.isFeatureEnabled(DISABLED_BY_DEFAULT_FEATURE));
Assert.assertTrue(Profile.isFeatureEnabled(PREVIEW_FEATURE));
Assert.assertTrue(Profile.isFeatureEnabled(EXPERIMENTAL_FEATURE));
if (DEPRECATED_FEATURE != null) {
Assert.assertTrue(Profile.isFeatureEnabled(DEPRECATED_FEATURE));
}
Files.delete(profileProperties);
Files.delete(tempDirectory);
System.getProperties().remove("jboss.server.config.dir");
}
@Test
public void configWithMultipleResolvers() {
Properties properties = new Properties();
properties.setProperty("keycloak.profile.feature." + PREVIEW_FEATURE.name().toLowerCase(), "enabled");
properties.setProperty(PropertiesProfileConfigResolver.getPropertyKey(PREVIEW_FEATURE), "enabled");
Profile.configure(new CommaSeparatedListProfileConfigResolver(DISABLED_BY_DEFAULT_FEATURE.getKey(), ""), new PropertiesProfileConfigResolver(properties));

View file

@ -6,6 +6,12 @@ The Keycloak JS adapter now uses the https://webpack.js.org/guides/package-expor
Keycloak introduces an improved truststores configuration options. The Keycloak truststore is now used across the server: for outgoing connections, mTLS, database drivers and more. It's no longer needed to configure separate truststores for individual areas. To configure the truststore, you can put your truststores files or certificates in the default `conf/truststores`, or use the new `truststore-paths` config option. For details refer to the relevant https://www.keycloak.org/server/keycloak-truststore[guide].
= Versioned Features
Features now support versioning. For preserving backward compatibility all existing features (incl. `account2` and `account3`) are marked as version 1. Newly introduced feature will leverage the versioning allowing users to easily select between different implementations of desired features.
For details refer to the https://www.keycloak.org/server/features[features guide].
== Keycloak CR Truststores
You may also take advantage of the new server-side handling of truststores via the Keycloak CR, for example:

View file

@ -13,6 +13,13 @@ import Keycloak from 'keycloak-js';
import AuthZ from 'keycloak-js/authz';
----
= Features Changes
It is no longer allowed to have the same feature in both the `--features` and `--features-disabled` list. The feature should appear in only one list.
The usage of unversioned feature names, e.g. `docker`, in the `--features` list will allow for the most supported / latest feature version to be enabled for you.
If you need more predictable behavior across releases, reference the particular version you want instead, e.g. `docker:v1`.
= Truststore Changes
The `spi-truststore-file-*` options and the truststore related options `https-trust-store-*` are deprecated, please use the new default location for truststore material, `conf/truststores`, or specify your desired paths via the `truststore-paths` option. For details refer to the relevant https://www.keycloak.org/server/keycloak-truststore[guide].

View file

@ -22,6 +22,14 @@ To enable all preview features, enter this command:
<@kc.build parameters="--features=\"preview\""/>
Enabled feature may be versioned, or unversioned. If you use a versioned feature name, e.g. feature:v1, that exact feature version will be enabled as long as it still exists in the runtime. If you instead use an unversioned name, e.g. just feature, the selection of the particular supported feature version may change from release to release according to the following precedence:
1. The highest default supported version
1. The highest non-default supported version
1. The highest deprecated version
1. The highest preview version
1. The highest experimental version
== Disabling features
To disable a feature that is enabled by default, enter this command:
@ -37,7 +45,9 @@ You can disable all default features by entering this command:
<@kc.build parameters="--features-disabled=\"default\""/>
This command can be used in combination with `features` to explicitly set what features should be available.
If a feature is added both to the `features-disabled` list and the `features` list, it will be enabled.
It is not allowed to have a feature in both the `features-disabled` list and the `features` list.
When a feature is disabled all versions of that feature are disabled.
== Supported features

View file

@ -6,29 +6,35 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public class FeatureOptions {
public static final Option<List> FEATURES = new OptionBuilder("features", List.class, Profile.Feature.class)
.category(OptionCategory.FEATURE)
.description("Enables a set of one or more features.")
.expectedValues(FeatureOptions::getFeatureValues)
.defaultValue(Optional.empty())
.expectedValues(() -> getFeatureValues(true))
.buildTime(true)
.build();
public static final Option FEATURES_DISABLED = new OptionBuilder("features-disabled", List.class, Profile.Feature.class)
.category(OptionCategory.FEATURE)
.description("Disables a set of one or more features.")
.expectedValues(FeatureOptions::getFeatureValues)
.expectedValues(() -> getFeatureValues(false))
.buildTime(true)
.build();
private static List<String> getFeatureValues() {
public static List<String> getFeatureValues(boolean includeVersions) {
List<String> features = new ArrayList<>();
for (Profile.Feature value : Profile.Feature.values()) {
features.add(value.getKey());
if (includeVersions) {
Profile.getAllUnversionedFeatureNames().forEach(f -> {
features.add(f + "[:" + Profile.getFeatureVersions(f).stream().sorted().map(v -> "v" + v.getVersion())
.collect(Collectors.joining(",")) + "]");
});
} else {
features.addAll(Profile.getAllUnversionedFeatureNames());
}
features.add(Profile.Feature.Type.PREVIEW.name().toLowerCase());

View file

@ -22,7 +22,7 @@ public class Option<T> {
this.category = category;
this.hidden = hidden;
this.buildTime = buildTime;
this.description = getDescriptionByCategorySupportLevel(description);
this.description = getDescriptionByCategorySupportLevel(description, category);
this.defaultValue = defaultValue;
this.expectedValues = expectedValues;
this.deprecatedMetadata = deprecatedMetadata;
@ -74,12 +74,9 @@ public class Option<T> {
);
}
private String getDescriptionByCategorySupportLevel(String description) {
if(description == null || description.isBlank()) {
return description;
}
switch(this.getCategory().getSupportLevel()) {
private static String getDescriptionByCategorySupportLevel(String description, OptionCategory category) {
if (description != null && !description.isBlank()) {
switch (category.getSupportLevel()) {
case PREVIEW:
description = "Preview: " + description;
break;
@ -87,7 +84,8 @@ public class Option<T> {
description = "Experimental: " + description;
break;
default:
description = description;
break;
}
}
return description;

View file

@ -37,6 +37,7 @@ import io.smallrye.config.SmallRyeConfig;
import org.apache.commons.lang3.SystemUtils;
import org.keycloak.common.Profile;
import org.keycloak.common.profile.PropertiesFileProfileConfigResolver;
import org.keycloak.common.profile.PropertiesProfileConfigResolver;
import org.keycloak.quarkus.runtime.configuration.PersistedConfigSource;
public final class Environment {
@ -101,7 +102,7 @@ public final class Environment {
public static String getProfile() {
String profile = System.getProperty(PROFILE);
if (profile == null) {
profile = System.getenv(ENV_PROFILE);
}
@ -132,7 +133,7 @@ public final class Environment {
if (profile == null) {
profile = defaultProfile;
}
return profile;
}
@ -249,7 +250,7 @@ public final class Environment {
Profile profile = Profile.getInstance();
if (profile == null) {
profile = Profile.configure(new QuarkusProfileConfigResolver(), new PropertiesFileProfileConfigResolver());
profile = Profile.configure(new QuarkusProfileConfigResolver(), new PropertiesProfileConfigResolver(QuarkusProfileConfigResolver::getConfig), new PropertiesFileProfileConfigResolver());
}
return profile;

View file

@ -91,7 +91,7 @@ public class KeycloakMain implements QuarkusApplication {
}
try {
Picocli.validateNonCliConfig(cliArgs, new Start(), new PrintWriter(System.out, true));
Picocli.validateConfig(cliArgs, new Start(), new PrintWriter(System.out, true));
} catch (PropertyException e) {
errorHandler.error(errStream, e.getMessage(), null);
System.exit(ExitCode.USAGE);

View file

@ -9,7 +9,7 @@ public class QuarkusProfileConfigResolver extends CommaSeparatedListProfileConfi
super(getConfig("kc.features"), getConfig("kc.features-disabled"));
}
private static String getConfig(String key) {
static String getConfig(String key) {
return Configuration.getRawPersistedProperty(key)
.orElse(Configuration.getRawValue(key));
}

View file

@ -68,6 +68,7 @@ import org.keycloak.quarkus.runtime.cli.command.Tools;
import org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource;
import org.keycloak.quarkus.runtime.configuration.Configuration;
import org.keycloak.quarkus.runtime.configuration.PersistedConfigSource;
import org.keycloak.quarkus.runtime.configuration.PropertyMappingInterceptor;
import org.keycloak.quarkus.runtime.configuration.QuarkusPropertiesConfigSource;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMappers;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper;
@ -261,98 +262,93 @@ public final class Picocli {
}
/**
* validate the expected values of non-cli properties
* Additional validation and handling of deprecated options
*
* @param cliArgs
* @param abstractCommand
*/
public static void validateNonCliConfig(List<String> cliArgs, AbstractCommand abstractCommand, PrintWriter out) {
public static void validateConfig(List<String> cliArgs, AbstractCommand abstractCommand, PrintWriter out) {
IncludeOptions options = getIncludeOptions(cliArgs, abstractCommand, abstractCommand.getName());
if (!options.includeBuildTime && !options.includeRuntime) {
return;
}
List<String> ignoredBuildTime = new ArrayList<>();
List<String> ignoredRunTime = new ArrayList<>();
Set<String> deprecatedInUse = new HashSet<>();
for (OptionCategory category : abstractCommand.getOptionCategories()) {
List<PropertyMapper> mappers = new ArrayList<>();
Optional.ofNullable(PropertyMappers.getRuntimeMappers().get(category)).ifPresent(mappers::addAll);
Optional.ofNullable(PropertyMappers.getBuildTimeMappers().get(category)).ifPresent(mappers::addAll);
for (PropertyMapper mapper : mappers) {
// bypass the PropertyMappingInterceptor - the transformations may cause unexpected errors
String value = null;
ConfigSource configSource = null;
for (ConfigSource cs : getConfig().getConfigSources()) {
if (cs.getOrdinal() < 300) {
break; // don't consider anything below standard env properties
}
value = cs.getValue(mapper.getFrom());
if (value != null) {
configSource = cs;
break;
}
}
try {
PropertyMappingInterceptor.disable(); // we don't want the mapped / transformed properties, we want what the user effectively supplied
List<String> ignoredBuildTime = new ArrayList<>();
List<String> ignoredRunTime = new ArrayList<>();
Set<String> deprecatedInUse = new HashSet<>();
for (OptionCategory category : abstractCommand.getOptionCategories()) {
List<PropertyMapper> mappers = new ArrayList<>();
Optional.ofNullable(PropertyMappers.getRuntimeMappers().get(category)).ifPresent(mappers::addAll);
Optional.ofNullable(PropertyMappers.getBuildTimeMappers().get(category)).ifPresent(mappers::addAll);
for (PropertyMapper<?> mapper : mappers) {
ConfigValue configValue = Configuration.getConfigValue(mapper.getFrom());
if (value == null) {
continue;
}
if (mapper.isBuildTime() && !options.includeBuildTime) {
ignoredBuildTime.add(mapper.getFrom());
continue;
}
if (mapper.isRunTime() && !options.includeRuntime) {
ignoredRunTime.add(mapper.getFrom());
continue;
}
if (!PropertyMapperParameterConsumer.isExpectedValue(mapper.getExpectedValues(), value)) {
throw new PropertyException(PropertyMapperParameterConsumer.getErrorMessage(mapper.getFrom(),
value, mapper.getExpectedValues(), mapper.getExpectedValues()) + ". From ConfigSource " + configSource.getName());
}
mapper.getDeprecatedMetadata().ifPresent(d -> {
DeprecatedMetadata metadata = (DeprecatedMetadata) d;
String optionName = mapper.getFrom();
if (optionName.startsWith(NS_KEYCLOAK_PREFIX)) {
optionName = optionName.substring(NS_KEYCLOAK_PREFIX.length());
// don't consider missing or anything below standard env properties
if (configValue.getValue() == null || configValue.getConfigSourceOrdinal() < 300) {
continue;
}
StringBuilder sb = new StringBuilder("\t- ");
sb.append(optionName);
if (metadata.getNote() != null || !metadata.getNewOptionsKeys().isEmpty()) {
sb.append(":");
if (mapper.isBuildTime() && !options.includeBuildTime) {
ignoredBuildTime.add(mapper.getFrom());
continue;
}
if (metadata.getNote() != null) {
sb.append(" ");
sb.append(metadata.getNote());
if (!metadata.getNote().endsWith(".")) {
sb.append(".");
}
if (mapper.isRunTime() && !options.includeRuntime) {
ignoredRunTime.add(mapper.getFrom());
continue;
}
if (!metadata.getNewOptionsKeys().isEmpty()) {
sb.append(" Use ");
sb.append(String.join(", ", metadata.getNewOptionsKeys()));
sb.append(".");
}
deprecatedInUse.add(sb.toString());
});
mapper.validate(configValue);
mapper.getDeprecatedMetadata().ifPresent(metadata -> {
handleDeprecated(deprecatedInUse, mapper, metadata);
});
}
}
Logger logger = Logger.getLogger(Picocli.class); // logger can't be instantiated in a class field
if (!ignoredBuildTime.isEmpty()) {
outputIgnoredProperties(ignoredBuildTime, true, logger);
} else if (!ignoredRunTime.isEmpty()) {
outputIgnoredProperties(ignoredRunTime, false, logger);
}
if (!deprecatedInUse.isEmpty()) {
logger.warn("The following used options are DEPRECATED and will be removed in a future release:\n" + String.join("\n", deprecatedInUse));
}
} finally {
PropertyMappingInterceptor.enable();
}
}
private static void handleDeprecated(Set<String> deprecatedInUse, PropertyMapper<?> mapper,
DeprecatedMetadata metadata) {
String optionName = mapper.getFrom();
if (optionName.startsWith(NS_KEYCLOAK_PREFIX)) {
optionName = optionName.substring(NS_KEYCLOAK_PREFIX.length());
}
StringBuilder sb = new StringBuilder("\t- ");
sb.append(optionName);
if (metadata.getNote() != null || !metadata.getNewOptionsKeys().isEmpty()) {
sb.append(":");
}
if (metadata.getNote() != null) {
sb.append(" ");
sb.append(metadata.getNote());
if (!metadata.getNote().endsWith(".")) {
sb.append(".");
}
}
Logger logger = Logger.getLogger(Picocli.class); // logger can't be instantiated in a class field
if (!ignoredBuildTime.isEmpty()) {
outputIgnoredProperties(ignoredBuildTime, true, logger);
} else if (!ignoredRunTime.isEmpty()) {
outputIgnoredProperties(ignoredRunTime, false, logger);
}
if (!deprecatedInUse.isEmpty()) {
logger.warn("The following used options are DEPRECATED and will be removed in a future release:\n" + String.join("\n", deprecatedInUse));
if (!metadata.getNewOptionsKeys().isEmpty()) {
sb.append(" Use ");
sb.append(String.join(", ", metadata.getNewOptionsKeys()));
sb.append(".");
}
deprecatedInUse.add(sb.toString());
}
private static void outputIgnoredProperties(List<String> properties, boolean build, Logger logger) {

View file

@ -21,8 +21,6 @@ import static org.keycloak.quarkus.runtime.cli.Picocli.ARG_PREFIX;
import java.util.Collection;
import java.util.Stack;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.keycloak.utils.StringUtil;
@ -58,22 +56,16 @@ public final class PropertyMapperParameterConsumer implements CommandLine.IParam
commandLine, "Missing required value for option '" + name + "' (" + argSpec.paramLabel() + ")." + getExpectedValuesMessage(argSpec.completionCandidates(), option.completionCandidates()));
}
// consumes the value
String value = args.pop();
// consumes the value, actual value validation will be performed later
args.pop();
if (!args.isEmpty() && isOptionValue(args.peek())) {
throw new ParameterException(
commandLine, "Option '" + name + "' expects a single value (" + argSpec.paramLabel() + ")" + getExpectedValuesMessage(argSpec.completionCandidates(), option.completionCandidates()));
}
if (isExpectedValue(StreamSupport.stream(option.completionCandidates().spliterator(), false).collect(Collectors.toList()), value)) {
return;
}
throw new ParameterException(commandLine, getErrorMessage(name, value, argSpec.completionCandidates(), option.completionCandidates()));
}
static String getErrorMessage(String name, String value, Iterable<String> specCandidates, Iterable<String> optionCandidates) {
public static String getErrorMessage(String name, String value, Iterable<String> specCandidates, Iterable<String> optionCandidates) {
return "Invalid value for option '" + name + "': " + value + "." + getExpectedValuesMessage(specCandidates, optionCandidates);
}
@ -85,7 +77,7 @@ public final class PropertyMapperParameterConsumer implements CommandLine.IParam
return optionCandidates.iterator().hasNext() ? " Expected values are: " + String.join(", ", specCandidates) : "";
}
static boolean isExpectedValue(Collection<String> expectedValues, String value) {
public static boolean isExpectedValue(Collection<String> expectedValues, String value) {
if (expectedValues.isEmpty()) {
// accept any
return true;

View file

@ -64,8 +64,8 @@ public abstract class AbstractCommand {
return Arrays.asList(OptionCategory.values());
}
protected void validateNonCliConfig() {
Picocli.validateNonCliConfig(ConfigArgsConfigSource.getAllCliArgs(), this, spec.commandLine().getOut());
protected void validateConfig() {
Picocli.validateConfig(ConfigArgsConfigSource.getAllCliArgs(), this, spec.commandLine().getOut());
}
public abstract String getName();

View file

@ -29,7 +29,7 @@ public abstract class AbstractStartCommand extends AbstractCommand implements Ru
public void run() {
doBeforeRun();
CommandLine cmd = spec.commandLine();
validateNonCliConfig();
validateConfig();
KeycloakMain.start((ExecutionExceptionHandler) cmd.getExecutionExceptionHandler(), cmd.getErr(), cmd.getParseResult().originalArgs().toArray(new String[0]));
}

View file

@ -74,7 +74,7 @@ public final class Build extends AbstractCommand implements Runnable {
exitWithErrorIfDevProfileIsSetAndNotStartDev();
System.setProperty("quarkus.launch.rebuild", "true");
validateNonCliConfig();
validateConfig();
println(spec.commandLine(), "Updating the configuration and installing your custom providers, if any. Please wait.");

View file

@ -46,6 +46,16 @@ import static org.keycloak.quarkus.runtime.Environment.isRebuild;
*/
public class PropertyMappingInterceptor implements ConfigSourceInterceptor {
private static ThreadLocal<Boolean> disable = new ThreadLocal<>();
public static void disable() {
disable.set(true);
}
public static void enable() {
disable.remove();
}
<T> Iterator<T> filterRuntime(Iterator<T> iter, Function<T, String> nameFunc) {
if (!isRebuild() && !Environment.isRebuildCheck()) {
return iter;
@ -70,6 +80,9 @@ public class PropertyMappingInterceptor implements ConfigSourceInterceptor {
@Override
public ConfigValue getValue(ConfigSourceInterceptorContext context, String name) {
if (Boolean.TRUE.equals(disable.get())) {
return context.proceed(name);
}
ConfigValue value = PropertyMappers.getValue(context, name);
if (value == null || value.getValue() == null) {

View file

@ -1,10 +1,21 @@
package org.keycloak.quarkus.runtime.configuration.mappers;
import org.keycloak.common.Profile;
import org.keycloak.common.Profile.Feature;
import org.keycloak.config.FeatureOptions;
import org.keycloak.quarkus.runtime.cli.PropertyException;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper.fromOption;
final class FeaturePropertyMappers {
public final class FeaturePropertyMappers {
private static Pattern VERSIONED_PATTERN = Pattern.compile("([^:]+):v(\\d+)");
private FeaturePropertyMappers() {
}
@ -13,10 +24,44 @@ final class FeaturePropertyMappers {
return new PropertyMapper[] {
fromOption(FeatureOptions.FEATURES)
.paramLabel("feature")
.validator((mapper, value) -> validateEnabledFeatures(value.getValue()))
.build(),
fromOption(FeatureOptions.FEATURES_DISABLED)
.paramLabel("feature")
.build()
};
}
public static void validateEnabledFeatures(String s) {
Stream.of(s.split(",")).forEach(feature -> {
if (!Profile.getFeatureVersions(feature).isEmpty()) {
return;
}
if (feature.equals(Profile.Feature.Type.PREVIEW.name().toLowerCase())) {
return;
}
Matcher matcher = VERSIONED_PATTERN.matcher(feature);
if (!matcher.matches()) {
if (feature.contains(":")) {
throw new PropertyException(String.format(
"%s has an invalid format for enabling a feature, expected format is feature:v{version}, e.g. docker:v1",
feature));
}
throw new PropertyException(String.format("%s is an unrecognized feature, it should be one of %s", feature,
FeatureOptions.getFeatureValues(false)));
}
String unversionedFeature = matcher.group(1);
Set<Feature> featureVersions = Profile.getFeatureVersions(unversionedFeature);
if (featureVersions.isEmpty()) {
throw new PropertyException(String.format("%s has an unrecognized feature, it should be one of %s",
feature, FeatureOptions.getFeatureValues(false)));
}
int version = Integer.parseInt(matcher.group(2));
if (!featureVersions.stream().anyMatch(f -> f.getVersion() == version)) {
throw new PropertyException(
String.format("%s has an unrecognized feature version, it should be one of %s", feature,
featureVersions.stream().map(Feature::getVersion).map(String::valueOf).collect(Collectors.toList())));
}
});
}
}

View file

@ -26,6 +26,7 @@ import static org.keycloak.quarkus.runtime.configuration.Configuration.toEnvVarF
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import io.smallrye.config.ConfigSourceInterceptorContext;
@ -36,6 +37,9 @@ import org.keycloak.config.DeprecatedMetadata;
import org.keycloak.config.Option;
import org.keycloak.config.OptionBuilder;
import org.keycloak.config.OptionCategory;
import org.keycloak.quarkus.runtime.cli.PropertyException;
import org.keycloak.quarkus.runtime.cli.PropertyMapperParameterConsumer;
import org.keycloak.quarkus.runtime.configuration.ConfigArgsConfigSource;
import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider;
@ -47,7 +51,8 @@ public class PropertyMapper<T> {
null,
null,
null,
false) {
false,
null) {
@Override
public ConfigValue getConfigValue(String name, ConfigSourceInterceptorContext context) {
return context.proceed(name);
@ -62,11 +67,12 @@ public class PropertyMapper<T> {
private final String paramLabel;
private final String envVarFormat;
private String cliFormat;
private BiConsumer<PropertyMapper<T>, ConfigValue> validator;
private static final Logger logger = Logger.getLogger(PropertyMapper.class);
PropertyMapper(Option<T> option, String to, BiFunction<Optional<String>, ConfigSourceInterceptorContext, Optional<String>> mapper,
String mapFrom, String paramLabel, boolean mask) {
String mapFrom, String paramLabel, boolean mask, BiConsumer<PropertyMapper<T>, ConfigValue> validator) {
this.option = option;
this.to = to == null ? getFrom() : to;
this.mapper = mapper == null ? PropertyMapper::defaultTransformer : mapper;
@ -75,6 +81,7 @@ public class PropertyMapper<T> {
this.mask = mask;
this.cliFormat = toCliFormat(option.getKey());
this.envVarFormat = toEnvVarFormat(getFrom());
this.validator = validator;
}
private static Optional<String> defaultTransformer(Optional<String> value, ConfigSourceInterceptorContext context) {
@ -235,6 +242,7 @@ public class PropertyMapper<T> {
private String mapFrom = null;
private boolean isMasked = false;
private String paramLabel;
private BiConsumer<PropertyMapper<T>, ConfigValue> validator = (mapper, value) -> mapper.validateExpectedValues(value);
public Builder(Option<T> option) {
this.option = option;
@ -265,11 +273,16 @@ public class PropertyMapper<T> {
return this;
}
public Builder<T> validator(BiConsumer<PropertyMapper<T>, ConfigValue> validator) {
this.validator = validator;
return this;
}
public PropertyMapper<T> build() {
if (paramLabel == null && Boolean.class.equals(option.getType())) {
paramLabel = Boolean.TRUE + "|" + Boolean.FALSE;
}
return new PropertyMapper<T>(option, to, mapper, mapFrom, paramLabel, isMasked);
return new PropertyMapper<T>(option, to, mapper, mapFrom, paramLabel, isMasked, validator);
}
}
@ -277,4 +290,21 @@ public class PropertyMapper<T> {
return new PropertyMapper.Builder<>(opt);
}
public void validate(ConfigValue value) {
if (validator != null) {
validator.accept(this, value);
}
}
public void validateExpectedValues(ConfigValue value) {
if (PropertyMapperParameterConsumer.isExpectedValue(getExpectedValues(), value.getValue())) {
return;
}
boolean cli = Optional.ofNullable(value.getConfigSourceName()).filter(name -> name.contains(ConfigArgsConfigSource.NAME)).isPresent();
throw new PropertyException(
PropertyMapperParameterConsumer.getErrorMessage(cli ? this.getCliFormat() : getFrom(),
value.getValue(), getExpectedValues(), getExpectedValues())
+ (cli ? "" : ". From ConfigSource " + value.getConfigSourceName()));
}
}

View file

@ -0,0 +1,55 @@
/*
* Copyright 2023 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.test;
import org.junit.Test;
import org.keycloak.common.Profile.Feature;
import org.keycloak.quarkus.runtime.cli.PropertyException;
import org.keycloak.quarkus.runtime.configuration.mappers.FeaturePropertyMappers;
import static org.junit.Assert.assertThrows;
public class FeaturePropertyMappersTest {
@Test
public void testInvalidFeatureFormat() {
assertThrows(PropertyException.class, () -> FeaturePropertyMappers.validateEnabledFeatures("invalid:"));
}
@Test
public void testInvalidFeature() {
assertThrows(PropertyException.class, () -> FeaturePropertyMappers.validateEnabledFeatures("invalid"));
}
@Test
public void testInvalidVersionedFeature() {
assertThrows(PropertyException.class, () -> FeaturePropertyMappers.validateEnabledFeatures("invalid:v1"));
}
@Test
public void testInvalidFeatureVersion() {
assertThrows(PropertyException.class, () -> FeaturePropertyMappers.validateEnabledFeatures(Feature.DOCKER.getUnversionedKey() + ":v0"));
}
@Test
public void testValidFeatures() {
FeaturePropertyMappers.validateEnabledFeatures(
Feature.DOCKER.getUnversionedKey() + "," + "preview" + "," + Feature.ACCOUNT2.getVersionedKey());
}
}

View file

@ -28,7 +28,7 @@ import static org.keycloak.quarkus.runtime.cli.command.AbstractStartCommand.OPTI
@LegacyStore
public class FeaturesDistTest {
private static final String PREVIEW_FEATURES_EXPECTED_LOG = "Preview features enabled: account3, admin-fine-grained-authz, client-secret-rotation, declarative-user-profile, dpop, multi-site, recovery-codes, scripts, token-exchange, update-email";
private static final String PREVIEW_FEATURES_EXPECTED_LOG = "Preview features enabled: account3:v1, admin-fine-grained-authz:v1, client-secret-rotation:v1, declarative-user-profile:v1, dpop:v1, multi-site:v1, recovery-codes:v1, scripts:v1, token-exchange:v1, update-email:v1";
@Test
public void testEnableOnBuild(KeycloakDistribution dist) {
@ -69,10 +69,16 @@ public class FeaturesDistTest {
@Test
@Launch({StartDev.NAME, "--features=token-exchange", "--features-disabled=token-exchange"})
public void testEnablePrecedenceOverDisable(LaunchResult result) {
public void testEnableDisableConflict(LaunchResult result) {
CLIResult cliResult = (CLIResult) result;
cliResult.assertStartedDevMode();
assertThat(cliResult.getOutput(), containsString("Preview features enabled: token-exchange"));
cliResult.assertError("token-exchange is in both the enabled and disabled feature lists");
}
@Test
@Launch({StartDev.NAME, "--features=token-exchange:v1", "--features-disabled=token-exchange"})
public void testEnableDisableConflictUsingVersioned(LaunchResult result) {
CLIResult cliResult = (CLIResult) result;
cliResult.assertError("Versioned feature token-exchange:v1 is not expected as token-exchange is already disabled");
}
@Test
@ -82,7 +88,7 @@ public class FeaturesDistTest {
CLIResult cliResult = (CLIResult) result;
cliResult.assertStartedDevMode();
assertThat(cliResult.getOutput(), CoreMatchers.allOf(
containsString("Preview features enabled: admin-fine-grained-authz, token-exchange")));
containsString("Preview features enabled: admin-fine-grained-authz:v1, token-exchange:v1")));
assertFalse(cliResult.getOutput().contains("declarative-user-profile"));
}
@ -93,7 +99,7 @@ public class FeaturesDistTest {
CLIResult cliResult = (CLIResult) result;
cliResult.assertStartedDevMode();
assertThat(cliResult.getOutput(), CoreMatchers.allOf(
containsString("Preview features enabled: admin-fine-grained-authz, token-exchange")));
containsString("Preview features enabled: admin-fine-grained-authz:v1, token-exchange:v1")));
assertFalse(cliResult.getOutput().contains("declarative-user-profile"));
}

View file

@ -39,7 +39,7 @@ public class FipsDistTest {
CLIResult cliResult = dist.run("start");
cliResult.assertStarted();
// Not shown as FIPS is not a preview anymore
cliResult.assertMessageWasShownExactlyNumberOfTimes("Preview features enabled: fips", 0);
cliResult.assertMessageWasShownExactlyNumberOfTimes("Preview features enabled: fips:v1", 0);
cliResult.assertMessage("Java security providers: [ \n"
+ " KC(BCFIPS version 1.000203, FIPS-JVM: " + KeycloakFipsSecurityProvider.isSystemFipsEnabled() + ") version 1.0 - class org.keycloak.crypto.fips.KeycloakFipsSecurityProvider");
});

View file

@ -23,6 +23,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.keycloak.quarkus.runtime.cli.command.AbstractStartCommand.OPTIMIZED_BUILD_OPTION_LONG;
import java.util.Optional;
import java.util.function.Consumer;
import org.junit.jupiter.api.Disabled;
@ -131,7 +132,12 @@ public class QuarkusPropertiesDistTest {
@Order(9)
void testMissingSmallRyeKeyStorePasswordProperty(LaunchResult result) {
CLIResult cliResult = (CLIResult) result;
cliResult.assertError("config-keystore-password must be specified");
assertTrue(
Optional.of(cliResult.getErrorOutput())
.filter(s -> s.contains("config-keystore-password must be specified")
|| s.contains("is required but it could not be found in any config source"))
.isPresent(),
() -> "The Error Output:\n " + cliResult.getErrorOutput() + " doesn't warn about the missing password");
}
@Disabled("Ensuring config-keystore is used only at runtime removes proactive validation of the path when only the keystore is used")

View file

@ -45,13 +45,15 @@ Transaction:
Feature:
--features <feature> Enables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
authorization, ciba, client-policies, client-secret-rotation,
declarative-user-profile, device-flow, docker, dpop, dynamic-scopes, fips,
impersonation, js-adapter, kerberos, linkedin-oauth, multi-site, par,
preview, recovery-codes, scripts, step-up-authentication, token-exchange,
transient-users, update-email, web-authn.
--features <feature> Enables a set of one or more features. Possible values are: account-api[:v1],
account2[:v1], account3[:v1], admin-api[:v1], admin-fine-grained-authz[:v1],
admin2[:v1], authorization[:v1], ciba[:v1], client-policies[:v1],
client-secret-rotation[:v1], declarative-user-profile[:v1], device-flow[:
v1], docker[:v1], dpop[:v1], dynamic-scopes[:v1], fips[:v1], impersonation[:
v1], js-adapter[:v1], kerberos[:v1], linkedin-oauth[:v1], multi-site[:v1],
par[:v1], preview, recovery-codes[:v1], scripts[:v1], step-up-authentication
[:v1], token-exchange[:v1], transient-users[:v1], update-email[:v1],
web-authn[:v1].
--features-disabled <feature>
Disables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,

View file

@ -45,13 +45,15 @@ Transaction:
Feature:
--features <feature> Enables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
authorization, ciba, client-policies, client-secret-rotation,
declarative-user-profile, device-flow, docker, dpop, dynamic-scopes, fips,
impersonation, js-adapter, kerberos, linkedin-oauth, multi-site, par,
preview, recovery-codes, scripts, step-up-authentication, token-exchange,
transient-users, update-email, web-authn.
--features <feature> Enables a set of one or more features. Possible values are: account-api[:v1],
account2[:v1], account3[:v1], admin-api[:v1], admin-fine-grained-authz[:v1],
admin2[:v1], authorization[:v1], ciba[:v1], client-policies[:v1],
client-secret-rotation[:v1], declarative-user-profile[:v1], device-flow[:
v1], docker[:v1], dpop[:v1], dynamic-scopes[:v1], fips[:v1], impersonation[:
v1], js-adapter[:v1], kerberos[:v1], linkedin-oauth[:v1], multi-site[:v1],
par[:v1], preview, recovery-codes[:v1], scripts[:v1], step-up-authentication
[:v1], token-exchange[:v1], transient-users[:v1], update-email[:v1],
web-authn[:v1].
--features-disabled <feature>
Disables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,

View file

@ -56,13 +56,15 @@ Transaction:
Feature:
--features <feature> Enables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
authorization, ciba, client-policies, client-secret-rotation,
declarative-user-profile, device-flow, docker, dpop, dynamic-scopes, fips,
impersonation, js-adapter, kerberos, linkedin-oauth, multi-site, par,
preview, recovery-codes, scripts, step-up-authentication, token-exchange,
transient-users, update-email, web-authn.
--features <feature> Enables a set of one or more features. Possible values are: account-api[:v1],
account2[:v1], account3[:v1], admin-api[:v1], admin-fine-grained-authz[:v1],
admin2[:v1], authorization[:v1], ciba[:v1], client-policies[:v1],
client-secret-rotation[:v1], declarative-user-profile[:v1], device-flow[:
v1], docker[:v1], dpop[:v1], dynamic-scopes[:v1], fips[:v1], impersonation[:
v1], js-adapter[:v1], kerberos[:v1], linkedin-oauth[:v1], multi-site[:v1],
par[:v1], preview, recovery-codes[:v1], scripts[:v1], step-up-authentication
[:v1], token-exchange[:v1], transient-users[:v1], update-email[:v1],
web-authn[:v1].
--features-disabled <feature>
Disables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
@ -152,4 +154,4 @@ Export:
--users-per-file <number>
Set the number of users per file. It is used only if 'users' is set to
'different_files'. Increasing this number leads to exponentially increasing
export times. Default: 50.
export times. Default: 50.

View file

@ -56,13 +56,15 @@ Transaction:
Feature:
--features <feature> Enables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
authorization, ciba, client-policies, client-secret-rotation,
declarative-user-profile, device-flow, docker, dpop, dynamic-scopes, fips,
impersonation, js-adapter, kerberos, linkedin-oauth, multi-site, par,
preview, recovery-codes, scripts, step-up-authentication, token-exchange,
transient-users, update-email, web-authn.
--features <feature> Enables a set of one or more features. Possible values are: account-api[:v1],
account2[:v1], account3[:v1], admin-api[:v1], admin-fine-grained-authz[:v1],
admin2[:v1], authorization[:v1], ciba[:v1], client-policies[:v1],
client-secret-rotation[:v1], declarative-user-profile[:v1], device-flow[:
v1], docker[:v1], dpop[:v1], dynamic-scopes[:v1], fips[:v1], impersonation[:
v1], js-adapter[:v1], kerberos[:v1], linkedin-oauth[:v1], multi-site[:v1],
par[:v1], preview, recovery-codes[:v1], scripts[:v1], step-up-authentication
[:v1], token-exchange[:v1], transient-users[:v1], update-email[:v1],
web-authn[:v1].
--features-disabled <feature>
Disables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
@ -152,4 +154,4 @@ Export:
--users-per-file <number>
Set the number of users per file. It is used only if 'users' is set to
'different_files'. Increasing this number leads to exponentially increasing
export times. Default: 50.
export times. Default: 50.

View file

@ -56,13 +56,15 @@ Transaction:
Feature:
--features <feature> Enables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
authorization, ciba, client-policies, client-secret-rotation,
declarative-user-profile, device-flow, docker, dpop, dynamic-scopes, fips,
impersonation, js-adapter, kerberos, linkedin-oauth, multi-site, par,
preview, recovery-codes, scripts, step-up-authentication, token-exchange,
transient-users, update-email, web-authn.
--features <feature> Enables a set of one or more features. Possible values are: account-api[:v1],
account2[:v1], account3[:v1], admin-api[:v1], admin-fine-grained-authz[:v1],
admin2[:v1], authorization[:v1], ciba[:v1], client-policies[:v1],
client-secret-rotation[:v1], declarative-user-profile[:v1], device-flow[:
v1], docker[:v1], dpop[:v1], dynamic-scopes[:v1], fips[:v1], impersonation[:
v1], js-adapter[:v1], kerberos[:v1], linkedin-oauth[:v1], multi-site[:v1],
par[:v1], preview, recovery-codes[:v1], scripts[:v1], step-up-authentication
[:v1], token-exchange[:v1], transient-users[:v1], update-email[:v1],
web-authn[:v1].
--features-disabled <feature>
Disables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
@ -146,4 +148,4 @@ Import:
--file <file> Set the path to a file that will be read.
--override <true|false>
Set if existing data should be overwritten. If set to false, data will be
ignored. Default: true.
ignored. Default: true.

View file

@ -56,13 +56,15 @@ Transaction:
Feature:
--features <feature> Enables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
authorization, ciba, client-policies, client-secret-rotation,
declarative-user-profile, device-flow, docker, dpop, dynamic-scopes, fips,
impersonation, js-adapter, kerberos, linkedin-oauth, multi-site, par,
preview, recovery-codes, scripts, step-up-authentication, token-exchange,
transient-users, update-email, web-authn.
--features <feature> Enables a set of one or more features. Possible values are: account-api[:v1],
account2[:v1], account3[:v1], admin-api[:v1], admin-fine-grained-authz[:v1],
admin2[:v1], authorization[:v1], ciba[:v1], client-policies[:v1],
client-secret-rotation[:v1], declarative-user-profile[:v1], device-flow[:
v1], docker[:v1], dpop[:v1], dynamic-scopes[:v1], fips[:v1], impersonation[:
v1], js-adapter[:v1], kerberos[:v1], linkedin-oauth[:v1], multi-site[:v1],
par[:v1], preview, recovery-codes[:v1], scripts[:v1], step-up-authentication
[:v1], token-exchange[:v1], transient-users[:v1], update-email[:v1],
web-authn[:v1].
--features-disabled <feature>
Disables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
@ -146,4 +148,4 @@ Import:
--file <file> Set the path to a file that will be read.
--override <true|false>
Set if existing data should be overwritten. If set to false, data will be
ignored. Default: true.
ignored. Default: true.

View file

@ -72,13 +72,15 @@ Transaction:
Feature:
--features <feature> Enables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
authorization, ciba, client-policies, client-secret-rotation,
declarative-user-profile, device-flow, docker, dpop, dynamic-scopes, fips,
impersonation, js-adapter, kerberos, linkedin-oauth, multi-site, par,
preview, recovery-codes, scripts, step-up-authentication, token-exchange,
transient-users, update-email, web-authn.
--features <feature> Enables a set of one or more features. Possible values are: account-api[:v1],
account2[:v1], account3[:v1], admin-api[:v1], admin-fine-grained-authz[:v1],
admin2[:v1], authorization[:v1], ciba[:v1], client-policies[:v1],
client-secret-rotation[:v1], declarative-user-profile[:v1], device-flow[:
v1], docker[:v1], dpop[:v1], dynamic-scopes[:v1], fips[:v1], impersonation[:
v1], js-adapter[:v1], kerberos[:v1], linkedin-oauth[:v1], multi-site[:v1],
par[:v1], preview, recovery-codes[:v1], scripts[:v1], step-up-authentication
[:v1], token-exchange[:v1], transient-users[:v1], update-email[:v1],
web-authn[:v1].
--features-disabled <feature>
Disables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,

View file

@ -57,8 +57,10 @@ Database:
--db-url-port <port> Sets the port of the default JDBC URL of the chosen vendor. If the `db-url`
option is set, this option is ignored.
--db-url-properties <properties>
Sets the properties of the default JDBC URL of the chosen vendor. If the
`db-url` option is set, this option is ignored.
Sets the properties of the default JDBC URL of the chosen vendor. Make sure to
set the properties accordingly to the format expected by the database
vendor, as well as appending the right character at the beginning of this
property value. If the `db-url` option is set, this option is ignored.
--db-username <username>
The username of the database user.
@ -70,13 +72,15 @@ Transaction:
Feature:
--features <feature> Enables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
authorization, ciba, client-policies, client-secret-rotation,
declarative-user-profile, device-flow, docker, dpop, dynamic-scopes, fips,
impersonation, js-adapter, kerberos, linkedin-oauth, multi-site, par,
preview, recovery-codes, scripts, step-up-authentication, token-exchange,
transient-users, update-email, web-authn.
--features <feature> Enables a set of one or more features. Possible values are: account-api[:v1],
account2[:v1], account3[:v1], admin-api[:v1], admin-fine-grained-authz[:v1],
admin2[:v1], authorization[:v1], ciba[:v1], client-policies[:v1],
client-secret-rotation[:v1], declarative-user-profile[:v1], device-flow[:
v1], docker[:v1], dpop[:v1], dynamic-scopes[:v1], fips[:v1], impersonation[:
v1], js-adapter[:v1], kerberos[:v1], linkedin-oauth[:v1], multi-site[:v1],
par[:v1], preview, recovery-codes[:v1], scripts[:v1], step-up-authentication
[:v1], token-exchange[:v1], transient-users[:v1], update-email[:v1],
web-authn[:v1].
--features-disabled <feature>
Disables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
@ -97,6 +101,9 @@ Hostname:
--hostname-admin-url <url>
Set the base URL for accessing the administration console, including scheme,
host, port and path
--hostname-debug <true|false>
Toggle the hostname debug page that is accessible at
/realms/master/hostname-debug Default: false.
--hostname-path <path>
This should be set if proxy uses a different context-path for Keycloak.
--hostname-port <port>
@ -165,6 +172,15 @@ Health:
are available at the '/health', '/health/ready' and '/health/live'
endpoints. Default: false.
Config:
--config-keystore <config-keystore>
Specifies a path to the KeyStore Configuration Source.
--config-keystore-password <config-keystore-password>
Specifies a password to the KeyStore Configuration Source.
--config-keystore-type <config-keystore-type>
Specifies a type of the KeyStore Configuration Source. Default: PKCS12.
Metrics:
--metrics-enabled <true|false>
@ -183,9 +199,12 @@ Proxy:
Vault:
--vault <provider> Enables a vault provider. Possible values are: file.
--vault <provider> Enables a vault provider. Possible values are: file, keystore.
--vault-dir <dir> If set, secrets can be obtained by reading the content of files within the
given directory.
--vault-file <file> Path to the keystore file.
--vault-pass <pass> Password for the vault keystore.
--vault-type <type> Specifies the type of the keystore file. Default: PKCS12.
Logging:
@ -257,5 +276,5 @@ Security:
Do NOT start the server using this command when deploying to production.
Use 'kc.bat start-dev --help-all' to list all available options, including
Use 'kc.bat start-dev --help-all' to list all available options, including
build options.

View file

@ -72,13 +72,15 @@ Transaction:
Feature:
--features <feature> Enables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
authorization, ciba, client-policies, client-secret-rotation,
declarative-user-profile, device-flow, docker, dpop, dynamic-scopes, fips,
impersonation, js-adapter, kerberos, linkedin-oauth, multi-site, par,
preview, recovery-codes, scripts, step-up-authentication, token-exchange,
transient-users, update-email, web-authn.
--features <feature> Enables a set of one or more features. Possible values are: account-api[:v1],
account2[:v1], account3[:v1], admin-api[:v1], admin-fine-grained-authz[:v1],
admin2[:v1], authorization[:v1], ciba[:v1], client-policies[:v1],
client-secret-rotation[:v1], declarative-user-profile[:v1], device-flow[:
v1], docker[:v1], dpop[:v1], dynamic-scopes[:v1], fips[:v1], impersonation[:
v1], js-adapter[:v1], kerberos[:v1], linkedin-oauth[:v1], multi-site[:v1],
par[:v1], preview, recovery-codes[:v1], scripts[:v1], step-up-authentication
[:v1], token-exchange[:v1], transient-users[:v1], update-email[:v1],
web-authn[:v1].
--features-disabled <feature>
Disables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,

View file

@ -57,8 +57,10 @@ Database:
--db-url-port <port> Sets the port of the default JDBC URL of the chosen vendor. If the `db-url`
option is set, this option is ignored.
--db-url-properties <properties>
Sets the properties of the default JDBC URL of the chosen vendor. If the
`db-url` option is set, this option is ignored.
Sets the properties of the default JDBC URL of the chosen vendor. Make sure to
set the properties accordingly to the format expected by the database
vendor, as well as appending the right character at the beginning of this
property value. If the `db-url` option is set, this option is ignored.
--db-username <username>
The username of the database user.
@ -70,13 +72,15 @@ Transaction:
Feature:
--features <feature> Enables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
authorization, ciba, client-policies, client-secret-rotation,
declarative-user-profile, device-flow, docker, dpop, dynamic-scopes, fips,
impersonation, js-adapter, kerberos, linkedin-oauth, multi-site, par,
preview, recovery-codes, scripts, step-up-authentication, token-exchange,
transient-users, update-email, web-authn.
--features <feature> Enables a set of one or more features. Possible values are: account-api[:v1],
account2[:v1], account3[:v1], admin-api[:v1], admin-fine-grained-authz[:v1],
admin2[:v1], authorization[:v1], ciba[:v1], client-policies[:v1],
client-secret-rotation[:v1], declarative-user-profile[:v1], device-flow[:
v1], docker[:v1], dpop[:v1], dynamic-scopes[:v1], fips[:v1], impersonation[:
v1], js-adapter[:v1], kerberos[:v1], linkedin-oauth[:v1], multi-site[:v1],
par[:v1], preview, recovery-codes[:v1], scripts[:v1], step-up-authentication
[:v1], token-exchange[:v1], transient-users[:v1], update-email[:v1],
web-authn[:v1].
--features-disabled <feature>
Disables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
@ -97,6 +101,9 @@ Hostname:
--hostname-admin-url <url>
Set the base URL for accessing the administration console, including scheme,
host, port and path
--hostname-debug <true|false>
Toggle the hostname debug page that is accessible at
/realms/master/hostname-debug Default: false.
--hostname-path <path>
This should be set if proxy uses a different context-path for Keycloak.
--hostname-port <port>
@ -165,6 +172,15 @@ Health:
are available at the '/health', '/health/ready' and '/health/live'
endpoints. Default: false.
Config:
--config-keystore <config-keystore>
Specifies a path to the KeyStore Configuration Source.
--config-keystore-password <config-keystore-password>
Specifies a password to the KeyStore Configuration Source.
--config-keystore-type <config-keystore-type>
Specifies a type of the KeyStore Configuration Source. Default: PKCS12.
Metrics:
--metrics-enabled <true|false>
@ -183,9 +199,12 @@ Proxy:
Vault:
--vault <provider> Enables a vault provider. Possible values are: file.
--vault <provider> Enables a vault provider. Possible values are: file, keystore.
--vault-dir <dir> If set, secrets can be obtained by reading the content of files within the
given directory.
--vault-file <file> Path to the keystore file.
--vault-pass <pass> Password for the vault keystore.
--vault-type <type> Specifies the type of the keystore file. Default: PKCS12.
Logging:

View file

@ -73,13 +73,15 @@ Transaction:
Feature:
--features <feature> Enables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
authorization, ciba, client-policies, client-secret-rotation,
declarative-user-profile, device-flow, docker, dpop, dynamic-scopes, fips,
impersonation, js-adapter, kerberos, linkedin-oauth, multi-site, par,
preview, recovery-codes, scripts, step-up-authentication, token-exchange,
transient-users, update-email, web-authn.
--features <feature> Enables a set of one or more features. Possible values are: account-api[:v1],
account2[:v1], account3[:v1], admin-api[:v1], admin-fine-grained-authz[:v1],
admin2[:v1], authorization[:v1], ciba[:v1], client-policies[:v1],
client-secret-rotation[:v1], declarative-user-profile[:v1], device-flow[:
v1], docker[:v1], dpop[:v1], dynamic-scopes[:v1], fips[:v1], impersonation[:
v1], js-adapter[:v1], kerberos[:v1], linkedin-oauth[:v1], multi-site[:v1],
par[:v1], preview, recovery-codes[:v1], scripts[:v1], step-up-authentication
[:v1], token-exchange[:v1], transient-users[:v1], update-email[:v1],
web-authn[:v1].
--features-disabled <feature>
Disables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,

View file

@ -58,8 +58,10 @@ Database:
--db-url-port <port> Sets the port of the default JDBC URL of the chosen vendor. If the `db-url`
option is set, this option is ignored.
--db-url-properties <properties>
Sets the properties of the default JDBC URL of the chosen vendor. If the
`db-url` option is set, this option is ignored.
Sets the properties of the default JDBC URL of the chosen vendor. Make sure to
set the properties accordingly to the format expected by the database
vendor, as well as appending the right character at the beginning of this
property value. If the `db-url` option is set, this option is ignored.
--db-username <username>
The username of the database user.
@ -71,13 +73,15 @@ Transaction:
Feature:
--features <feature> Enables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
authorization, ciba, client-policies, client-secret-rotation,
declarative-user-profile, device-flow, docker, dpop, dynamic-scopes, fips,
impersonation, js-adapter, kerberos, linkedin-oauth, multi-site, par,
preview, recovery-codes, scripts, step-up-authentication, token-exchange,
transient-users, update-email, web-authn.
--features <feature> Enables a set of one or more features. Possible values are: account-api[:v1],
account2[:v1], account3[:v1], admin-api[:v1], admin-fine-grained-authz[:v1],
admin2[:v1], authorization[:v1], ciba[:v1], client-policies[:v1],
client-secret-rotation[:v1], declarative-user-profile[:v1], device-flow[:
v1], docker[:v1], dpop[:v1], dynamic-scopes[:v1], fips[:v1], impersonation[:
v1], js-adapter[:v1], kerberos[:v1], linkedin-oauth[:v1], multi-site[:v1],
par[:v1], preview, recovery-codes[:v1], scripts[:v1], step-up-authentication
[:v1], token-exchange[:v1], transient-users[:v1], update-email[:v1],
web-authn[:v1].
--features-disabled <feature>
Disables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
@ -98,6 +102,9 @@ Hostname:
--hostname-admin-url <url>
Set the base URL for accessing the administration console, including scheme,
host, port and path
--hostname-debug <true|false>
Toggle the hostname debug page that is accessible at
/realms/master/hostname-debug Default: false.
--hostname-path <path>
This should be set if proxy uses a different context-path for Keycloak.
--hostname-port <port>
@ -166,6 +173,15 @@ Health:
are available at the '/health', '/health/ready' and '/health/live'
endpoints. Default: false.
Config:
--config-keystore <config-keystore>
Specifies a path to the KeyStore Configuration Source.
--config-keystore-password <config-keystore-password>
Specifies a password to the KeyStore Configuration Source.
--config-keystore-type <config-keystore-type>
Specifies a type of the KeyStore Configuration Source. Default: PKCS12.
Metrics:
--metrics-enabled <true|false>
@ -184,9 +200,12 @@ Proxy:
Vault:
--vault <provider> Enables a vault provider. Possible values are: file.
--vault <provider> Enables a vault provider. Possible values are: file, keystore.
--vault-dir <dir> If set, secrets can be obtained by reading the content of files within the
given directory.
--vault-file <file> Path to the keystore file.
--vault-pass <pass> Password for the vault keystore.
--vault-type <type> Specifies the type of the keystore file. Default: PKCS12.
Logging:

View file

@ -73,13 +73,15 @@ Transaction:
Feature:
--features <feature> Enables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
authorization, ciba, client-policies, client-secret-rotation,
declarative-user-profile, device-flow, docker, dpop, dynamic-scopes, fips,
impersonation, js-adapter, kerberos, linkedin-oauth, multi-site, par,
preview, recovery-codes, scripts, step-up-authentication, token-exchange,
transient-users, update-email, web-authn.
--features <feature> Enables a set of one or more features. Possible values are: account-api[:v1],
account2[:v1], account3[:v1], admin-api[:v1], admin-fine-grained-authz[:v1],
admin2[:v1], authorization[:v1], ciba[:v1], client-policies[:v1],
client-secret-rotation[:v1], declarative-user-profile[:v1], device-flow[:
v1], docker[:v1], dpop[:v1], dynamic-scopes[:v1], fips[:v1], impersonation[:
v1], js-adapter[:v1], kerberos[:v1], linkedin-oauth[:v1], multi-site[:v1],
par[:v1], preview, recovery-codes[:v1], scripts[:v1], step-up-authentication
[:v1], token-exchange[:v1], transient-users[:v1], update-email[:v1],
web-authn[:v1].
--features-disabled <feature>
Disables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,

View file

@ -58,8 +58,10 @@ Database:
--db-url-port <port> Sets the port of the default JDBC URL of the chosen vendor. If the `db-url`
option is set, this option is ignored.
--db-url-properties <properties>
Sets the properties of the default JDBC URL of the chosen vendor. If the
`db-url` option is set, this option is ignored.
Sets the properties of the default JDBC URL of the chosen vendor. Make sure to
set the properties accordingly to the format expected by the database
vendor, as well as appending the right character at the beginning of this
property value. If the `db-url` option is set, this option is ignored.
--db-username <username>
The username of the database user.
@ -71,13 +73,15 @@ Transaction:
Feature:
--features <feature> Enables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
authorization, ciba, client-policies, client-secret-rotation,
declarative-user-profile, device-flow, docker, dpop, dynamic-scopes, fips,
impersonation, js-adapter, kerberos, linkedin-oauth, multi-site, par,
preview, recovery-codes, scripts, step-up-authentication, token-exchange,
transient-users, update-email, web-authn.
--features <feature> Enables a set of one or more features. Possible values are: account-api[:v1],
account2[:v1], account3[:v1], admin-api[:v1], admin-fine-grained-authz[:v1],
admin2[:v1], authorization[:v1], ciba[:v1], client-policies[:v1],
client-secret-rotation[:v1], declarative-user-profile[:v1], device-flow[:
v1], docker[:v1], dpop[:v1], dynamic-scopes[:v1], fips[:v1], impersonation[:
v1], js-adapter[:v1], kerberos[:v1], linkedin-oauth[:v1], multi-site[:v1],
par[:v1], preview, recovery-codes[:v1], scripts[:v1], step-up-authentication
[:v1], token-exchange[:v1], transient-users[:v1], update-email[:v1],
web-authn[:v1].
--features-disabled <feature>
Disables a set of one or more features. Possible values are: account-api,
account2, account3, admin-api, admin-fine-grained-authz, admin2,
@ -98,6 +102,9 @@ Hostname:
--hostname-admin-url <url>
Set the base URL for accessing the administration console, including scheme,
host, port and path
--hostname-debug <true|false>
Toggle the hostname debug page that is accessible at
/realms/master/hostname-debug Default: false.
--hostname-path <path>
This should be set if proxy uses a different context-path for Keycloak.
--hostname-port <port>
@ -166,6 +173,15 @@ Health:
are available at the '/health', '/health/ready' and '/health/live'
endpoints. Default: false.
Config:
--config-keystore <config-keystore>
Specifies a path to the KeyStore Configuration Source.
--config-keystore-password <config-keystore-password>
Specifies a password to the KeyStore Configuration Source.
--config-keystore-type <config-keystore-type>
Specifies a type of the KeyStore Configuration Source. Default: PKCS12.
Metrics:
--metrics-enabled <true|false>
@ -184,9 +200,12 @@ Proxy:
Vault:
--vault <provider> Enables a vault provider. Possible values are: file.
--vault <provider> Enables a vault provider. Possible values are: file, keystore.
--vault-dir <dir> If set, secrets can be obtained by reading the content of files within the
given directory.
--vault-file <file> Path to the keystore file.
--vault-pass <pass> Password for the vault keystore.
--vault-type <type> Specifies the type of the keystore file. Default: PKCS12.
Logging:

View file

@ -24,7 +24,9 @@ import org.jboss.resteasy.reactive.NoCache;
import org.keycloak.http.HttpRequest;
import org.keycloak.Config;
import org.keycloak.common.Profile;
import org.keycloak.common.Profile.Feature;
import org.keycloak.common.enums.HostnameVerificationPolicy;
import org.keycloak.common.profile.PropertiesProfileConfigResolver;
import org.keycloak.common.util.HtmlUtils;
import org.keycloak.common.util.Time;
import org.keycloak.component.ComponentModel;
@ -115,6 +117,8 @@ import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
@ -869,13 +873,13 @@ public class TestingResourceProvider implements RealmResourceProvider {
private void setFeatureInProfileFile(File file, Profile.Feature featureProfile, String newState) {
doWithProperties(file, props -> {
props.setProperty("feature." + featureProfile.toString().toLowerCase(), newState);
props.setProperty(PropertiesProfileConfigResolver.getPropertyKey(featureProfile), newState);
});
}
private void unsetFeatureInProfileFile(File file, Profile.Feature featureProfile) {
doWithProperties(file, props -> {
props.remove("feature." + featureProfile.toString().toLowerCase());
props.remove(PropertiesProfileConfigResolver.getPropertyKey(featureProfile));
});
}
@ -951,36 +955,48 @@ public class TestingResourceProvider implements RealmResourceProvider {
}
private Set<Profile.Feature> updateFeature(String featureKey, boolean shouldEnable) {
Profile.Feature feature;
Collection<Profile.Feature> features = null;
try {
feature = Profile.Feature.valueOf(featureKey);
features = Arrays.asList(Profile.Feature.valueOf(featureKey));
} catch (IllegalArgumentException e) {
Set<Feature> featureVersions = Profile.getFeatureVersions(featureKey);
if (!shouldEnable) {
features = featureVersions;
} else if (!featureVersions.isEmpty()) {
// the set is ordered by preferred feature
features = Arrays.asList(featureVersions.iterator().next());
}
}
if (features == null || features.isEmpty()) {
System.err.printf("Feature '%s' doesn't exist!!\n", featureKey);
throw new BadRequestException();
}
if (Profile.getInstance().getFeatures().get(feature) != shouldEnable) {
FeatureDeployerUtil.initBeforeChangeFeature(feature);
for (Feature feature : features) {
if (Profile.getInstance().getFeatures().get(feature) != shouldEnable) {
FeatureDeployerUtil.initBeforeChangeFeature(feature);
String jbossServerConfigDir = System.getProperty("jboss.server.config.dir");
// If we are in jboss-based container, we need to write profile.properties file, otherwise the change in system property will disappear after restart
if (jbossServerConfigDir != null) {
setFeatureInProfileFile(new File(jbossServerConfigDir, "profile.properties"), feature, shouldEnable ? "enabled" : "disabled");
}
String jbossServerConfigDir = System.getProperty("jboss.server.config.dir");
// If we are in jboss-based container, we need to write profile.properties file, otherwise the change in system property will disappear after restart
if (jbossServerConfigDir != null) {
setFeatureInProfileFile(new File(jbossServerConfigDir, "profile.properties"), feature, shouldEnable ? "enabled" : "disabled");
}
Profile current = Profile.getInstance();
Profile current = Profile.getInstance();
Map<Profile.Feature, Boolean> updatedFeatures = new HashMap<>();
updatedFeatures.putAll(current.getFeatures());
updatedFeatures.put(feature, shouldEnable);
Map<Profile.Feature, Boolean> updatedFeatures = new HashMap<>();
updatedFeatures.putAll(current.getFeatures());
updatedFeatures.put(feature, shouldEnable);
Profile.init(current.getName(), updatedFeatures);
Profile.init(current.getName(), updatedFeatures);
if (shouldEnable) {
FeatureDeployerUtil.deployFactoriesAfterFeatureEnabled(feature);
} else {
FeatureDeployerUtil.undeployFactoriesAfterFeatureDisabled(feature);
if (shouldEnable) {
FeatureDeployerUtil.deployFactoriesAfterFeatureEnabled(feature);
} else {
FeatureDeployerUtil.undeployFactoriesAfterFeatureDisabled(feature);
}
}
}