KEYCLOAK-8556 Improvements to profile

This commit is contained in:
stianst 2018-10-11 19:38:29 +02:00 committed by Marek Posolda
parent 4483677cdd
commit 11374a2707
15 changed files with 318 additions and 94 deletions

View file

@ -50,6 +50,11 @@
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
</dependency>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>

View file

@ -17,12 +17,12 @@
package org.keycloak.common;
import org.jboss.logging.Logger;
import java.io.File;
import java.io.FileInputStream;
import java.util.Arrays;
import java.util.Collections;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
@ -32,95 +32,101 @@ import java.util.Set;
*/
public class Profile {
private static final Logger logger = Logger.getLogger(Profile.class);
public enum Type {
DEFAULT,
DISABLED_BY_DEFAULT,
PREVIEW,
EXPERIMENTAL
}
public enum Feature {
ACCOUNT2,
ADMIN_FINE_GRAINED_AUTHZ,
DOCKER,
IMPERSONATION,
OPENSHIFT_INTEGRATION,
SCRIPTS,
TOKEN_EXCHANGE
ACCOUNT2(Type.EXPERIMENTAL),
ACCOUNT_API(Type.PREVIEW),
ADMIN_FINE_GRAINED_AUTHZ(Type.PREVIEW),
DOCKER(Type.DISABLED_BY_DEFAULT),
IMPERSONATION(Type.DEFAULT),
OPENSHIFT_INTEGRATION(Type.DEFAULT),
SCRIPTS(Type.PREVIEW),
TOKEN_EXCHANGE(Type.PREVIEW);
private Type type;
Feature(Type type) {
this.type = type;
}
public Type getType() {
return type;
}
}
private enum ProductValue {
KEYCLOAK(),
RHSSO(Feature.ACCOUNT2);
private List<Feature> excluded;
ProductValue(Feature... excluded) {
this.excluded = Arrays.asList(excluded);
}
KEYCLOAK,
RHSSO
}
private enum ProfileValue {
PRODUCT(Feature.ADMIN_FINE_GRAINED_AUTHZ, Feature.SCRIPTS, Feature.DOCKER, Feature.ACCOUNT2, Feature.TOKEN_EXCHANGE),
PREVIEW(Feature.ACCOUNT2),
COMMUNITY(Feature.DOCKER, Feature.ACCOUNT2);
private List<Feature> disabled;
ProfileValue(Feature... disabled) {
this.disabled = Arrays.asList(disabled);
}
COMMUNITY,
PRODUCT,
PREVIEW
}
private static final Profile CURRENT = new Profile();
private static Profile CURRENT = new Profile();
private final ProductValue product;
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 Profile() {
Config config = new Config();
product = "rh-sso".equals(Version.NAME) ? ProductValue.RHSSO : ProductValue.KEYCLOAK;
profile = ProfileValue.valueOf(config.getProfile().toUpperCase());
try {
Properties props = new Properties();
for (Feature f : Feature.values()) {
Boolean enabled = config.getConfig(f);
String jbossServerConfigDir = System.getProperty("jboss.server.config.dir");
if (jbossServerConfigDir != null) {
File file = new File(jbossServerConfigDir, "profile.properties");
if (file.isFile()) {
props.load(new FileInputStream(file));
}
}
if (System.getProperties().containsKey("keycloak.profile")) {
props.setProperty("profile", System.getProperty("keycloak.profile"));
}
for (String k : System.getProperties().stringPropertyNames()) {
if (k.startsWith("keycloak.profile.feature.")) {
props.put(k.replace("keycloak.profile.feature.", "feature."), System.getProperty(k));
}
}
if (props.containsKey("profile")) {
profile = ProfileValue.valueOf(props.getProperty("profile").toUpperCase());
} else {
profile = ProfileValue.valueOf(Version.DEFAULT_PROFILE.toUpperCase());
}
disabledFeatures.addAll(profile.disabled);
disabledFeatures.removeAll(product.excluded);
for (String k : props.stringPropertyNames()) {
if (k.startsWith("feature.")) {
Feature f = Feature.valueOf(k.replace("feature.", "").toUpperCase());
if (props.get(k).equals("enabled")) {
disabledFeatures.remove(f);
} else if (props.get(k).equals("disabled")) {
switch (f.getType()) {
case DEFAULT:
if (enabled != null && !enabled) {
disabledFeatures.add(f);
}
}
break;
case DISABLED_BY_DEFAULT:
if (enabled == null || !enabled) {
disabledFeatures.add(f);
}
break;
case PREVIEW:
previewFeatures.add(f);
if (enabled == null || !enabled) {
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;
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static void init() {
CURRENT = new Profile();
}
public static String getName() {
return CURRENT.profile.name().toLowerCase();
}
@ -129,11 +135,68 @@ public class Profile {
return CURRENT.disabledFeatures;
}
public static Set<Feature> getPreviewFeatures() {
return CURRENT.previewFeatures;
}
public static Set<Feature> getExperimentalFeatures() {
return CURRENT.experimentalFeatures;
}
public static boolean isFeatureEnabled(Feature feature) {
if (CURRENT.product.excluded.contains(feature)) {
return false;
}
return !CURRENT.disabledFeatures.contains(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()) {
properties.load(new FileInputStream(file));
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public String getProfile() {
String profile = System.getProperty("keycloak.profile");
if (profile != null) {
return profile;
}
profile = properties.getProperty("profile");
if (profile != null) {
return profile;
}
return Version.DEFAULT_PROFILE;
}
public Boolean getConfig(Feature feature) {
String config = System.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);
}
}
}
}

View file

@ -0,0 +1,97 @@
package org.keycloak.common;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Properties;
import java.util.Set;
public class ProfileTest {
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void checkDefaults() {
Assert.assertEquals("community", Profile.getName());
assertEquals(Profile.getDisabledFeatures(), Profile.Feature.ACCOUNT2, Profile.Feature.ACCOUNT_API, Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.DOCKER, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE);
assertEquals(Profile.getPreviewFeatures(), Profile.Feature.ACCOUNT_API, Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ, Profile.Feature.SCRIPTS, Profile.Feature.TOKEN_EXCHANGE);
assertEquals(Profile.getExperimentalFeatures(), Profile.Feature.ACCOUNT2);
}
@Test
public void configWithSystemProperties() {
Assert.assertEquals("community", Profile.getName());
Assert.assertFalse(Profile.isFeatureEnabled(Profile.Feature.DOCKER));
Assert.assertTrue(Profile.isFeatureEnabled(Profile.Feature.IMPERSONATION));
System.setProperty("keycloak.profile", "preview");
System.setProperty("keycloak.profile.feature.docker", "enabled");
System.setProperty("keycloak.profile.feature.impersonation", "disabled");
Profile.init();
Assert.assertEquals("preview", Profile.getName());
Assert.assertTrue(Profile.isFeatureEnabled(Profile.Feature.DOCKER));
Assert.assertFalse(Profile.isFeatureEnabled(Profile.Feature.IMPERSONATION));
System.getProperties().remove("keycloak.profile");
System.getProperties().remove("keycloak.profile.feature.docker");
System.getProperties().remove("keycloak.profile.feature.impersonation");
Profile.init();
}
@Test
public void configWithPropertiesFile() throws IOException {
Assert.assertEquals("community", Profile.getName());
Assert.assertFalse(Profile.isFeatureEnabled(Profile.Feature.DOCKER));
Assert.assertTrue(Profile.isFeatureEnabled(Profile.Feature.IMPERSONATION));
File d = temporaryFolder.newFolder();
File f = new File(d, "profile.properties");
Properties p = new Properties();
p.setProperty("profile", "preview");
p.setProperty("feature.docker", "enabled");
p.setProperty("feature.impersonation", "disabled");
PrintWriter pw = new PrintWriter(f);
p.list(pw);
pw.close();
System.setProperty("jboss.server.config.dir", d.getAbsolutePath());
Profile.init();
Assert.assertEquals("preview", Profile.getName());
Assert.assertTrue(Profile.isFeatureEnabled(Profile.Feature.DOCKER));
Assert.assertFalse(Profile.isFeatureEnabled(Profile.Feature.IMPERSONATION));
System.getProperties().remove("jboss.server.config.dir");
Profile.init();
}
public static void assertEquals(Set<Profile.Feature> actual, Profile.Feature... expected) {
Profile.Feature[] a = actual.toArray(new Profile.Feature[actual.size()]);
Arrays.sort(a, new FeatureComparator());
Arrays.sort(expected, new FeatureComparator());
Assert.assertArrayEquals(a, expected);
}
private static class FeatureComparator implements Comparator<Profile.Feature> {
@Override
public int compare(Profile.Feature o1, Profile.Feature o2) {
return o1.name().compareTo(o2.name());
}
}
}

View file

@ -21,6 +21,7 @@ import org.keycloak.common.Profile;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -29,15 +30,16 @@ public class ProfileInfoRepresentation {
private String name;
private List<String> disabledFeatures;
private List<String> previewFeatures;
private List<String> experimentalFeatures;
public static ProfileInfoRepresentation create() {
ProfileInfoRepresentation info = new ProfileInfoRepresentation();
info.name = Profile.getName();
info.disabledFeatures = new LinkedList<>();
for (Profile.Feature f : Profile.getDisabledFeatures()) {
info.disabledFeatures.add(f.name());
}
info.disabledFeatures = names(Profile.getDisabledFeatures());
info.previewFeatures = names(Profile.getPreviewFeatures());
info.experimentalFeatures = names(Profile.getExperimentalFeatures());
return info;
}
@ -50,4 +52,20 @@ public class ProfileInfoRepresentation {
return disabledFeatures;
}
public List<String> getPreviewFeatures() {
return previewFeatures;
}
public List<String> getExperimentalFeatures() {
return experimentalFeatures;
}
private static List<String> names(Set<Profile.Feature> featureSet) {
List<String> l = new LinkedList();
for (Profile.Feature f : featureSet) {
l.add(f.name());
}
return l;
}
}

View file

@ -27,6 +27,7 @@
<module name="org.bouncycastle" />
<module name="javax.api"/>
<module name="javax.activation.api"/>
<module name="org.jboss.logging"/>
<module name="sun.jdk" optional="true" />
</dependencies>

View file

@ -27,6 +27,7 @@
<module name="org.bouncycastle" />
<module name="javax.api"/>
<module name="javax.activation.api"/>
<module name="org.jboss.logging"/>
<module name="sun.jdk" optional="true" />
</dependencies>

View file

@ -27,6 +27,7 @@
<module name="org.bouncycastle" />
<module name="javax.api"/>
<module name="javax.activation.api"/>
<module name="org.jboss.logging"/>
<module name="sun.jdk" optional="true" />
</dependencies>

View file

@ -24,6 +24,7 @@
<module name="org.bouncycastle" />
<module name="javax.api"/>
<module name="javax.activation.api"/>
<module name="org.jboss.logging"/>
<module name="sun.jdk" optional="true" />
</dependencies>
</module>

View file

@ -27,6 +27,7 @@
<module name="org.bouncycastle" />
<module name="javax.api"/>
<module name="javax.activation.api"/>
<module name="org.jboss.logging"/>
<module name="sun.jdk" optional="true" />
</dependencies>

View file

@ -27,6 +27,7 @@
<module name="org.bouncycastle" />
<module name="javax.api"/>
<module name="javax.activation.api"/>
<module name="org.jboss.logging"/>
<module name="sun.jdk" optional="true" />
</dependencies>

View file

@ -206,7 +206,7 @@ public class AccountRestService {
@Produces(MediaType.APPLICATION_JSON)
@NoCache
public Response sessions() {
checkAccount2Enabled();
checkAccountApiEnabled();
List<SessionRepresentation> reps = new LinkedList<>();
List<UserSessionModel> sessions = session.sessions().getUserSessions(realm, user);
@ -244,7 +244,7 @@ public class AccountRestService {
@Produces(MediaType.APPLICATION_JSON)
@NoCache
public Response sessionsLogout(@QueryParam("current") boolean removeCurrent) {
checkAccount2Enabled();
checkAccountApiEnabled();
UserSessionModel userSession = auth.getSession();
List<UserSessionModel> userSessions = session.sessions().getUserSessions(realm, user);
@ -268,7 +268,7 @@ public class AccountRestService {
@Produces(MediaType.APPLICATION_JSON)
@NoCache
public Response sessionLogout(@QueryParam("id") String id) {
checkAccount2Enabled();
checkAccountApiEnabled();
UserSessionModel userSession = session.sessions().getUserSession(realm, id);
if (userSession != null && userSession.getUser().equals(user)) {
AuthenticationManager.backchannelLogout(session, userSession, true);
@ -278,7 +278,7 @@ public class AccountRestService {
@Path("/credentials")
public AccountCredentialResource credentials() {
checkAccount2Enabled();
checkAccountApiEnabled();
return new AccountCredentialResource(session, event, user);
}
@ -286,8 +286,8 @@ public class AccountRestService {
// TODO Applications
// TODO Logs
private static void checkAccount2Enabled() {
if (!Profile.isFeatureEnabled(Profile.Feature.ACCOUNT2)) {
private static void checkAccountApiEnabled() {
if (!Profile.isFeatureEnabled(Profile.Feature.ACCOUNT_API)) {
throw new NotFoundException();
}
}

View file

@ -44,7 +44,7 @@ import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.junit.Assert.*;
import org.keycloak.services.messages.Messages;
import static org.keycloak.common.Profile.Feature.ACCOUNT2;
import static org.keycloak.common.Profile.Feature.ACCOUNT_API;
import static org.keycloak.testsuite.ProfileAssume.assumeFeatureEnabled;
/**
@ -234,7 +234,7 @@ public class AccountRestServiceTest extends AbstractTestRealmKeycloakTest {
@Test
public void testGetSessions() throws IOException {
assumeFeatureEnabled(ACCOUNT2);
assumeFeatureEnabled(ACCOUNT_API);
List<SessionRepresentation> sessions = SimpleHttp.doGet(getAccountUrl("sessions"), client).auth(tokenUtil.getToken()).asJson(new TypeReference<List<SessionRepresentation>>() {});
@ -243,14 +243,14 @@ public class AccountRestServiceTest extends AbstractTestRealmKeycloakTest {
@Test
public void testGetPasswordDetails() throws IOException {
assumeFeatureEnabled(ACCOUNT2);
assumeFeatureEnabled(ACCOUNT_API);
getPasswordDetails();
}
@Test
public void testPostPasswordUpdate() throws IOException {
assumeFeatureEnabled(ACCOUNT2);
assumeFeatureEnabled(ACCOUNT_API);
//Get the time of lastUpdate
AccountCredentialResource.PasswordDetails initialDetails = getPasswordDetails();
@ -275,7 +275,7 @@ public class AccountRestServiceTest extends AbstractTestRealmKeycloakTest {
@Test
public void testPasswordConfirmation() throws IOException {
assumeFeatureEnabled(ACCOUNT2);
assumeFeatureEnabled(ACCOUNT_API);
updatePassword("password", "Str0ng3rP4ssw0rd", "confirmationDoesNotMatch", 400);
@ -318,7 +318,7 @@ public class AccountRestServiceTest extends AbstractTestRealmKeycloakTest {
@Test
public void testDeleteSession() throws IOException {
assumeFeatureEnabled(ACCOUNT2);
assumeFeatureEnabled(ACCOUNT_API);
TokenUtil viewToken = new TokenUtil("view-account-access", "password");
String sessionId = oauth.doLogin("view-account-access", "password").getSessionState();

View file

@ -26,6 +26,7 @@ import org.junit.Rule;
import org.junit.Test;
import org.keycloak.authentication.authenticators.browser.ScriptBasedAuthenticatorFactory;
import org.keycloak.authentication.authenticators.browser.UsernamePasswordFormFactory;
import org.keycloak.common.Profile;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventType;
@ -64,6 +65,11 @@ public class ScriptAuthenticatorTest extends AbstractFlowTest {
public static final String EXECUTION_ID = "scriptAuth";
@BeforeClass
public static void verifyEnvironment() {
ProfileAssume.assumeFeatureEnabled(Profile.Feature.SCRIPTS);
}
@Override
public void configureTestRealm(RealmRepresentation testRealm) {

View file

@ -1121,11 +1121,17 @@ include-representation.tooltip=Include JSON representation for create and update
clear-admin-events.tooltip=Deletes all admin events in the database.
server-version=Server Version
server-profile=Server Profile
server-disabled=Server Disabled Features
server-disabled=Disabled Features
server-disabled.tooltip=Features that are not currently enabled. Some features are not enabled by default. This applies to all preview and experimental features.
server-preview=Preview Features
server-preview.tooltip=Preview features are not supported in production use and may be significantly changed or removed in the future.
server-experimental=Experimental Features
server-experimental.tooltip=Experimental features are experimental features that may not be fully function. Never use experimental features in production.
info=Info
providers=Providers
server-time=Server Time
server-uptime=Server Uptime
profile=Profile
memory=Memory
total-memory=Total Memory
free-memory=Free Memory

View file

@ -14,14 +14,6 @@
<td width="20%">{{:: 'server-version' | translate}}</td>
<td>{{serverInfo.systemInfo.version}}</td>
</tr>
<tr>
<td width="20%">{{:: 'server-profile' | translate}}</td>
<td>{{serverInfo.profileInfo.name | capitalize}}</td>
</tr>
<tr data-ng-if="serverInfo.profileInfo.disabledFeatures.length > 0">
<td width="20%">{{:: 'server-disabled' | translate}}</td>
<td>{{serverInfo.profileInfo.disabledFeatures.join(', ').toLowerCase() | capitalize}}</td>
</tr>
<tr>
<td>{{:: 'server-time' | translate}}</td>
<td>{{serverInfo.systemInfo.serverTime}}</td>
@ -32,6 +24,37 @@
</tr>
</table>
<table class="table table-striped table-bordered">
<legend>{{:: 'profile' | translate}}</legend>
<tr>
<td width="20%">{{:: 'server-profile' | translate}}</td>
<td>{{serverInfo.profileInfo.name | capitalize}}</td>
</tr>
<tr data-ng-if="serverInfo.profileInfo.disabledFeatures.length > 0">
<td width="20%">
<span>{{:: 'server-disabled' | translate}}</span>
<kc-tooltip>{{:: 'server-disabled.tooltip' | translate}}</kc-tooltip>
</td>
<td>{{serverInfo.profileInfo.disabledFeatures.sort().join(', ')}}</td>
</tr>
<tr data-ng-if="serverInfo.profileInfo.previewFeatures.length > 0">
<td width="20%">
<span>{{:: 'server-preview' | translate}}</span>
<kc-tooltip>{{:: 'server-preview.tooltip' | translate}}</kc-tooltip>
</td>
<td>{{serverInfo.profileInfo.previewFeatures.sort().join(', ')}}</td>
</tr>
<tr data-ng-if="serverInfo.profileInfo.experimentalFeatures.length > 0">
<td width="20%">
<span>{{:: 'server-experimental' | translate}}</span>
<kc-tooltip>{{:: 'server-experimental.tooltip' | translate}}</kc-tooltip>
</td>
<td>{{serverInfo.profileInfo.experimentalFeatures.sort().join(', ')}}</td>
</tr>
</table>
<fieldset>
<legend>{{:: 'memory' | translate}}</legend>
<table class="table table-striped table-bordered" style="margin-top: 0;">