diff --git a/common/src/main/java/org/keycloak/common/Profile.java b/common/src/main/java/org/keycloak/common/Profile.java index ac16874f22..91b0a806cc 100755 --- a/common/src/main/java/org/keycloak/common/Profile.java +++ b/common/src/main/java/org/keycloak/common/Profile.java @@ -19,7 +19,14 @@ package org.keycloak.common; import java.io.File; import java.io.FileInputStream; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; import java.util.Properties; +import java.util.Set; /** * @author Bill Burke @@ -27,43 +34,87 @@ import java.util.Properties; */ public class Profile { - private enum ProfileValue { - PRODUCT, PREVIEW, COMMUNITY + public enum Feature { + AUTHORIZATION, IMPERSONATION, SCRIPTS } - private static ProfileValue value = load(); + private enum ProfileValue { + PRODUCT(Feature.AUTHORIZATION, Feature.SCRIPTS), + PREVIEW, + COMMUNITY; - static ProfileValue load() { - String profile = null; + private List disabled; + + ProfileValue() { + this.disabled = Collections.emptyList(); + } + + ProfileValue(Feature... disabled) { + this.disabled = Arrays.asList(disabled); + } + } + + private static final Profile CURRENT = new Profile(); + + private final ProfileValue profile; + + private final Set disabledFeatures = new HashSet<>(); + + private Profile() { try { - profile = System.getProperty("keycloak.profile"); - if (profile == null) { - String jbossServerConfigDir = System.getProperty("jboss.server.config.dir"); - if (jbossServerConfigDir != null) { - File file = new File(jbossServerConfigDir, "profile.properties"); - if (file.isFile()) { - Properties props = new Properties(); - props.load(new FileInputStream(file)); - profile = props.getProperty("profile"); + Properties props = new Properties(); + + 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); + + 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")) { + disabledFeatures.add(f); } } } } catch (Exception e) { - } - - if (profile == null) { - return ProfileValue.valueOf(Version.DEFAULT_PROFILE.toUpperCase()); - } else { - return ProfileValue.valueOf(profile.toUpperCase()); + throw new RuntimeException(e); } } public static String getName() { - return value.name().toLowerCase(); + return CURRENT.profile.name().toLowerCase(); } - public static boolean isPreviewEnabled() { - return value.ordinal() >= ProfileValue.PREVIEW.ordinal(); + public static Set getDisabledFeatures() { + return CURRENT.disabledFeatures; + } + + public static boolean isFeatureEnabled(Feature feature) { + return !CURRENT.disabledFeatures.contains(feature); } } diff --git a/core/src/main/java/org/keycloak/representations/info/ProfileInfoRepresentation.java b/core/src/main/java/org/keycloak/representations/info/ProfileInfoRepresentation.java index 3c474d03ba..c0cbea83f4 100644 --- a/core/src/main/java/org/keycloak/representations/info/ProfileInfoRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/info/ProfileInfoRepresentation.java @@ -19,18 +19,26 @@ package org.keycloak.representations.info; import org.keycloak.common.Profile; +import java.util.LinkedList; +import java.util.List; + /** * @author Stian Thorgersen */ public class ProfileInfoRepresentation { private String name; - private boolean previewEnabled; + private List disabledFeatures; public static ProfileInfoRepresentation create() { ProfileInfoRepresentation info = new ProfileInfoRepresentation(); - info.setName(Profile.getName()); - info.setPreviewEnabled(Profile.isPreviewEnabled()); + + info.name = Profile.getName(); + info.disabledFeatures = new LinkedList<>(); + for (Profile.Feature f : Profile.getDisabledFeatures()) { + info.disabledFeatures.add(f.name()); + } + return info; } @@ -38,16 +46,8 @@ public class ProfileInfoRepresentation { return name; } - public void setName(String name) { - this.name = name; - } - - public boolean isPreviewEnabled() { - return previewEnabled; - } - - public void setPreviewEnabled(boolean previewEnabled) { - this.previewEnabled = previewEnabled; + public List getDisabledFeatures() { + return disabledFeatures; } } diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticatorFactory.java index b2e8b9872d..4a1003c8d4 100644 --- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticatorFactory.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/ScriptBasedAuthenticatorFactory.java @@ -153,6 +153,6 @@ public class ScriptBasedAuthenticatorFactory implements AuthenticatorFactory, En @Override public boolean isSupported() { - return Profile.isPreviewEnabled(); + return Profile.isFeatureEnabled(Profile.Feature.SCRIPTS); } } diff --git a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java index 3ee7938cef..150804232f 100755 --- a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java +++ b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java @@ -22,6 +22,7 @@ import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.AuthorizationService; import org.keycloak.common.ClientConnection; +import org.keycloak.common.Profile; import org.keycloak.common.util.KeycloakUriBuilder; import org.keycloak.events.EventBuilder; import org.keycloak.models.ClientModel; @@ -264,7 +265,7 @@ public class RealmsResource { @Path("{realm}/authz") public Object getAuthorizationService(@PathParam("realm") String name) { - ProfileHelper.requirePreview(); + ProfileHelper.requireFeature(Profile.Feature.AUTHORIZATION); init(name); AuthorizationProvider authorization = this.session.getProvider(AuthorizationProvider.class); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java index 1275022646..c97a8f5578 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java @@ -164,7 +164,7 @@ public class ClientResource { RepresentationToModel.updateClient(rep, client); - if (Profile.isPreviewEnabled()) { + if (Profile.isFeatureEnabled(Profile.Feature.AUTHORIZATION)) { if (TRUE.equals(rep.getAuthorizationServicesEnabled())) { authorization().enable(); } else { @@ -190,7 +190,7 @@ public class ClientResource { ClientRepresentation representation = ModelToRepresentation.toRepresentation(client); - if (Profile.isPreviewEnabled()) { + if (Profile.isFeatureEnabled(Profile.Feature.AUTHORIZATION)) { representation.setAuthorizationServicesEnabled(authorization().isEnabled()); } @@ -577,7 +577,7 @@ public class ClientResource { @Path("/authz") public AuthorizationService authorization() { - ProfileHelper.requirePreview(); + ProfileHelper.requireFeature(Profile.Feature.AUTHORIZATION); AuthorizationService resource = new AuthorizationService(this.session, this.client, this.auth); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java index d1f1d3ab69..83265cbca4 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java @@ -23,6 +23,7 @@ import org.jboss.resteasy.spi.NotFoundException; import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.common.ClientConnection; +import org.keycloak.common.Profile; import org.keycloak.common.util.Time; import org.keycloak.credential.CredentialModel; import org.keycloak.email.EmailException; @@ -70,6 +71,7 @@ import org.keycloak.models.UserManager; import org.keycloak.services.managers.UserSessionManager; import org.keycloak.services.resources.AccountService; import org.keycloak.services.validation.Validation; +import org.keycloak.utils.ProfileHelper; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; @@ -319,6 +321,8 @@ public class UsersResource { @NoCache @Produces(MediaType.APPLICATION_JSON) public Map impersonate(final @PathParam("id") String id) { + ProfileHelper.requireFeature(Profile.Feature.IMPERSONATION); + auth.init(RealmAuth.Resource.IMPERSONATION); auth.requireManage(); diff --git a/services/src/main/java/org/keycloak/utils/ProfileHelper.java b/services/src/main/java/org/keycloak/utils/ProfileHelper.java index 719bd24e19..b1a29f4593 100644 --- a/services/src/main/java/org/keycloak/utils/ProfileHelper.java +++ b/services/src/main/java/org/keycloak/utils/ProfileHelper.java @@ -27,9 +27,9 @@ import javax.ws.rs.core.Response; */ public class ProfileHelper { - public static void requirePreview() { - if (!Profile.isPreviewEnabled()) { - throw new WebApplicationException("Feature not available in current profile", Response.Status.NOT_IMPLEMENTED); + public static void requireFeature(Profile.Feature feature) { + if (!Profile.isFeatureEnabled(feature)) { + throw new WebApplicationException("Feature not enabled", Response.Status.NOT_IMPLEMENTED); } } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/ProfileAssume.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/ProfileAssume.java index 7fbf59b068..9dd598f299 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/ProfileAssume.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/ProfileAssume.java @@ -26,11 +26,11 @@ import org.keycloak.common.Profile; public class ProfileAssume { public static void assumePreview() { - Assume.assumeTrue("Ignoring test as community/preview profile is not enabled", Profile.isPreviewEnabled()); + Assume.assumeTrue("Ignoring test as community/preview profile is not enabled", !Profile.getName().equals("product")); } public static void assumePreviewDisabled() { - Assume.assumeFalse("Ignoring test as community/preview profile is enabled", Profile.isPreviewEnabled()); + Assume.assumeFalse("Ignoring test as community/preview profile is enabled", !Profile.getName().equals("product")); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java index 18c1616f12..fc8b25bbe9 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java @@ -19,6 +19,7 @@ package org.keycloak.testsuite.admin.authentication; import org.junit.Test; import org.keycloak.authentication.authenticators.broker.IdpCreateUserIfUniqueAuthenticatorFactory; +import org.keycloak.common.Profile; import org.keycloak.representations.idm.AuthenticatorConfigInfoRepresentation; import org.keycloak.representations.idm.ConfigPropertyRepresentation; import org.keycloak.testsuite.Assert; @@ -32,8 +33,6 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; -import static org.keycloak.common.Profile.isPreviewEnabled; - /** * @author Marko Strukelj */ @@ -137,7 +136,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 (isPreviewEnabled()) { + if (Profile.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."); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java index 3e16c4b0d2..3c296a12cf 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java @@ -379,7 +379,7 @@ public class ExportImportUtil { Assert.assertNotNull(linked); Assert.assertEquals("my-service-user", linked.getUsername()); - if (Profile.isPreviewEnabled()) { + if (Profile.isFeatureEnabled(Profile.Feature.AUTHORIZATION)) { assertAuthorizationSettings(realmRsc); } } diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties index 5380f1725c..0106590b45 100644 --- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties +++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties @@ -898,6 +898,7 @@ 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 info=Info providers=Providers server-time=Server Time diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html index 2dc08ea388..af99e7dbd7 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/client-detail.html @@ -110,7 +110,7 @@ -
+
{{:: 'authz-authorization-services-enabled.tooltip' | translate}}
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/server-info.html b/themes/src/main/resources/theme/base/admin/resources/partials/server-info.html index 299a93b3c9..4a4b296d90 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/server-info.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/server-info.html @@ -16,7 +16,11 @@ {{:: 'server-profile' | translate}} - {{serverInfo.profileInfo.name}} + {{serverInfo.profileInfo.name | capitalize}} + + + {{:: 'server-disabled' | translate}} + {{serverInfo.profileInfo.disabledFeatures.join(', ').toLowerCase() | capitalize}} {{:: 'server-time' | translate}} diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/user-list.html b/themes/src/main/resources/theme/base/admin/resources/partials/user-list.html index 36510c57bb..4864acf736 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/user-list.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/user-list.html @@ -31,7 +31,7 @@ {{:: 'email' | translate}} {{:: 'last-name' | translate}} {{:: 'first-name' | translate}} - {{:: 'actions' | translate}} + {{:: 'actions' | translate}} @@ -53,7 +53,7 @@ {{user.lastName}} {{user.firstName}} {{:: 'edit' | translate}} - {{:: 'impersonate' | translate}} + {{:: 'impersonate' | translate}} {{:: 'delete' | translate}}