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:
parent
15b10f58fc
commit
667ce4be9e
41 changed files with 805 additions and 417 deletions
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -6,7 +6,7 @@ public interface ProfileConfigResolver {
|
|||
|
||||
Profile.ProfileName getProfileName();
|
||||
|
||||
FeatureConfig getFeatureConfig(Profile.Feature feature);
|
||||
FeatureConfig getFeatureConfig(String feature);
|
||||
|
||||
public enum FeatureConfig {
|
||||
ENABLED,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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("-", "_");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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].
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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]));
|
||||
}
|
||||
|
||||
|
|
|
@ -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.");
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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())));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -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"));
|
||||
}
|
||||
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
|
@ -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.
|
|
@ -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,
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue