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,87 +113,161 @@ public class Profile {
public Type getType() {
return type;
}
public Set<Feature> getDependencies() {
return dependencies;
}
private enum ProfileValue {
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 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 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 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 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;
private void logUnsupportedFeatures() {
logUnsuportedFeatures(Feature.Type.PREVIEW, Logger.Level.INFO);
logUnsuportedFeatures(Feature.Type.EXPERIMENTAL, Logger.Level.WARN);
logUnsuportedFeatures(Feature.Type.DEPRECATED, Logger.Level.WARN);
}
profile = properties.getProperty("profile");
if (profile != null) {
if (profile.equals("community")) {
profile = "default";
}
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(", "));
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;
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:

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:

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:

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:

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:

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,90 +891,67 @@ 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;
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();
@Produces(MediaType.APPLICATION_JSON)
public Set<Profile.Feature> disableFeature(@PathParam("feature") String feature) {
return updateFeature(feature, false);
}
if (!Profile.isFeatureEnabled(featureProfile))
return Response.noContent().build();
private Set<Profile.Feature> updateFeature(String featureKey, boolean shouldEnable) {
Profile.Feature feature;
FeatureDeployerUtil.initBeforeChangeFeature(featureProfile);
try {
feature = Profile.Feature.valueOf(featureKey);
} catch (IllegalArgumentException e) {
System.err.printf("Feature '%s' doesn't exist!!\n", featureKey);
throw new BadRequestException();
}
disableFeatureProperties(featureProfile);
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"), featureProfile, "disabled");
setFeatureInProfileFile(new File(jbossServerConfigDir, "profile.properties"), feature, shouldEnable ? "enabled" : "disabled");
}
Profile.init();
Profile current = Profile.getInstance();
FeatureDeployerUtil.undeployFactoriesAfterFeatureDisabled(featureProfile);
Map<Profile.Feature, Boolean> updatedFeatures = new HashMap<>();
updatedFeatures.putAll(current.getFeatures());
updatedFeatures.put(feature, shouldEnable);
if (!Profile.isFeatureEnabled(featureProfile))
return Response.noContent().build();
else
return Response.status(Response.Status.NOT_FOUND).build();
}
Profile.init(current.getName(), updatedFeatures);
/**
* 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");
if (shouldEnable) {
FeatureDeployerUtil.deployFactoriesAfterFeatureEnabled(feature);
} else {
System.getProperties().remove("keycloak.profile.feature." + feature.toString().toLowerCase());
FeatureDeployerUtil.undeployFactoriesAfterFeatureDisabled(feature);
}
}
return Profile.getInstance().getDisabledFeatures();
}
@GET
@Path("/set-system-property")
@Consumes(MediaType.TEXT_HTML_UTF_8)

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

@ -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();