Refactor Profile

Closes #15206
This commit is contained in:
stianst 2022-10-28 12:09:35 +02:00 committed by Pedro Igor
parent 600fff4f6f
commit 1de9c201c6
49 changed files with 838 additions and 659 deletions

View file

@ -17,16 +17,20 @@
package org.keycloak.common;
import static org.keycloak.common.Profile.Type.DEPRECATED;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Properties;
import java.util.Set;
import org.jboss.logging.Logger;
import org.keycloak.common.profile.ProfileConfigResolver;
import org.keycloak.common.profile.ProfileException;
import org.keycloak.common.profile.PropertiesFileProfileConfigResolver;
import org.keycloak.common.profile.PropertiesProfileConfigResolver;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -34,167 +38,74 @@ import org.jboss.logging.Logger;
*/
public class Profile {
private static final Logger logger = Logger.getLogger(Profile.class);
private static Profile CURRENT;
private final ProfileValue profile;
private final Set<Feature> disabledFeatures = new HashSet<>();
private final Set<Feature> previewFeatures = new HashSet<>();
private final Set<Feature> experimentalFeatures = new HashSet<>();
private final Set<Feature> deprecatedFeatures = new HashSet<>();
private final PropertyResolver propertyResolver;
public Profile(PropertyResolver resolver) {
this.propertyResolver = resolver;
Config config = new Config();
profile = ProfileValue.valueOf(config.getProfile().toUpperCase());
for (Feature f : Feature.values()) {
Boolean enabled = config.getConfig(f);
Type type = f.getType();
switch (type) {
case DEFAULT:
if (enabled != null && !enabled) {
disabledFeatures.add(f);
}
break;
case DEPRECATED:
deprecatedFeatures.add(f);
case DISABLED_BY_DEFAULT:
if (enabled == null || !enabled) {
disabledFeatures.add(f);
} else if (DEPRECATED.equals(type)) {
logger.warnf("Deprecated feature enabled: " + f.name().toLowerCase());
}
break;
case PREVIEW:
previewFeatures.add(f);
if ((enabled == null || !enabled) && !profile.equals(ProfileValue.PREVIEW)) {
disabledFeatures.add(f);
} else {
logger.info("Preview feature enabled: " + f.name().toLowerCase());
}
break;
case EXPERIMENTAL:
experimentalFeatures.add(f);
if (enabled == null || !enabled) {
disabledFeatures.add(f);
} else {
logger.warn("Experimental feature enabled: " + f.name().toLowerCase());
}
break;
}
}
if ((!disabledFeatures.contains(Feature.ADMIN2) || !disabledFeatures.contains(Feature.ADMIN)) && disabledFeatures.contains(Feature.ADMIN_API)) {
throw new RuntimeException(String.format("Invalid value for feature: %s needs to be enabled because it is required by feature %s.",
Feature.ADMIN_API, Arrays.asList(Feature.ADMIN, Feature.ADMIN2)));
}
}
private static Profile getInstance() {
if (CURRENT == null) {
CURRENT = new Profile(null);
}
return CURRENT;
}
public static void setInstance(Profile instance) {
CURRENT = instance;
}
public static void init() {
PropertyResolver resolver = null;
if (CURRENT != null) {
resolver = CURRENT.propertyResolver;
}
CURRENT = new Profile(resolver);
}
public static String getName() {
return getInstance().profile.name().toLowerCase();
}
public static Set<Feature> getDisabledFeatures() {
return getInstance().disabledFeatures;
}
public static Set<Feature> getPreviewFeatures() {
return getInstance().previewFeatures;
}
public static Set<Feature> getExperimentalFeatures() {
return getInstance().experimentalFeatures;
}
public static Set<Feature> getDeprecatedFeatures() {
return getInstance().deprecatedFeatures;
}
public static boolean isFeatureEnabled(Feature feature) {
return !getInstance().disabledFeatures.contains(feature);
}
public enum Type {
DEFAULT,
DISABLED_BY_DEFAULT,
PREVIEW,
EXPERIMENTAL,
DEPRECATED;
}
public enum Feature {
AUTHORIZATION("Authorization Service", Type.DEFAULT),
ACCOUNT2("New Account Management Console", Type.DEFAULT),
ACCOUNT_API("Account Management REST API", Type.DEFAULT),
ACCOUNT2("New Account Management Console", Type.DEFAULT, Feature.ACCOUNT_API),
ADMIN_FINE_GRAINED_AUTHZ("Fine-Grained Admin Permissions", Type.PREVIEW),
/**
* Controls the availability of the Admin REST-API.
*/
ADMIN_API("Admin API", Type.DEFAULT),
/**
* Controls the availability of the legacy admin-console.
* Note that the admin-console requires the {@link #ADMIN_API} feature.
*/
@Deprecated
ADMIN("Legacy Admin Console", Type.DEPRECATED),
/**
* Controls the availability of the admin-console.
* Note that the admin-console requires the {@link #ADMIN_API} feature.
*/
ADMIN2("New Admin Console", Type.DEFAULT),
DOCKER("Docker Registry protocol", Type.DISABLED_BY_DEFAULT),
IMPERSONATION("Ability for admins to impersonate users", Type.DEFAULT),
OPENSHIFT_INTEGRATION("Extension to enable securing OpenShift", Type.PREVIEW),
SCRIPTS("Write custom authenticators using JavaScript", Type.PREVIEW),
TOKEN_EXCHANGE("Token Exchange Service", Type.PREVIEW),
WEB_AUTHN("W3C Web Authentication (WebAuthn)", Type.DEFAULT),
CLIENT_POLICIES("Client configuration policies", Type.DEFAULT),
CIBA("OpenID Connect Client Initiated Backchannel Authentication (CIBA)", Type.DEFAULT),
MAP_STORAGE("New store", Type.EXPERIMENTAL),
PAR("OAuth 2.0 Pushed Authorization Requests (PAR)", Type.DEFAULT),
DECLARATIVE_USER_PROFILE("Configure user profiles using a declarative style", Type.PREVIEW),
DYNAMIC_SCOPES("Dynamic OAuth 2.0 scopes", Type.EXPERIMENTAL),
CLIENT_SECRET_ROTATION("Client Secret Rotation", Type.PREVIEW),
STEP_UP_AUTHENTICATION("Step-up Authentication", Type.DEFAULT),
RECOVERY_CODES("Recovery codes", Type.PREVIEW),
UPDATE_EMAIL("Update Email Action", Type.PREVIEW),
JS_ADAPTER("Host keycloak.js and keycloak-authz.js through the Keycloak sever", Type.DEFAULT);
ADMIN2("New Admin Console", Type.DEFAULT, Feature.ADMIN_API),
DOCKER("Docker Registry protocol", Type.DISABLED_BY_DEFAULT),
IMPERSONATION("Ability for admins to impersonate users", Type.DEFAULT),
OPENSHIFT_INTEGRATION("Extension to enable securing OpenShift", Type.PREVIEW),
SCRIPTS("Write custom authenticators using JavaScript", Type.PREVIEW),
TOKEN_EXCHANGE("Token Exchange Service", Type.PREVIEW),
WEB_AUTHN("W3C Web Authentication (WebAuthn)", Type.DEFAULT),
CLIENT_POLICIES("Client configuration policies", Type.DEFAULT),
CIBA("OpenID Connect Client Initiated Backchannel Authentication (CIBA)", Type.DEFAULT),
MAP_STORAGE("New store", Type.EXPERIMENTAL),
PAR("OAuth 2.0 Pushed Authorization Requests (PAR)", Type.DEFAULT),
DECLARATIVE_USER_PROFILE("Configure user profiles using a declarative style", Type.PREVIEW),
DYNAMIC_SCOPES("Dynamic OAuth 2.0 scopes", Type.EXPERIMENTAL),
CLIENT_SECRET_ROTATION("Client Secret Rotation", Type.PREVIEW),
STEP_UP_AUTHENTICATION("Step-up Authentication", Type.DEFAULT),
RECOVERY_CODES("Recovery codes", Type.PREVIEW),
UPDATE_EMAIL("Update Email Action", Type.PREVIEW),
JS_ADAPTER("Host keycloak.js and keycloak-authz.js through the Keycloak sever", Type.DEFAULT);
private final Type type;
private String label;
private Set<Feature> dependencies;
Feature(String label, Type type) {
this.label = label;
this.type = type;
}
Feature(String label, Type type, Feature... dependencies) {
this.label = label;
this.type = type;
this.dependencies = Arrays.stream(dependencies).collect(Collectors.toSet());
}
public String getKey() {
return name().toLowerCase().replaceAll("_", "-");
}
public String getLabel() {
return label;
}
@ -202,88 +113,162 @@ public class Profile {
public Type getType() {
return type;
}
public Set<Feature> getDependencies() {
return dependencies;
}
public enum Type {
DEFAULT("Default"),
DISABLED_BY_DEFAULT("Disabled by default"),
PREVIEW("Preview"),
EXPERIMENTAL("Experimental"),
DEPRECATED("Deprecated");
private String label;
Type(String label) {
this.label = label;
}
public String getLabel() {
return label;
}
}
}
private enum ProfileValue {
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;
private final Map<Feature, Boolean> features;
public static Profile defaults() {
return configure();
}
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)));
verifyConfig(features);
CURRENT = new Profile(profile, features);
return CURRENT;
}
public static Profile init(ProfileName profileName, Map<Feature, Boolean> features) {
CURRENT = new Profile(profileName, features);
return CURRENT;
}
private Profile(ProfileName profileName, Map<Feature, Boolean> features) {
this.profileName = profileName;
this.features = Collections.unmodifiableMap(features);
logUnsupportedFeatures();
}
public static Profile getInstance() {
return CURRENT;
}
public static boolean isFeatureEnabled(Feature feature) {
return getInstance().features.get(feature);
}
public ProfileName getName() {
return profileName;
}
public Set<Feature> getAllFeatures() {
return features.keySet();
}
public Set<Feature> getDisabledFeatures() {
return features.entrySet().stream().filter(e -> !e.getValue()).map(Map.Entry::getKey).collect(Collectors.toSet());
}
public Set<Feature> getPreviewFeatures() {
return getFeatures(Feature.Type.PREVIEW);
}
public Set<Feature> getExperimentalFeatures() {
return getFeatures(Feature.Type.EXPERIMENTAL);
}
public Set<Feature> getDeprecatedFeatures() {
return getFeatures(Feature.Type.DEPRECATED);
}
public Set<Feature> getFeatures(Feature.Type type) {
return features.keySet().stream().filter(f -> f.getType().equals(type)).collect(Collectors.toSet());
}
public Map<Feature, Boolean> getFeatures() {
return features;
}
public enum ProfileName {
DEFAULT,
PREVIEW
}
public interface PropertyResolver {
String resolve(String feature);
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) ? true : false;
default:
return false;
}
}
}
private class Config {
private Properties properties;
public Config() {
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.load(is);
}
private static void verifyConfig(Map<Feature, Boolean> features) {
for (Feature f : features.keySet()) {
if (f.getDependencies() != null) {
for (Feature d : f.getDependencies()) {
if (!features.get(d)) {
throw new ProfileException("Feature " + f.getKey() + " depends on disabled feature " + d.getKey());
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public String getProfile() {
String profile = getProperty("keycloak.profile");
if (profile != null) {
return profile;
}
profile = properties.getProperty("profile");
if (profile != null) {
if (profile.equals("community")) {
profile = "default";
}
return profile;
}
return ProfileValue.DEFAULT.name();
}
public Boolean getConfig(Feature feature) {
String config = getProperty("keycloak.profile.feature." + feature.name().toLowerCase());
if (config == null) {
config = properties.getProperty("feature." + feature.name().toLowerCase());
}
if (config == null) {
return null;
} else if (config.equals("enabled")) {
return Boolean.TRUE;
} else if (config.equals("disabled")) {
return Boolean.FALSE;
} else {
throw new RuntimeException("Invalid value for feature " + config);
}
}
private String getProperty(String name) {
String value = System.getProperty(name);
if (value != null) {
return value;
}
if (propertyResolver != null) {
return propertyResolver.resolve(name);
}
return null;
}
}
}
private void logUnsupportedFeatures() {
logUnsuportedFeatures(Feature.Type.PREVIEW, Logger.Level.INFO);
logUnsuportedFeatures(Feature.Type.EXPERIMENTAL, Logger.Level.WARN);
logUnsuportedFeatures(Feature.Type.DEPRECATED, Logger.Level.WARN);
}
private void logUnsuportedFeatures(Feature.Type type, Logger.Level level) {
String enabledFeaturesOfType = features.entrySet().stream()
.filter(e -> e.getValue() && e.getKey().getType().equals(type))
.map(e -> e.getKey().getKey()).sorted().collect(Collectors.joining(", "));
if (!enabledFeaturesOfType.isEmpty()) {
logger.logv(level, "{0} features enabled: {1}", type.getLabel(), enabledFeaturesOfType);
}
}
}

View file

@ -0,0 +1,41 @@
package org.keycloak.common.profile;
import org.keycloak.common.Profile;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;
public class CommaSeparatedListProfileConfigResolver implements ProfileConfigResolver {
private Set<String> enabledFeatures;
private Set<String> disabledFeatures;
public CommaSeparatedListProfileConfigResolver(String enabledFeatures, String disabledFeatures) {
if (enabledFeatures != null) {
this.enabledFeatures = Arrays.stream(enabledFeatures.split(",")).collect(Collectors.toSet());
}
if (disabledFeatures != null) {
this.disabledFeatures = Arrays.stream(disabledFeatures.split(",")).collect(Collectors.toSet());
}
}
@Override
public Profile.ProfileName getProfileName() {
if (enabledFeatures != null && enabledFeatures.contains(Profile.ProfileName.PREVIEW.name().toLowerCase())) {
return Profile.ProfileName.PREVIEW;
}
return null;
}
@Override
public FeatureConfig getFeatureConfig(Profile.Feature feature) {
String key = feature.getKey();
if (enabledFeatures != null && enabledFeatures.contains(key)) {
return FeatureConfig.ENABLED;
} else if (disabledFeatures != null && disabledFeatures.contains(key)) {
return FeatureConfig.DISABLED;
}
return FeatureConfig.UNCONFIGURED;
}
}

View file

@ -0,0 +1,17 @@
package org.keycloak.common.profile;
import org.keycloak.common.Profile;
public interface ProfileConfigResolver {
Profile.ProfileName getProfileName();
FeatureConfig getFeatureConfig(Profile.Feature feature);
public enum FeatureConfig {
ENABLED,
DISABLED,
UNCONFIGURED
}
}

View file

@ -0,0 +1,12 @@
package org.keycloak.common.profile;
public class ProfileException extends RuntimeException {
public ProfileException(String message) {
super(message);
}
public ProfileException(String message, Throwable cause) {
super(message, cause);
}
}

View file

@ -0,0 +1,59 @@
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 PropertiesFileProfileConfigResolver() {
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);
}
}
}
} catch (IOException e) {
throw new ProfileException("Failed to load profile propeties file", e);
}
}
@Override
public Profile.ProfileName getProfileName() {
if (properties != null) {
String profile = properties.getProperty("profile");
if (profile != null) {
return Profile.ProfileName.valueOf(profile.toUpperCase());
}
}
return null;
}
@Override
public FeatureConfig getFeatureConfig(Profile.Feature feature) {
if (properties != null) {
String config = properties.getProperty("feature." + feature.name().toLowerCase());
if (config != null) {
switch (config) {
case "enabled":
return FeatureConfig.ENABLED;
case "disabled":
return FeatureConfig.DISABLED;
default:
throw new ProfileException("Invalid config value '" + config + "' for feature " + feature.getKey());
}
}
}
return FeatureConfig.UNCONFIGURED;
}
}

View file

@ -0,0 +1,36 @@
package org.keycloak.common.profile;
import org.keycloak.common.Profile;
import java.util.Properties;
public class PropertiesProfileConfigResolver implements ProfileConfigResolver {
private Properties properties;
public PropertiesProfileConfigResolver(Properties properties) {
this.properties = properties;
}
@Override
public Profile.ProfileName getProfileName() {
String profile = properties.getProperty("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());
if (config != null) {
switch (config) {
case "enabled":
return FeatureConfig.ENABLED;
case "disabled":
return FeatureConfig.DISABLED;
default:
throw new ProfileException("Invalid config value '" + config + "' for feature " + feature.getKey());
}
}
return FeatureConfig.UNCONFIGURED;
}
}

View file

@ -1,54 +1,198 @@
package org.keycloak.common;
import org.junit.After;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.keycloak.common.Profile.Feature;
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.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Properties;
import java.util.Set;
import static org.junit.Assert.assertTrue;
public class ProfileTest {
private static final Profile.Feature DEFAULT_FEATURE = Profile.Feature.AUTHORIZATION;
private static final Profile.Feature DISABLED_BY_DEFAULT_FEATURE = Profile.Feature.DOCKER;
private static final Profile.Feature PREVIEW_FEATURE = Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ;
private static final Profile.Feature EXPERIMENTAL_FEATURE = Profile.Feature.DYNAMIC_SCOPES;
private static final Profile.Feature DEPRECATED_FEATURE = Profile.Feature.ADMIN;
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void checkDefaults() {
Assert.assertEquals("default", Profile.getName());
assertEquals(Profile.getDisabledFeatures(), Profile.Feature.ADMIN, Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.DYNAMIC_SCOPES, Profile.Feature.DOCKER, Profile.Feature.RECOVERY_CODES, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.MAP_STORAGE, Profile.Feature.DECLARATIVE_USER_PROFILE, Feature.CLIENT_SECRET_ROTATION, Feature.UPDATE_EMAIL);
assertEquals(Profile.getPreviewFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.RECOVERY_CODES, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.DECLARATIVE_USER_PROFILE, Feature.CLIENT_SECRET_ROTATION, Feature.UPDATE_EMAIL);
@BeforeClass
public static void beforeClass() {
Assert.assertEquals(Profile.Feature.Type.DEFAULT, DEFAULT_FEATURE.getType());
Assert.assertEquals(Profile.Feature.Type.DISABLED_BY_DEFAULT, DISABLED_BY_DEFAULT_FEATURE.getType());
Assert.assertEquals(Profile.Feature.Type.PREVIEW, PREVIEW_FEATURE.getType());
Assert.assertEquals(Profile.Feature.Type.EXPERIMENTAL, EXPERIMENTAL_FEATURE.getType());
Assert.assertEquals(Profile.Feature.Type.DEPRECATED, DEPRECATED_FEATURE.getType());
}
@After
public void afterTest() {
Profile.defaults();
}
@Test
public void configWithSystemProperties() {
Assert.assertEquals("default", Profile.getName());
Assert.assertFalse(Profile.isFeatureEnabled(Profile.Feature.DOCKER));
Assert.assertFalse(Profile.isFeatureEnabled(Profile.Feature.OPENSHIFT_INTEGRATION));
assertTrue(Profile.isFeatureEnabled(Profile.Feature.IMPERSONATION));
public void checkDefaults() {
Profile profile = Profile.defaults();
System.setProperty("keycloak.profile", "preview");
System.setProperty("keycloak.profile.feature.docker", "enabled");
System.setProperty("keycloak.profile.feature.impersonation", "disabled");
System.setProperty("keycloak.profile.feature.upload_scripts", "enabled");
Assert.assertTrue(Profile.isFeatureEnabled(DEFAULT_FEATURE));
Assert.assertFalse(Profile.isFeatureEnabled(DISABLED_BY_DEFAULT_FEATURE));
Assert.assertFalse(Profile.isFeatureEnabled(PREVIEW_FEATURE));
Assert.assertFalse(Profile.isFeatureEnabled(EXPERIMENTAL_FEATURE));
Assert.assertFalse(Profile.isFeatureEnabled(DEPRECATED_FEATURE));
Profile.init();
Assert.assertEquals(Profile.ProfileName.DEFAULT, profile.getName());
assertEquals(profile.getDisabledFeatures(), Profile.Feature.ADMIN, Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.DYNAMIC_SCOPES, Profile.Feature.DOCKER, Profile.Feature.RECOVERY_CODES, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.MAP_STORAGE, Profile.Feature.DECLARATIVE_USER_PROFILE, Profile.Feature.CLIENT_SECRET_ROTATION, Profile.Feature.UPDATE_EMAIL);
assertEquals(profile.getPreviewFeatures(), Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.RECOVERY_CODES, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE, Profile.Feature.OPENSHIFT_INTEGRATION, Profile.Feature.DECLARATIVE_USER_PROFILE, Profile.Feature.CLIENT_SECRET_ROTATION, Profile.Feature.UPDATE_EMAIL);
}
Assert.assertEquals("preview", Profile.getName());
assertTrue(Profile.isFeatureEnabled(Profile.Feature.DOCKER));
assertTrue(Profile.isFeatureEnabled(Profile.Feature.OPENSHIFT_INTEGRATION));
Assert.assertFalse(Profile.isFeatureEnabled(Profile.Feature.IMPERSONATION));
@Test
public void checkFailureIfDependencyDisabled() {
Properties properties = new Properties();
properties.setProperty("keycloak.profile.feature.account_api", "disabled");
System.getProperties().remove("keycloak.profile");
System.getProperties().remove("keycloak.profile.feature.docker");
System.getProperties().remove("keycloak.profile.feature.impersonation");
System.getProperties().remove("keycloak.profile.feature.upload_scripts");
try {
Profile.configure(new PropertiesProfileConfigResolver(properties));
} catch (ProfileException e) {
Assert.assertEquals("Feature account2 depends on disabled feature account-api", e.getMessage());
}
}
Profile.init();
@Test
public void checkErrorOnBadConfig() {
Properties properties = new Properties();
properties.setProperty("keycloak.profile.feature.account_api", "invalid");
try {
Profile.configure(new PropertiesProfileConfigResolver(properties));
} catch (ProfileException e) {
Assert.assertEquals("Invalid config value 'invalid' for feature account-api", e.getMessage());
}
}
@Test
public void enablePreviewWithProperties() {
Properties properties = new Properties();
properties.setProperty("keycloak.profile", "preview");
Profile.configure(new PropertiesProfileConfigResolver(properties));
Assert.assertEquals(Profile.ProfileName.PREVIEW, Profile.getInstance().getName());
Assert.assertTrue(Profile.isFeatureEnabled(PREVIEW_FEATURE));
}
@Test
public void enablePreviewWithCommaSeparatedList() {
Profile.configure(new CommaSeparatedListProfileConfigResolver("preview", null));
Assert.assertEquals(Profile.ProfileName.PREVIEW, Profile.getInstance().getName());
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");
properties.store(new FileOutputStream(profileProperties.toFile()), "");
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() + "," + DEPRECATED_FEATURE.getKey();
String disabledFeatures = DEFAULT_FEATURE.getKey();
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));
Assert.assertTrue(Profile.isFeatureEnabled(DEPRECATED_FEATURE));
}
@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("keycloak.profile.feature." + DEPRECATED_FEATURE.name().toLowerCase(), "enabled");
Profile.configure(new PropertiesProfileConfigResolver(properties));
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));
Assert.assertTrue(Profile.isFeatureEnabled(DEPRECATED_FEATURE));
}
@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");
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");
properties.store(new FileOutputStream(profileProperties.toFile()), "");
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));
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");
Profile.configure(new CommaSeparatedListProfileConfigResolver(DISABLED_BY_DEFAULT_FEATURE.getKey(), ""), new PropertiesProfileConfigResolver(properties));
Assert.assertTrue(Profile.isFeatureEnabled(DISABLED_BY_DEFAULT_FEATURE));
Assert.assertTrue(Profile.isFeatureEnabled(PREVIEW_FEATURE));
}
public static void assertEquals(Set<Profile.Feature> actual, Profile.Feature... expected) {

View file

@ -22,6 +22,7 @@ import org.keycloak.common.Profile;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -36,10 +37,12 @@ public class ProfileInfoRepresentation {
public static ProfileInfoRepresentation create() {
ProfileInfoRepresentation info = new ProfileInfoRepresentation();
info.name = Profile.getName();
info.disabledFeatures = names(Profile.getDisabledFeatures());
info.previewFeatures = names(Profile.getPreviewFeatures());
info.experimentalFeatures = names(Profile.getExperimentalFeatures());
Profile profile = Profile.getInstance();
info.name = profile.getName().name().toLowerCase();
info.disabledFeatures = names(profile.getDisabledFeatures());
info.previewFeatures = names(profile.getPreviewFeatures());
info.experimentalFeatures = names(profile.getExperimentalFeatures());
return info;
}

View file

@ -13,26 +13,26 @@ public class Features {
public Features() {
this.features = Arrays.stream(Profile.Feature.values())
.filter(f -> !f.getType().equals(Profile.Type.EXPERIMENTAL))
.filter(f -> !f.getType().equals(Profile.Feature.Type.EXPERIMENTAL))
.map(f -> new Feature(f))
.sorted(Comparator.comparing(Feature::getName))
.collect(Collectors.toList());
}
public List<Feature> getSupported() {
return features.stream().filter(f -> f.getType().equals(Profile.Type.DEFAULT)).collect(Collectors.toList());
return features.stream().filter(f -> f.getType().equals(Profile.Feature.Type.DEFAULT)).collect(Collectors.toList());
}
public List<Feature> getSupportedDisabledByDefault() {
return features.stream().filter(f -> f.getType().equals(Profile.Type.DISABLED_BY_DEFAULT)).collect(Collectors.toList());
return features.stream().filter(f -> f.getType().equals(Profile.Feature.Type.DISABLED_BY_DEFAULT)).collect(Collectors.toList());
}
public List<Feature> getDeprecated() {
return features.stream().filter(f -> f.getType().equals(Profile.Type.DEPRECATED)).collect(Collectors.toList());
return features.stream().filter(f -> f.getType().equals(Profile.Feature.Type.DEPRECATED)).collect(Collectors.toList());
}
public List<Feature> getPreview() {
return features.stream().filter(f -> f.getType().equals(Profile.Type.PREVIEW)).collect(Collectors.toList());
return features.stream().filter(f -> f.getType().equals(Profile.Feature.Type.PREVIEW)).collect(Collectors.toList());
}
public class Feature {
@ -44,14 +44,14 @@ public class Features {
}
public String getName() {
return profileFeature.name().toLowerCase().replaceAll("_", "-");
return profileFeature.getKey();
}
public String getDescription() {
return profileFeature.getLabel();
}
private Profile.Type getType() {
private Profile.Feature.Type getType() {
return profileFeature.getType();
}

View file

@ -3,6 +3,7 @@ package org.keycloak.config;
import org.keycloak.common.Profile;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
@ -27,10 +28,12 @@ public class FeatureOptions {
List<String> features = new ArrayList<>();
for (Profile.Feature value : Profile.Feature.values()) {
features.add(value.name().toLowerCase().replace('_', '-'));
features.add(value.getKey());
}
features.add(Profile.Type.PREVIEW.name().toLowerCase());
features.add(Profile.Feature.Type.PREVIEW.name().toLowerCase());
Collections.sort(features);
return features;
}

View file

@ -0,0 +1,6 @@
package org.keycloak.quarkus.deployment;
import io.quarkus.builder.item.SimpleBuildItem;
public final class ConfigBuildItem extends SimpleBuildItem {
}

View file

@ -94,6 +94,8 @@ import org.jboss.resteasy.plugins.server.servlet.ResteasyContextParameters;
import org.jboss.resteasy.spi.ResteasyDeployment;
import org.keycloak.Config;
import org.keycloak.common.crypto.FipsMode;
import org.keycloak.common.profile.PropertiesFileProfileConfigResolver;
import org.keycloak.common.profile.PropertiesProfileConfigResolver;
import org.keycloak.config.SecurityOptions;
import org.keycloak.config.StorageOptions;
import org.keycloak.config.TransactionOptions;
@ -101,7 +103,7 @@ import org.keycloak.connections.jpa.JpaConnectionProvider;
import org.keycloak.connections.jpa.JpaConnectionSpi;
import org.keycloak.models.map.storage.jpa.JpaMapStorageProviderFactory;
import org.keycloak.protocol.saml.mappers.DeployedScriptSAMLProtocolMapper;
import org.keycloak.quarkus.runtime.QuarkusProfile;
import org.keycloak.quarkus.runtime.QuarkusProfileConfigResolver;
import org.keycloak.quarkus.runtime.configuration.PersistedConfigSource;
import org.keycloak.quarkus.runtime.configuration.QuarkusPropertiesConfigSource;
import org.keycloak.quarkus.runtime.configuration.mappers.PropertyMapper;
@ -207,6 +209,26 @@ class KeycloakProcessor {
return new FeatureBuildItem("keycloak");
}
@Record(ExecutionTime.STATIC_INIT)
@BuildStep
ConfigBuildItem initConfig(KeycloakRecorder recorder) {
Config.init(new MicroProfileConfigProvider());
recorder.initConfig();
return new ConfigBuildItem();
}
@Record(ExecutionTime.STATIC_INIT)
@BuildStep
@Consume(ConfigBuildItem.class)
ProfileBuildItem configureProfile(KeycloakRecorder recorder) {
Profile profile = Profile.configure(
new QuarkusProfileConfigResolver(),
new PropertiesFileProfileConfigResolver()); // Need profile.properties for now as testsuite relies on it
recorder.configureProfile(profile.getName(), profile.getFeatures());
return new ProfileBuildItem();
}
/**
* <p>Configures the persistence unit for Quarkus.
*
@ -331,8 +353,8 @@ class KeycloakProcessor {
*/
@Record(ExecutionTime.STATIC_INIT)
@BuildStep
@Consume(ProfileBuildItem.class)
KeycloakSessionFactoryPreInitBuildItem configureKeycloakSessionFactory(KeycloakRecorder recorder, List<PersistenceXmlDescriptorBuildItem> descriptors) {
Profile.setInstance(new QuarkusProfile());
Map<Spi, Map<Class<? extends Provider>, Map<String, Class<? extends ProviderFactory>>>> factories = new HashMap<>();
Map<Class<? extends Provider>, String> defaultProviders = new HashMap<>();
Map<String, ProviderFactory> preConfiguredProviders = new HashMap<>();

View file

@ -0,0 +1,6 @@
package org.keycloak.quarkus.deployment;
import io.quarkus.builder.item.SimpleBuildItem;
public final class ProfileBuildItem extends SimpleBuildItem {
}

View file

@ -64,6 +64,14 @@ public class KeycloakRecorder {
public static final String DEFAULT_HEALTH_ENDPOINT = "/health";
public static final String DEFAULT_METRICS_ENDPOINT = "/metrics";
public void initConfig() {
Config.init(new MicroProfileConfigProvider());
}
public void configureProfile(Profile.ProfileName profileName, Map<Profile.Feature, Boolean> features) {
Profile.init(profileName, features);
}
public void configureLiquibase(Map<String, List<String>> services) {
ServiceLocator locator = Scope.getCurrentScope().getServiceLocator();
if (locator instanceof FastServiceLocator)
@ -75,8 +83,6 @@ public class KeycloakRecorder {
Map<Class<? extends Provider>, String> defaultProviders,
Map<String, ProviderFactory> preConfiguredProviders,
List<ClasspathThemeProviderFactory.ThemesRepresentation> themes, boolean reaugmented) {
Config.init(new MicroProfileConfigProvider());
Profile.setInstance(new QuarkusProfile());
QuarkusKeycloakSessionFactory.setInstance(new QuarkusKeycloakSessionFactory(factories, defaultProviders, preConfiguredProviders, themes, reaugmented));
}

View file

@ -1,97 +0,0 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.quarkus.runtime;
import static org.keycloak.quarkus.runtime.configuration.Configuration.getRawPersistedProperty;
import org.keycloak.common.Profile;
import org.keycloak.quarkus.runtime.configuration.Configuration;
public class QuarkusProfile extends Profile {
public QuarkusProfile() {
super(new DefaultPropertyResolver());
}
private static class DefaultPropertyResolver implements PropertyResolver {
@Override
public String resolve(String key) {
if (isFeaturePresent(key, getCurrentValue("kc.features"))) {
if (isPreviewProfileKey(key)) {
return Profile.Type.PREVIEW.name();
}
return "enabled";
}
if (isFeaturePresent(key, getCurrentValue("kc.features-disabled"))) {
if (!isPreviewProfileKey(key)) {
return "disabled";
}
}
return null;
}
private boolean isFeaturePresent(String key, String features) {
if (features == null) {
return false;
}
for (String feature : features.split(",")) {
if (isPreviewProfileKey(key)) {
try {
Profile.Type profileType = Profile.Type.valueOf(feature);
if (Profile.Type.PREVIEW.equals(profileType)) {
return true;
}
} catch (IllegalArgumentException ignore) {
}
return false;
}
if (key.substring(key.lastIndexOf('.') + 1).toUpperCase().equals(feature)) {
return true;
}
}
return false;
}
private boolean isPreviewProfileKey(String key) {
return key.equals("keycloak.profile");
}
private String getCurrentValue(String name) {
String enabledFeatures = getRawPersistedProperty(name).orElse(null);
if (enabledFeatures == null) {
enabledFeatures = Configuration.getRawValue(name);
}
if (enabledFeatures == null) {
return null;
}
return enabledFeatures.toUpperCase().replace('-', '_');
}
}
}

View file

@ -0,0 +1,17 @@
package org.keycloak.quarkus.runtime;
import org.keycloak.common.profile.CommaSeparatedListProfileConfigResolver;
import org.keycloak.quarkus.runtime.configuration.Configuration;
public class QuarkusProfileConfigResolver extends CommaSeparatedListProfileConfigResolver {
public QuarkusProfileConfigResolver() {
super(getConfig("kc.features"), getConfig("kc.features-disabled"));
}
private static String getConfig(String key) {
return Configuration.getRawPersistedProperty(key)
.orElse(Configuration.getRawValue(key));
}
}

View file

@ -2,9 +2,7 @@ package org.keycloak.quarkus.runtime.configuration.mappers;
import org.keycloak.common.Profile;
import org.keycloak.config.FeatureOptions;
import org.keycloak.config.StorageOptions;
import org.keycloak.quarkus.runtime.configuration.Configuration;
import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider;
import static java.util.Optional.of;
import static org.keycloak.config.StorageOptions.STORAGE;
@ -42,7 +40,7 @@ final class FeaturePropertyMappers {
Set<String> featureSet = new HashSet<>(List.of(features.orElse("").split(",")));
featureSet.add(Profile.Feature.MAP_STORAGE.name().replace('_', '-'));
featureSet.add(Profile.Feature.MAP_STORAGE.getKey());
return of(String.join(",", featureSet));
}

View file

@ -52,10 +52,18 @@ public class FeaturesDistTest {
@Test
@Launch({StartDev.NAME, "--features=preview", "--features-disabled=token-exchange"})
public void testPreviewFeatureDisabledInPreviewMode(LaunchResult result) {
CLIResult cliResult = (CLIResult) result;
cliResult.assertStartedDevMode();
assertFalse(cliResult.getOutput().contains("token-exchange"));
}
@Test
@Launch({StartDev.NAME, "--features=token-exchange", "--features-disabled=token-exchange"})
public void testEnablePrecedenceOverDisable(LaunchResult result) {
CLIResult cliResult = (CLIResult) result;
cliResult.assertStartedDevMode();
assertPreviewFeaturesEnabled((CLIResult) result);
assertThat(cliResult.getOutput(), containsString("Preview features enabled: token-exchange"));
}
@Test
@ -65,8 +73,7 @@ public class FeaturesDistTest {
CLIResult cliResult = (CLIResult) result;
cliResult.assertStartedDevMode();
assertThat(cliResult.getOutput(), CoreMatchers.allOf(
containsString("Preview feature enabled: admin_fine_grained_authz"),
containsString("Preview feature enabled: token_exchange")));
containsString("Preview features enabled: admin-fine-grained-authz, token-exchange")));
assertFalse(cliResult.getOutput().contains("declarative-user-profile"));
}
@ -77,17 +84,12 @@ public class FeaturesDistTest {
CLIResult cliResult = (CLIResult) result;
cliResult.assertStartedDevMode();
assertThat(cliResult.getOutput(), CoreMatchers.allOf(
containsString("Preview feature enabled: admin_fine_grained_authz"),
containsString("Preview feature enabled: token_exchange")));
containsString("Preview features enabled: admin-fine-grained-authz, token-exchange")));
assertFalse(cliResult.getOutput().contains("declarative-user-profile"));
}
private void assertPreviewFeaturesEnabled(CLIResult result) {
assertThat(result.getOutput(), CoreMatchers.allOf(
containsString("Preview feature enabled: admin_fine_grained_authz"),
containsString("Preview feature enabled: openshift_integration"),
containsString("Preview feature enabled: scripts"),
containsString("Preview feature enabled: token_exchange"),
containsString("Preview feature enabled: declarative_user_profile")));
containsString("Preview features enabled: admin-fine-grained-authz, client-secret-rotation, declarative-user-profile, openshift-integration, recovery-codes, scripts, token-exchange, update-email")));
}
}

View file

@ -48,7 +48,7 @@ public class ChmStorageDistTest {
}
private void assertExpectedMessages(CLIResult cliResult, RawDistRootPath distPath) {
cliResult.assertMessage("Experimental feature enabled: map_storage");
cliResult.assertMessage("Experimental features enabled: map-storage");
cliResult.assertMessage("Hibernate ORM is disabled because no JPA entities were found");
Assert.assertFalse(distPath.getDistRootPath().resolve("data").resolve("h2").toFile().exists());
}

View file

@ -32,7 +32,7 @@ public class HotRodStoreDistTest {
@Launch({ "start", "--optimized", "--http-enabled=true", "--hostname-strict=false" })
void testSuccessful(LaunchResult result) {
CLIResult cliResult = (CLIResult) result;
cliResult.assertMessage("Experimental feature enabled: map_storage");
cliResult.assertMessage("Experimental features enabled: map-storage");
cliResult.assertMessage("[org.keycloak.models.map.storage.hotRod.connections.DefaultHotRodConnectionProviderFactory] (main) HotRod client configuration was successful.");
cliResult.assertStarted();
}

View file

@ -33,7 +33,7 @@ public class JPAStoreDistTest {
@Launch({ "start", "--optimized", "--http-enabled=true", "--hostname-strict=false" })
void testSuccessful(LaunchResult result) {
CLIResult cliResult = (CLIResult) result;
cliResult.assertMessage("Experimental feature enabled: map_storage");
cliResult.assertMessage("Experimental features enabled: map-storage");
cliResult.assertMessage("[org.keycloak.models.map.storage.jpa.liquibase.updater.MapJpaLiquibaseUpdaterProvider] (main) Initializing database schema. Using changelog META-INF/jpa-realms-changelog.xml");
cliResult.assertStarted();
}

View file

@ -43,19 +43,19 @@ Transaction:
Feature:
--features <feature> Enables a set of one or more features. Possible values are: authorization,
account2, account-api, admin-fine-grained-authz, admin-api, admin, admin2,
docker, impersonation, openshift-integration, scripts, token-exchange,
web-authn, client-policies, ciba, map-storage, par,
declarative-user-profile, dynamic-scopes, client-secret-rotation,
step-up-authentication, recovery-codes, update-email, js-adapter, preview.
--features <feature> Enables a set of one or more features. Possible values are: account-api,
account2, admin, admin-api, admin-fine-grained-authz, admin2, authorization,
ciba, client-policies, client-secret-rotation, declarative-user-profile,
docker, dynamic-scopes, impersonation, js-adapter, map-storage,
openshift-integration, par, preview, recovery-codes, scripts,
step-up-authentication, token-exchange, update-email, web-authn.
--features-disabled <feature>
Disables a set of one or more features. Possible values are: authorization,
account2, account-api, admin-fine-grained-authz, admin-api, admin, admin2,
docker, impersonation, openshift-integration, scripts, token-exchange,
web-authn, client-policies, ciba, map-storage, par,
declarative-user-profile, dynamic-scopes, client-secret-rotation,
step-up-authentication, recovery-codes, update-email, js-adapter, preview.
Disables a set of one or more features. Possible values are: account-api,
account2, admin, admin-api, admin-fine-grained-authz, admin2, authorization,
ciba, client-policies, client-secret-rotation, declarative-user-profile,
docker, dynamic-scopes, impersonation, js-adapter, map-storage,
openshift-integration, par, preview, recovery-codes, scripts,
step-up-authentication, token-exchange, update-email, web-authn.
HTTP/TLS:
@ -104,4 +104,4 @@ Examples:
Change the relative path:
$ kc.sh build --http-relative-path=/auth
$ kc.sh build --http-relative-path=/auth

View file

@ -66,19 +66,19 @@ Transaction:
Feature:
--features <feature> Enables a set of one or more features. Possible values are: authorization,
account2, account-api, admin-fine-grained-authz, admin-api, admin, admin2,
docker, impersonation, openshift-integration, scripts, token-exchange,
web-authn, client-policies, ciba, map-storage, par,
declarative-user-profile, dynamic-scopes, client-secret-rotation,
step-up-authentication, recovery-codes, update-email, js-adapter, preview.
--features <feature> Enables a set of one or more features. Possible values are: account-api,
account2, admin, admin-api, admin-fine-grained-authz, admin2, authorization,
ciba, client-policies, client-secret-rotation, declarative-user-profile,
docker, dynamic-scopes, impersonation, js-adapter, map-storage,
openshift-integration, par, preview, recovery-codes, scripts,
step-up-authentication, token-exchange, update-email, web-authn.
--features-disabled <feature>
Disables a set of one or more features. Possible values are: authorization,
account2, account-api, admin-fine-grained-authz, admin-api, admin, admin2,
docker, impersonation, openshift-integration, scripts, token-exchange,
web-authn, client-policies, ciba, map-storage, par,
declarative-user-profile, dynamic-scopes, client-secret-rotation,
step-up-authentication, recovery-codes, update-email, js-adapter, preview.
Disables a set of one or more features. Possible values are: account-api,
account2, admin, admin-api, admin-fine-grained-authz, admin2, authorization,
ciba, client-policies, client-secret-rotation, declarative-user-profile,
docker, dynamic-scopes, impersonation, js-adapter, map-storage,
openshift-integration, par, preview, recovery-codes, scripts,
step-up-authentication, token-exchange, update-email, web-authn.
Hostname:
@ -221,4 +221,4 @@ Logging:
Do NOT start the server using this command when deploying to production.
Use 'kc.sh start-dev --help-all' to list all available options, including build
options.
options.

View file

@ -124,19 +124,19 @@ Transaction:
Feature:
--features <feature> Enables a set of one or more features. Possible values are: authorization,
account2, account-api, admin-fine-grained-authz, admin-api, admin, admin2,
docker, impersonation, openshift-integration, scripts, token-exchange,
web-authn, client-policies, ciba, map-storage, par,
declarative-user-profile, dynamic-scopes, client-secret-rotation,
step-up-authentication, recovery-codes, update-email, js-adapter, preview.
--features <feature> Enables a set of one or more features. Possible values are: account-api,
account2, admin, admin-api, admin-fine-grained-authz, admin2, authorization,
ciba, client-policies, client-secret-rotation, declarative-user-profile,
docker, dynamic-scopes, impersonation, js-adapter, map-storage,
openshift-integration, par, preview, recovery-codes, scripts,
step-up-authentication, token-exchange, update-email, web-authn.
--features-disabled <feature>
Disables a set of one or more features. Possible values are: authorization,
account2, account-api, admin-fine-grained-authz, admin-api, admin, admin2,
docker, impersonation, openshift-integration, scripts, token-exchange,
web-authn, client-policies, ciba, map-storage, par,
declarative-user-profile, dynamic-scopes, client-secret-rotation,
step-up-authentication, recovery-codes, update-email, js-adapter, preview.
Disables a set of one or more features. Possible values are: account-api,
account2, admin, admin-api, admin-fine-grained-authz, admin2, authorization,
ciba, client-policies, client-secret-rotation, declarative-user-profile,
docker, dynamic-scopes, impersonation, js-adapter, map-storage,
openshift-integration, par, preview, recovery-codes, scripts,
step-up-authentication, token-exchange, update-email, web-authn.
Hostname:
@ -285,4 +285,4 @@ Security (Experimental):
Do NOT start the server using this command when deploying to production.
Use 'kc.sh start-dev --help-all' to list all available options, including build
options.
options.

View file

@ -72,19 +72,19 @@ Transaction:
Feature:
--features <feature> Enables a set of one or more features. Possible values are: authorization,
account2, account-api, admin-fine-grained-authz, admin-api, admin, admin2,
docker, impersonation, openshift-integration, scripts, token-exchange,
web-authn, client-policies, ciba, map-storage, par,
declarative-user-profile, dynamic-scopes, client-secret-rotation,
step-up-authentication, recovery-codes, update-email, js-adapter, preview.
--features <feature> Enables a set of one or more features. Possible values are: account-api,
account2, admin, admin-api, admin-fine-grained-authz, admin2, authorization,
ciba, client-policies, client-secret-rotation, declarative-user-profile,
docker, dynamic-scopes, impersonation, js-adapter, map-storage,
openshift-integration, par, preview, recovery-codes, scripts,
step-up-authentication, token-exchange, update-email, web-authn.
--features-disabled <feature>
Disables a set of one or more features. Possible values are: authorization,
account2, account-api, admin-fine-grained-authz, admin-api, admin, admin2,
docker, impersonation, openshift-integration, scripts, token-exchange,
web-authn, client-policies, ciba, map-storage, par,
declarative-user-profile, dynamic-scopes, client-secret-rotation,
step-up-authentication, recovery-codes, update-email, js-adapter, preview.
Disables a set of one or more features. Possible values are: account-api,
account2, admin, admin-api, admin-fine-grained-authz, admin2, authorization,
ciba, client-policies, client-secret-rotation, declarative-user-profile,
docker, dynamic-scopes, impersonation, js-adapter, map-storage,
openshift-integration, par, preview, recovery-codes, scripts,
step-up-authentication, token-exchange, update-email, web-authn.
Hostname:
@ -231,4 +231,4 @@ By default, this command tries to update the server configuration by running a
$ kc.sh start '--optimized'
By doing that, the server should start faster based on any previous
configuration you have set when manually running the 'build' command.
configuration you have set when manually running the 'build' command.

View file

@ -130,19 +130,19 @@ Transaction:
Feature:
--features <feature> Enables a set of one or more features. Possible values are: authorization,
account2, account-api, admin-fine-grained-authz, admin-api, admin, admin2,
docker, impersonation, openshift-integration, scripts, token-exchange,
web-authn, client-policies, ciba, map-storage, par,
declarative-user-profile, dynamic-scopes, client-secret-rotation,
step-up-authentication, recovery-codes, update-email, js-adapter, preview.
--features <feature> Enables a set of one or more features. Possible values are: account-api,
account2, admin, admin-api, admin-fine-grained-authz, admin2, authorization,
ciba, client-policies, client-secret-rotation, declarative-user-profile,
docker, dynamic-scopes, impersonation, js-adapter, map-storage,
openshift-integration, par, preview, recovery-codes, scripts,
step-up-authentication, token-exchange, update-email, web-authn.
--features-disabled <feature>
Disables a set of one or more features. Possible values are: authorization,
account2, account-api, admin-fine-grained-authz, admin-api, admin, admin2,
docker, impersonation, openshift-integration, scripts, token-exchange,
web-authn, client-policies, ciba, map-storage, par,
declarative-user-profile, dynamic-scopes, client-secret-rotation,
step-up-authentication, recovery-codes, update-email, js-adapter, preview.
Disables a set of one or more features. Possible values are: account-api,
account2, admin, admin-api, admin-fine-grained-authz, admin2, authorization,
ciba, client-policies, client-secret-rotation, declarative-user-profile,
docker, dynamic-scopes, impersonation, js-adapter, map-storage,
openshift-integration, par, preview, recovery-codes, scripts,
step-up-authentication, token-exchange, update-email, web-authn.
Hostname:
@ -295,4 +295,4 @@ By default, this command tries to update the server configuration by running a
$ kc.sh start '--optimized'
By doing that, the server should start faster based on any previous
configuration you have set when manually running the 'build' command.
configuration you have set when manually running the 'build' command.

View file

@ -110,6 +110,7 @@ public class OIDCLoginProtocolFactory extends AbstractLoginProtocolFactory {
@Override
public void init(Config.Scope config) {
initBuiltIns();
this.providerConfig = new OIDCProviderConfig(config);
if (providerConfig.isLegacyLogoutRedirectUri()) {
logger.warnf("Deprecated switch '%s' is enabled. Please try to disable it and update your clients to use OpenID Connect compliant way for RP-initiated logout.", CONFIG_LEGACY_LOGOUT_REDIRECT_URI);
@ -129,9 +130,9 @@ public class OIDCLoginProtocolFactory extends AbstractLoginProtocolFactory {
return builtins;
}
static Map<String, ProtocolMapperModel> builtins = new HashMap<>();
private Map<String, ProtocolMapperModel> builtins = new HashMap<>();
static {
void initBuiltIns() {
ProtocolMapperModel model;
model = UserPropertyMapper.createClaimMapper(USERNAME,
"username",
@ -218,7 +219,7 @@ public class OIDCLoginProtocolFactory extends AbstractLoginProtocolFactory {
}
}
private static void createUserAttributeMapper(String name, String attrName, String claimName, String type) {
private void createUserAttributeMapper(String name, String attrName, String claimName, String type) {
ProtocolMapperModel model = UserAttributeMapper.createClaimMapper(name,
attrName,
claimName, type,
@ -297,7 +298,7 @@ public class OIDCLoginProtocolFactory extends AbstractLoginProtocolFactory {
}
public static ClientScopeModel addRolesClientScope(RealmModel newRealm) {
public ClientScopeModel addRolesClientScope(RealmModel newRealm) {
ClientScopeModel rolesScope = KeycloakModelUtils.getClientScopeByName(newRealm, ROLES_SCOPE);
if (rolesScope == null) {
rolesScope = newRealm.addClientScope(ROLES_SCOPE);
@ -320,7 +321,7 @@ public class OIDCLoginProtocolFactory extends AbstractLoginProtocolFactory {
}
public static ClientScopeModel addWebOriginsClientScope(RealmModel newRealm) {
public ClientScopeModel addWebOriginsClientScope(RealmModel newRealm) {
ClientScopeModel originsScope = KeycloakModelUtils.getClientScopeByName(newRealm, WEB_ORIGINS_SCOPE);
if (originsScope == null) {
originsScope = newRealm.addClientScope(WEB_ORIGINS_SCOPE);
@ -347,7 +348,7 @@ public class OIDCLoginProtocolFactory extends AbstractLoginProtocolFactory {
* @param newRealm the realm to which the {@code microprofile-jwt} scope is to be added.
* @return a reference to the {@code microprofile-jwt} client scope that was either created or already exists in the realm.
*/
public static ClientScopeModel addMicroprofileJWTClientScope(RealmModel newRealm) {
public ClientScopeModel addMicroprofileJWTClientScope(RealmModel newRealm) {
ClientScopeModel microprofileScope = KeycloakModelUtils.getClientScopeByName(newRealm, MICROPROFILE_JWT_SCOPE);
if (microprofileScope == null) {
microprofileScope = newRealm.addClientScope(MICROPROFILE_JWT_SCOPE);
@ -366,7 +367,7 @@ public class OIDCLoginProtocolFactory extends AbstractLoginProtocolFactory {
}
public static void addAcrClientScope(RealmModel newRealm) {
public void addAcrClientScope(RealmModel newRealm) {
if (Profile.isFeatureEnabled(Profile.Feature.STEP_UP_AUTHENTICATION)) {
ClientScopeModel acrScope = KeycloakModelUtils.getClientScopeByName(newRealm, ACR_SCOPE);
if (acrScope == null) {

View file

@ -23,17 +23,15 @@ import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.LoginProtocolFactory;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.services.managers.RealmManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -84,26 +82,29 @@ public class DefaultMigrationProvider implements MigrationProvider {
new RealmManager(session).setupAdminCli(realm);
}
private OIDCLoginProtocolFactory getOIDCLoginProtocolFactory() {
return (OIDCLoginProtocolFactory) session.getKeycloakSessionFactory().getProviderFactory(LoginProtocol.class, OIDCLoginProtocol.LOGIN_PROTOCOL);
}
@Override
public ClientScopeModel addOIDCRolesClientScope(RealmModel realm) {
return OIDCLoginProtocolFactory.addRolesClientScope(realm);
return getOIDCLoginProtocolFactory().addRolesClientScope(realm);
}
@Override
public ClientScopeModel addOIDCWebOriginsClientScope(RealmModel realm) {
return OIDCLoginProtocolFactory.addWebOriginsClientScope(realm);
return getOIDCLoginProtocolFactory().addWebOriginsClientScope(realm);
}
@Override
public ClientScopeModel addOIDCMicroprofileJWTClientScope(RealmModel realm) {
return OIDCLoginProtocolFactory.addMicroprofileJWTClientScope(realm);
return getOIDCLoginProtocolFactory().addMicroprofileJWTClientScope(realm);
}
@Override
public void addOIDCAcrClientScope(RealmModel realm) {
OIDCLoginProtocolFactory.addAcrClientScope(realm);
getOIDCLoginProtocolFactory().addAcrClientScope(realm);
}
@Override

View file

@ -112,6 +112,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.UUID;
@ -890,89 +891,66 @@ public class TestingResourceProvider implements RealmResourceProvider {
}
}
@GET
@Path("/list-disabled-features")
@Produces(MediaType.APPLICATION_JSON)
public Set<Profile.Feature> listDisabledFeatures() {
return Profile.getInstance().getDisabledFeatures();
}
@POST
@Path("/enable-feature/{feature}")
@Consumes(MediaType.APPLICATION_JSON)
public Response enableFeature(@PathParam("feature") String feature) {
Profile.Feature featureProfile;
try {
featureProfile = Profile.Feature.valueOf(feature);
} catch (IllegalArgumentException e) {
System.err.printf("Feature '%s' doesn't exist!!\n", feature);
return Response.status(Response.Status.NOT_FOUND).build();
}
if (Profile.isFeatureEnabled(featureProfile))
return Response.noContent().build();
FeatureDeployerUtil.initBeforeChangeFeature(featureProfile);
System.setProperty("keycloak.profile.feature." + featureProfile.toString().toLowerCase(), "enabled");
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"), featureProfile, "enabled");
}
Profile.init();
FeatureDeployerUtil.deployFactoriesAfterFeatureEnabled(featureProfile);
if (Profile.isFeatureEnabled(featureProfile))
return Response.noContent().build();
else
return Response.status(Response.Status.NOT_FOUND).build();
@Produces(MediaType.APPLICATION_JSON)
public Set<Profile.Feature> enableFeature(@PathParam("feature") String feature) {
return updateFeature(feature, true);
}
@POST
@Path("/disable-feature/{feature}")
@Consumes(MediaType.APPLICATION_JSON)
public Response disableFeature(@PathParam("feature") String feature) {
Profile.Feature featureProfile;
@Produces(MediaType.APPLICATION_JSON)
public Set<Profile.Feature> disableFeature(@PathParam("feature") String feature) {
return updateFeature(feature, false);
}
private Set<Profile.Feature> updateFeature(String featureKey, boolean shouldEnable) {
Profile.Feature feature;
try {
featureProfile = Profile.Feature.valueOf(feature);
feature = Profile.Feature.valueOf(featureKey);
} catch (IllegalArgumentException e) {
System.err.printf("Feature '%s' doesn't exist!!\n", feature);
return Response.status(Response.Status.NOT_FOUND).build();
System.err.printf("Feature '%s' doesn't exist!!\n", featureKey);
throw new BadRequestException();
}
if (!Profile.isFeatureEnabled(featureProfile))
return Response.noContent().build();
if (Profile.getInstance().getFeatures().get(feature) != shouldEnable) {
FeatureDeployerUtil.initBeforeChangeFeature(feature);
FeatureDeployerUtil.initBeforeChangeFeature(featureProfile);
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");
}
disableFeatureProperties(featureProfile);
Profile current = Profile.getInstance();
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"), featureProfile, "disabled");
Map<Profile.Feature, Boolean> updatedFeatures = new HashMap<>();
updatedFeatures.putAll(current.getFeatures());
updatedFeatures.put(feature, shouldEnable);
Profile.init(current.getName(), updatedFeatures);
if (shouldEnable) {
FeatureDeployerUtil.deployFactoriesAfterFeatureEnabled(feature);
} else {
FeatureDeployerUtil.undeployFactoriesAfterFeatureDisabled(feature);
}
}
Profile.init();
FeatureDeployerUtil.undeployFactoriesAfterFeatureDisabled(featureProfile);
if (!Profile.isFeatureEnabled(featureProfile))
return Response.noContent().build();
else
return Response.status(Response.Status.NOT_FOUND).build();
return Profile.getInstance().getDisabledFeatures();
}
/**
* KEYCLOAK-12958
*/
private void disableFeatureProperties(Profile.Feature feature) {
Profile.Type type = feature.getType();
if (type.equals(Profile.Type.DEFAULT)) {
System.setProperty("keycloak.profile.feature." + feature.toString().toLowerCase(), "disabled");
} else {
System.getProperties().remove("keycloak.profile.feature." + feature.toString().toLowerCase());
}
}
@GET
@Path("/set-system-property")

View file

@ -18,14 +18,9 @@
package org.keycloak.testsuite;
import org.junit.Assume;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.common.Profile;
import org.keycloak.representations.info.ProfileInfoRepresentation;
import org.keycloak.testsuite.util.AdminClientUtil;
import org.keycloak.testsuite.arquillian.TestContext;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
@ -33,51 +28,34 @@ import java.util.Set;
*/
public class ProfileAssume {
private static Set<String> disabledFeatures;
private static String profile;
private static Set<Profile.Feature> DISABLED_FEATURES;
private static TestContext TEST_CONTEXT;
private static void updateProfile() {
String host = System.getProperty("auth.server.host", "localhost");
String port = System.getProperty("auth.server.http.port", "8180");
boolean adapterCompatTesting = Boolean.parseBoolean(System.getProperty("testsuite.adapter.compat.testing"));
String authServerContextRoot = "http://" + host + ":" + port;
try (Keycloak adminClient = AdminClientUtil.createAdminClient(adapterCompatTesting, authServerContextRoot)) {
ProfileInfoRepresentation profileInfo = adminClient.serverInfo().getInfo().getProfileInfo();
profile = profileInfo.getName();
List<String> disabled = profileInfo.getDisabledFeatures();
disabledFeatures = Collections.unmodifiableSet(new HashSet<>(disabled));
} catch (Exception e) {
throw new RuntimeException("Failed to obtain profile / features info from serverinfo endpoint of " + authServerContextRoot, e);
if (DISABLED_FEATURES == null) {
DISABLED_FEATURES = TEST_CONTEXT.getTestingClient().testing().listDisabledFeatures();
}
}
public static void assumeFeatureEnabled(Profile.Feature feature) {
updateProfile();
Assume.assumeTrue("Ignoring test as feature " + feature.name() + " is not enabled", isFeatureEnabled(feature));
Assume.assumeTrue("Ignoring test as feature " + feature.getKey() + " is not enabled", isFeatureEnabled(feature));
}
public static void assumeFeatureDisabled(Profile.Feature feature) {
Assume.assumeTrue("Ignoring test as feature " + feature.name() + " is enabled", !isFeatureEnabled(feature));
}
public static void assumePreview() {
updateProfile();
Assume.assumeTrue("Ignoring test as community/preview profile is not enabled", !profile.equals("product"));
}
public static void assumePreviewDisabled() {
updateProfile();
Assume.assumeFalse("Ignoring test as community/preview profile is enabled", !profile.equals("product"));
}
public static void assumeCommunity() {
updateProfile();
Assume.assumeTrue("Ignoring test as community profile is not enabled", profile.equals("community"));
Assume.assumeTrue("Ignoring test as feature " + feature.getKey() + " is enabled", !isFeatureEnabled(feature));
}
public static boolean isFeatureEnabled(Profile.Feature feature) {
updateProfile();
return !disabledFeatures.contains(feature.name());
return !DISABLED_FEATURES.contains(feature);
}
public static void updateDisabledFeatures(Set<Profile.Feature> disabledFeatures) {
DISABLED_FEATURES = disabledFeatures;
}
public static void setTestContext(TestContext testContext) {
TEST_CONTEXT = testContext;
}
}

View file

@ -43,6 +43,7 @@ import org.keycloak.admin.client.Keycloak;
import org.keycloak.common.util.StringPropertyReplacer;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.services.error.KeycloakErrorHandler;
import org.keycloak.testsuite.ProfileAssume;
import org.keycloak.testsuite.arquillian.annotation.SetDefaultProvider;
import org.keycloak.testsuite.arquillian.annotation.UncaughtServerErrorExpected;
import org.keycloak.testsuite.arquillian.annotation.EnableVault;
@ -462,6 +463,7 @@ public class AuthServerTestEnricher {
public void initializeTestContext(@Observes(precedence = 2) BeforeClass event) throws Exception {
TestContext testContext = new TestContext(suiteContext, event.getTestClass().getJavaClass());
testContextProducer.set(testContext);
ProfileAssume.setTestContext(testContext);
boolean wasUpdated = false;

View file

@ -28,6 +28,7 @@ import org.jboss.arquillian.core.api.annotation.Inject;
import org.jboss.arquillian.test.spi.TestResult;
import org.keycloak.common.Profile;
import org.keycloak.common.util.reflections.Reflections;
import org.keycloak.testsuite.ProfileAssume;
import org.keycloak.testsuite.arquillian.annotation.ModelTest;
import org.keycloak.testsuite.client.KeycloakTestingClient;
@ -50,7 +51,7 @@ public class ModelTestExecutor extends LocalTestExecuter {
super.execute(event);
} else {
TestResult result = new TestResult();
if (annotation.skipForMapStorage() && Profile.isFeatureEnabled(Profile.Feature.MAP_STORAGE)) {
if (annotation.skipForMapStorage() && ProfileAssume.isFeatureEnabled(Profile.Feature.MAP_STORAGE)) {
result = TestResult.skipped();
}
else {

View file

@ -27,11 +27,6 @@ public @interface DisableFeature {
*/
boolean skipRestart() default false;
/**
* The feature will be disabled only if the `product` profile is activated
*/
boolean onlyForProduct() default false;
/**
* Feature disable should be the last action in @Before context.
* If the test halted, the feature is returned to the previous state.

View file

@ -27,11 +27,6 @@ public @interface EnableFeature {
*/
boolean skipRestart() default false;
/**
* The feature will be enabled only if the `product` profile is activated
*/
boolean onlyForProduct() default false;
/**
* Feature enable should be the last action in @Before context.
* If the test halted, the feature is returned to the previous state.

View file

@ -71,21 +71,18 @@ public class KeycloakContainerFeaturesController {
private Profile.Feature feature;
private boolean skipRestart;
private FeatureAction action;
private boolean onlyForProduct;
private final AnnotatedElement annotatedElement;
public UpdateFeature(Profile.Feature feature, boolean skipRestart, FeatureAction action, boolean onlyForProduct
, AnnotatedElement annotatedElement) {
public UpdateFeature(Profile.Feature feature, boolean skipRestart, FeatureAction action, AnnotatedElement annotatedElement) {
this.feature = feature;
this.skipRestart = skipRestart;
this.action = action;
this.onlyForProduct = onlyForProduct;
this.annotatedElement = annotatedElement;
}
private void assertPerformed() {
assertThat("An annotation requested to " + action.name() +
" feature " + feature.name() + ", however after performing this operation " +
" feature " + feature.getKey() + ", however after performing this operation " +
"the feature is not in desired state" ,
ProfileAssume.isFeatureEnabled(feature),
is(action == FeatureAction.ENABLE));
@ -122,10 +119,6 @@ public class KeycloakContainerFeaturesController {
return action;
}
public boolean isOnlyForProduct() {
return onlyForProduct;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
@ -147,7 +140,6 @@ public class KeycloakContainerFeaturesController {
private void updateFeatures(Set<UpdateFeature> updateFeatures) throws Exception {
updateFeatures = updateFeatures.stream()
.filter(this::skipForProduct)
.collect(Collectors.toSet());
updateFeatures.forEach(UpdateFeature::performAction);
@ -160,11 +152,6 @@ public class KeycloakContainerFeaturesController {
updateFeatures.forEach(UpdateFeature::assertPerformed);
}
// KEYCLOAK-12958 WebAuthn profile product/project
private boolean skipForProduct(UpdateFeature feature) {
return !feature.onlyForProduct || Profile.getName().equals("product");
}
private void checkAnnotatedElementForFeatureAnnotations(AnnotatedElement annotatedElement, State state) throws Exception {
Set<UpdateFeature> updateFeatureSet = new HashSet<>();
@ -191,13 +178,12 @@ public class KeycloakContainerFeaturesController {
ret.addAll(Arrays.stream(annotatedElement.getAnnotationsByType(EnableFeature.class))
.map(annotation -> new UpdateFeature(annotation.value(), annotation.skipRestart(),
state == State.BEFORE ? FeatureAction.ENABLE : FeatureAction.DISABLE, annotation.onlyForProduct(), annotatedElement))
state == State.BEFORE ? FeatureAction.ENABLE : FeatureAction.DISABLE, annotatedElement))
.collect(Collectors.toSet()));
ret.addAll(Arrays.stream(annotatedElement.getAnnotationsByType(DisableFeature.class))
.map(annotation -> new UpdateFeature(annotation.value(), annotation.skipRestart(),
state == State.BEFORE ? FeatureAction.DISABLE : FeatureAction.ENABLE, annotation.onlyForProduct(),
annotatedElement))
state == State.BEFORE ? FeatureAction.DISABLE : FeatureAction.ENABLE, annotatedElement))
.collect(Collectors.toSet()));
return ret;

View file

@ -22,7 +22,9 @@ import javax.ws.rs.core.Response;
import org.jboss.resteasy.client.jaxrs.ResteasyClient;
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
import org.jboss.resteasy.client.jaxrs.ResteasyWebTarget;
import org.junit.Assert;
import org.keycloak.common.Profile;
import org.keycloak.testsuite.ProfileAssume;
import org.keycloak.testsuite.client.resources.TestApplicationResource;
import org.keycloak.testsuite.client.resources.TestExampleCompanyResource;
import org.keycloak.testsuite.client.resources.TestSamlApplicationResource;
@ -31,6 +33,9 @@ import org.keycloak.testsuite.runonserver.*;
import org.keycloak.testsuite.util.AdminClientUtil;
import org.keycloak.util.JsonSerialization;
import java.util.List;
import java.util.Set;
import static org.junit.Assert.assertEquals;
/**
@ -74,15 +79,15 @@ public class KeycloakTestingClient implements AutoCloseable {
}
public void enableFeature(Profile.Feature feature) {
try (Response response = testing().enableFeature(feature.toString())) {
assertEquals(204, response.getStatus());
}
Set<Profile.Feature> disabledFeatures = testing().enableFeature(feature.toString());
Assert.assertFalse(disabledFeatures.contains(feature));
ProfileAssume.updateDisabledFeatures(disabledFeatures);
}
public void disableFeature(Profile.Feature feature) {
try (Response response = testing().disableFeature(feature.toString())) {
assertEquals(204, response.getStatus());
}
Set<Profile.Feature> disabledFeatures = testing().disableFeature(feature.toString());
Assert.assertTrue(disabledFeatures.contains(feature));
ProfileAssume.updateDisabledFeatures(disabledFeatures);
}
public TestApplicationResource testApp() { return target.proxy(TestApplicationResource.class); }

View file

@ -18,6 +18,7 @@
package org.keycloak.testsuite.client.resources;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.keycloak.common.Profile;
import org.keycloak.representations.idm.AdminEventRepresentation;
import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
import org.keycloak.representations.idm.EventRepresentation;
@ -38,6 +39,8 @@ import javax.ws.rs.core.Response;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.infinispan.commons.time.TimeService;
/**
@ -332,15 +335,22 @@ public interface TestingResource {
@Produces(MediaType.TEXT_HTML_UTF_8)
String getJavascriptTestingEnvironment();
@GET
@Path("/list-disabled-features")
@Produces(MediaType.APPLICATION_JSON)
Set<Profile.Feature> listDisabledFeatures();
@POST
@Path("/enable-feature/{feature}")
@Consumes(MediaType.APPLICATION_JSON)
Response enableFeature(@PathParam("feature") String feature);
@Produces(MediaType.APPLICATION_JSON)
Set<Profile.Feature> enableFeature(@PathParam("feature") String feature);
@POST
@Path("/disable-feature/{feature}")
@Consumes(MediaType.APPLICATION_JSON)
Response disableFeature(@PathParam("feature") String feature);
@Produces(MediaType.APPLICATION_JSON)
Set<Profile.Feature> disableFeature(@PathParam("feature") String feature);
/**
* If property-value is null, the system property will be unset (removed) on the server

View file

@ -879,8 +879,6 @@ public class DemoServletsAdapterTest extends AbstractServletsAdapterTest {
@Test
public void testOIDCUiLocalesParamForwarding() {
ProfileAssume.assumeCommunity();
RealmRepresentation demoRealmRep = testRealmResource().toRepresentation();
boolean enabled = demoRealmRep.isInternationalizationEnabled();
String defaultLocale = demoRealmRep.getDefaultLocale();

View file

@ -23,6 +23,7 @@ import org.keycloak.common.Profile;
import org.keycloak.representations.idm.AuthenticatorConfigInfoRepresentation;
import org.keycloak.representations.idm.ConfigPropertyRepresentation;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.ProfileAssume;
import javax.ws.rs.NotFoundException;
import java.util.ArrayList;
@ -141,7 +142,7 @@ public class ProvidersTest extends AbstractAuthenticationTest {
"Validates a OTP on a separate OTP form. Only shown if required based on the configured conditions.");
addProviderInfo(result, "auth-cookie", "Cookie", "Validates the SSO cookie set by the auth server.");
addProviderInfo(result, "auth-otp-form", "OTP Form", "Validates a OTP on a separate OTP form.");
if (Profile.isFeatureEnabled(Profile.Feature.SCRIPTS)) {
if (ProfileAssume.isFeatureEnabled(Profile.Feature.SCRIPTS)) {
addProviderInfo(result, "auth-script-based", "Script", "Script based authentication. Allows to define custom authentication logic via JavaScript.");
}
addProviderInfo(result, "auth-spnego", "Kerberos", "Initiates the SPNEGO protocol. Most often used with Kerberos.");

View file

@ -1,31 +0,0 @@
package org.keycloak.testsuite.admin.authentication;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.testsuite.ProfileAssume;
import javax.ws.rs.BadRequestException;
import java.util.HashMap;
/**
* @author mhajas
*/
public class ScriptBasedAuthenticatorTest extends AbstractAuthenticationTest {
@Test
public void checkIfTurnedOffWithProductProfile() throws InterruptedException {
ProfileAssume.assumePreviewDisabled();
HashMap<String, String> params = new HashMap<>();
params.put("newName", "Copy-of-browser");
authMgmtResource.copy("browser", params);
params.put("provider", "auth-script-based");
try {
authMgmtResource.addExecution("Copy-of-browser", params);
Assert.fail("Adding script based authenticator should fail with product profile");
} catch (BadRequestException ex) {
//Expected
}
}
}

View file

@ -604,10 +604,8 @@ public class SocialLoginTest extends AbstractKeycloakTest {
String username = users.get(0).getUsername();
checkFeature(501, username);
Response tokenResp = testingClient.testing().enableFeature(Profile.Feature.TOKEN_EXCHANGE.toString());
assertEquals(200, tokenResp.getStatus());
testingClient.enableFeature(Profile.Feature.TOKEN_EXCHANGE);
ProfileAssume.assumeFeatureEnabled(Profile.Feature.TOKEN_EXCHANGE);
Client httpClient = AdminClientUtil.createResteasyClient();
try {
@ -690,8 +688,7 @@ public class SocialLoginTest extends AbstractKeycloakTest {
adminClient.realm(REALM).identityProviders().get(idp.getAlias()).update(idp);
} finally {
httpClient.close();
tokenResp = testingClient.testing().disableFeature(Profile.Feature.TOKEN_EXCHANGE.toString());
assertEquals(200, tokenResp.getStatus());
testingClient.disableFeature(Profile.Feature.TOKEN_EXCHANGE);
checkFeature(501, username);
}
}

View file

@ -16,6 +16,7 @@
*/
package org.keycloak.testsuite.crossdc;
import org.junit.Ignore;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.common.Profile;
@ -86,6 +87,10 @@ public class ActionTokenCrossDCTest extends AbstractAdminCrossDCTest {
@Test
@InitialDcState(authServers = ServerSetup.ALL_NODES_IN_FIRST_DC_FIRST_NODE_IN_SECOND_DC)
// KEYCLOAK-17584: Temporarily disable the test for 'community' profile till KEYCLOAK-17628 isn't fixed. In other words till:
// * The test is either rewritten to start using the new Wildfly subsystem for base metrics introduced in Wildfly 22,
// * Or Keycloak is able to load the Eclipse MicroProfile Metrics subsystem from the microprofile Galleon feature-pack
@Ignore
public void sendResetPasswordEmailSuccessWorksInCrossDc(
@JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=0, cacheName=InfinispanConnectionProvider.ACTION_TOKEN_CACHE) InfinispanStatistics cacheDc0Node0Statistics,
@JmxInfinispanCacheStatistics(dc=DC.FIRST, dcNodeIndex=1, cacheName=InfinispanConnectionProvider.ACTION_TOKEN_CACHE) InfinispanStatistics cacheDc0Node1Statistics,
@ -93,11 +98,6 @@ public class ActionTokenCrossDCTest extends AbstractAdminCrossDCTest {
@JmxInfinispanChannelStatistics() InfinispanStatistics channelStatisticsCrossDc) throws Exception {
log.debug("--DC: START sendResetPasswordEmailSuccessWorksInCrossDc");
// KEYCLOAK-17584: Temporarily disable the test for 'community' profile till KEYCLOAK-17628 isn't fixed. In other words till:
// * The test is either rewritten to start using the new Wildfly subsystem for base metrics introduced in Wildfly 22,
// * Or Keycloak is able to load the Eclipse MicroProfile Metrics subsystem from the microprofile Galleon feature-pack
Assume.assumeTrue("Ignoring test as product profile is not enabled", Profile.getName().equals("product"));
cacheDc0Node1Statistics.waitToBecomeAvailable(10, TimeUnit.SECONDS);
Comparable originalNumberOfEntries = cacheDc0Node0Statistics.getSingleStatistics(Constants.STAT_CACHE_NUMBER_OF_ENTRIES_IN_MEMORY);

View file

@ -717,4 +717,4 @@ public class LevelOfAssuranceFlowTest extends AbstractTestRealmKeycloakTest {
Assert.assertEquals(expectedError, errorPage.getError());
events.clear();
}
}
}

View file

@ -60,8 +60,6 @@ public class AccountPageTest extends AbstractI18NTest {
@Test
public void testLocalizedReferrerLinkContent() {
ProfileAssume.assumeCommunity();
RealmResource testRealm = testRealm();
List<ClientRepresentation> foundClients = testRealm.clients().findByClientId("var-named-test-app");
if (foundClients.isEmpty()) {

View file

@ -97,8 +97,6 @@ public class EmailTest extends AbstractI18NTest {
@Test
public void restPasswordEmailGerman() throws IOException, MessagingException {
ProfileAssume.assumeCommunity();
changeUserLocale("de");
loginPage.open();
@ -118,8 +116,6 @@ public class EmailTest extends AbstractI18NTest {
//KEYCLOAK-7478
@Test
public void changeLocaleOnInfoPage() throws InterruptedException, IOException, MessagingException {
ProfileAssume.assumeCommunity();
UserResource testUser = ApiUtil.findUserByUsernameId(testRealm(), "login-test");
testUser.executeActionsEmail(Arrays.asList(UserModel.RequiredAction.UPDATE_PASSWORD.toString()));

View file

@ -88,8 +88,6 @@ public class LoginPageTest extends AbstractI18NTest {
@Test
public void languageDropdown() {
ProfileAssume.assumeCommunity();
loginPage.open();
Assert.assertEquals("English", loginPage.getLanguageDropdownText());
@ -123,8 +121,6 @@ public class LoginPageTest extends AbstractI18NTest {
@Test
public void acceptLanguageHeader() throws IOException {
ProfileAssume.assumeCommunity();
try(CloseableHttpClient httpClient = (CloseableHttpClient) new HttpClientBuilder().build()) {
ApacheHttpClient43Engine engine = new ApacheHttpClient43Engine(httpClient);
ResteasyClient client = ((ResteasyClientBuilder) ResteasyClientBuilder.newBuilder()).httpEngine(engine).build();
@ -156,8 +152,6 @@ public class LoginPageTest extends AbstractI18NTest {
// KEYCLOAK-3887
@Test
public void languageChangeRequiredActions() {
ProfileAssume.assumeCommunity();
UserResource user = ApiUtil.findUserByUsernameId(testRealm(), "test-user@localhost");
UserRepresentation userRep = user.toRepresentation();
userRep.setRequiredActions(Arrays.asList(UserModel.RequiredAction.UPDATE_PASSWORD.toString()));
@ -183,8 +177,6 @@ public class LoginPageTest extends AbstractI18NTest {
// KEYCLOAK-3887
@Test
public void languageChangeConsentScreen() {
ProfileAssume.assumeCommunity();
// Set client, which requires consent
oauth.clientId("third-party");
@ -210,8 +202,6 @@ public class LoginPageTest extends AbstractI18NTest {
@Test
public void languageUserUpdates() {
ProfileAssume.assumeCommunity();
loginPage.open();
loginPage.openLanguage("Deutsch");

View file

@ -516,8 +516,6 @@ public class X509BrowserLoginTest extends AbstractX509AuthenticationTest {
// KEYCLOAK-6866
@Test
public void changeLocaleOnX509InfoPage() {
ProfileAssume.assumeCommunity();
AuthenticatorConfigRepresentation cfg = newConfig("x509-browser-config", createLoginSubjectEmail2UsernameOrEmailConfig().getConfig());
String cfgId = createConfig(browserExecution.getId(), cfg);
Assert.assertNotNull(cfgId);

View file

@ -24,6 +24,8 @@ import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
import org.keycloak.authorization.policy.provider.PolicySpi;
import org.keycloak.authorization.store.StoreFactorySpi;
import org.keycloak.cluster.ClusterSpi;
import org.keycloak.common.Profile;
import org.keycloak.common.profile.PropertiesProfileConfigResolver;
import org.keycloak.common.util.Time;
import org.keycloak.component.ComponentFactoryProviderFactory;
import org.keycloak.component.ComponentFactorySpi;
@ -60,6 +62,7 @@ import java.lang.management.LockInfo;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
@ -290,6 +293,13 @@ public abstract class KeycloakModelTest {
LOG.debugf("Creating factory %d in %s using the following configuration:\n %s", factoryIndex, threadName, CONFIG);
DefaultKeycloakSessionFactory res = new DefaultKeycloakSessionFactory() {
@Override
public void init() {
Profile.configure(new PropertiesProfileConfigResolver(System.getProperties()));
super.init();
}
@Override
protected boolean isEnabled(ProviderFactory factory, Scope scope) {
return super.isEnabled(factory, scope) && isFactoryAllowed(factory);

View file

@ -23,6 +23,9 @@ import java.nio.file.Files;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.common.Profile;
import org.keycloak.common.profile.PropertiesFileProfileConfigResolver;
import org.keycloak.common.profile.PropertiesProfileConfigResolver;
import org.keycloak.platform.PlatformProvider;
public class TestPlatform implements PlatformProvider {
@ -31,6 +34,13 @@ public class TestPlatform implements PlatformProvider {
private File tmpDir;
public TestPlatform() {
Profile.configure(
new PropertiesProfileConfigResolver(System.getProperties()),
new PropertiesFileProfileConfigResolver()
);
}
@Override
public void onStartup(Runnable startupHook) {
startupHook.run();