Merge pull request #3676 from stianst/KEYCLOAK-4109
KEYCLOAK-4109 Ability to disable impersonation
This commit is contained in:
commit
f6323d94ec
14 changed files with 114 additions and 54 deletions
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -153,6 +153,6 @@ public class ScriptBasedAuthenticatorFactory implements AuthenticatorFactory, En
|
|||
|
||||
@Override
|
||||
public boolean isSupported() {
|
||||
return Profile.isPreviewEnabled();
|
||||
return Profile.isFeatureEnabled(Profile.Feature.SCRIPTS);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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.");
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
Loading…
Reference in a new issue