Merge pull request #3676 from stianst/KEYCLOAK-4109

KEYCLOAK-4109 Ability to disable impersonation
This commit is contained in:
Stian Thorgersen 2016-12-20 09:35:03 +01:00 committed by GitHub
commit f6323d94ec
14 changed files with 114 additions and 54 deletions

View file

@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -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<Feature> 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<Feature> disabledFeatures = new HashSet<>();
private Profile() {
try {
profile = System.getProperty("keycloak.profile");
if (profile == null) {
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()) {
Properties props = new Properties();
props.load(new FileInputStream(file));
profile = props.getProperty("profile");
}
}
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<Feature> getDisabledFeatures() {
return CURRENT.disabledFeatures;
}
public static boolean isFeatureEnabled(Feature feature) {
return !CURRENT.disabledFeatures.contains(feature);
}
}

View file

@ -19,18 +19,26 @@ package org.keycloak.representations.info;
import org.keycloak.common.Profile;
import java.util.LinkedList;
import java.util.List;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class ProfileInfoRepresentation {
private String name;
private boolean previewEnabled;
private List<String> 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<String> getDisabledFeatures() {
return disabledFeatures;
}
}

View file

@ -153,6 +153,6 @@ public class ScriptBasedAuthenticatorFactory implements AuthenticatorFactory, En
@Override
public boolean isSupported() {
return Profile.isPreviewEnabled();
return Profile.isFeatureEnabled(Profile.Feature.SCRIPTS);
}
}

View file

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

View file

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

View file

@ -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<String, Object> impersonate(final @PathParam("id") String id) {
ProfileHelper.requireFeature(Profile.Feature.IMPERSONATION);
auth.init(RealmAuth.Resource.IMPERSONATION);
auth.requireManage();

View file

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

View file

@ -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"));
}
}

View file

@ -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 <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
*/
@ -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.");

View file

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

View file

@ -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

View file

@ -110,7 +110,7 @@
<input ng-model="client.serviceAccountsEnabled" name="serviceAccountsEnabled" id="serviceAccountsEnabled" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
</div>
</div>
<div class="form-group" data-ng-show="serverInfo.profileInfo.previewEnabled && protocol == 'openid-connect'">
<div class="form-group" data-ng-show="serverInfo.profileInfo.disabledFeatures.indexOf('AUTHORIZATION') == -1 && protocol == 'openid-connect'">
<label class="col-md-2 control-label" for="authorizationServicesEnabled">{{:: 'authz-authorization-services-enabled' | translate}}</label>
<kc-tooltip>{{:: 'authz-authorization-services-enabled.tooltip' | translate}}</kc-tooltip>
<div class="col-md-6">

View file

@ -16,7 +16,11 @@
</tr>
<tr>
<td width="20%">{{:: 'server-profile' | translate}}</td>
<td>{{serverInfo.profileInfo.name}}</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>

View file

@ -31,7 +31,7 @@
<th class="w-15">{{:: 'email' | translate}}</th>
<th class="w-15">{{:: 'last-name' | translate}}</th>
<th class="w-15">{{:: 'first-name' | translate}}</th>
<th colspan="{{access.impersonation == true ? '3' : '2'}}">{{:: 'actions' | translate}}</th>
<th colspan="{{serverInfo.profileInfo.disabledFeatures.indexOf('IMPERSONATION') == -1 && access.impersonation == true ? '3' : '2'}}">{{:: 'actions' | translate}}</th>
</tr>
</thead>
<tfoot data-ng-show="users && (users.length >= query.max || query.first > 0)">
@ -53,7 +53,7 @@
<td class="clip">{{user.lastName}}</td>
<td class="clip">{{user.firstName}}</td>
<td class="kc-action-cell" kc-open="/realms/{{realm.realm}}/users/{{user.id}}">{{:: 'edit' | translate}}</td>
<td data-ng-show="access.impersonation" class="kc-action-cell" data-ng-click="impersonate(user.id)">{{:: 'impersonate' | translate}}</td>
<td data-ng-show="serverInfo.profileInfo.disabledFeatures.indexOf('IMPERSONATION') == -1 && access.impersonation" class="kc-action-cell" data-ng-click="impersonate(user.id)">{{:: 'impersonate' | translate}}</td>
<td data-ng-show="access.manageUsers" class="kc-action-cell" data-ng-click="removeUser(user)">{{:: 'delete' | translate}}</td>
</tr>
<tr data-ng-show="!users || users.length == 0">