diff --git a/events/api/src/main/java/org/keycloak/events/Details.java b/events/api/src/main/java/org/keycloak/events/Details.java
index 73d98ffb70..38ff34078a 100755
--- a/events/api/src/main/java/org/keycloak/events/Details.java
+++ b/events/api/src/main/java/org/keycloak/events/Details.java
@@ -32,5 +32,7 @@ public interface Details {
String CONSENT_VALUE_NO_CONSENT_REQUIRED = "no_consent_required"; // No consent is required by client
String CONSENT_VALUE_CONSENT_GRANTED = "consent_granted"; // Consent granted by user
String CONSENT_VALUE_PERSISTED_CONSENT = "persistent_consent"; // Persistent consent used (was already granted by user before)
+ String IMPERSONATOR_REALM = "impersonator_realm";
+ String IMPERSONATOR = "impersonator";
}
diff --git a/events/api/src/main/java/org/keycloak/events/EventType.java b/events/api/src/main/java/org/keycloak/events/EventType.java
index dc40379d82..be72f0623b 100755
--- a/events/api/src/main/java/org/keycloak/events/EventType.java
+++ b/events/api/src/main/java/org/keycloak/events/EventType.java
@@ -62,7 +62,8 @@ public enum EventType {
IDENTITY_PROVIDER_RETRIEVE_TOKEN(false),
IDENTITY_PROVIDER_RETRIEVE_TOKEN_ERROR(false),
IDENTITY_PROVIDER_ACCCOUNT_LINKING(false),
- IDENTITY_PROVIDER_ACCCOUNT_LINKING_ERROR(false);
+ IDENTITY_PROVIDER_ACCCOUNT_LINKING_ERROR(false),
+ IMPERSONATE(true);
private boolean saveByDefault;
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-detail.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-detail.html
index 10a2e720d2..270865f12e 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-detail.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-detail.html
@@ -119,7 +119,7 @@
-
+
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-list.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-list.html
index dc2c4ac7e7..a846fe8b5b 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-list.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-list.html
@@ -50,7 +50,7 @@
{{user.lastName}} |
{{user.firstName}} |
{{user.email}} |
- |
+ |
Please enter a search, or click on view all users |
diff --git a/model/api/src/main/java/org/keycloak/models/ImpersonationConstants.java b/model/api/src/main/java/org/keycloak/models/ImpersonationConstants.java
index d051f0180b..a5cb38500e 100755
--- a/model/api/src/main/java/org/keycloak/models/ImpersonationConstants.java
+++ b/model/api/src/main/java/org/keycloak/models/ImpersonationConstants.java
@@ -10,6 +10,7 @@ import org.keycloak.models.utils.KeycloakModelUtils;
public class ImpersonationConstants {
public static String IMPERSONATION_ROLE = "impersonation";
+
public static void setupMasterRealmRole(RealmProvider model, RealmModel realm) {
RealmModel adminRealm;
RoleModel adminRole;
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 e84d5ae0ce..43f50e1135 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
@@ -8,6 +8,9 @@ import org.keycloak.ClientConnection;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.email.EmailException;
import org.keycloak.email.EmailProvider;
+import org.keycloak.events.Details;
+import org.keycloak.events.EventBuilder;
+import org.keycloak.events.EventType;
import org.keycloak.events.admin.OperationType;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
@@ -293,12 +296,20 @@ public class UsersResource {
AuthenticationManager.expireRememberMeCookie(realm, uriInfo, clientConnection);
AuthenticationManager.backchannelLogout(session, authenticatedRealm, userSession, uriInfo, clientConnection, headers, true);
}
+ EventBuilder event = new EventBuilder(realm, session, clientConnection);
+
UserSessionModel userSession = session.sessions().createUserSession(realm, user, user.getUsername(), clientConnection.getRemoteAddr(), "impersonate", false, null, null);
AuthenticationManager.createLoginCookie(realm, userSession.getUser(), userSession, uriInfo, clientConnection);
URI redirect = AccountService.accountServiceApplicationPage(uriInfo).build(realm.getName());
Map result = new HashMap<>();
result.put("sameRealm", sameRealm);
result.put("redirect", redirect.toString());
+ event.event(EventType.IMPERSONATE)
+ .session(userSession)
+ .user(user)
+ .detail(Details.IMPERSONATOR_REALM,authenticatedRealm.getName())
+ .detail(Details.IMPERSONATOR, auth.getAuth().getUser().getUsername()).success();
+
return result;
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/ImpersonationTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/ImpersonationTest.java
new file mode 100755
index 0000000000..a73d9cebc2
--- /dev/null
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/ImpersonationTest.java
@@ -0,0 +1,212 @@
+/*
+ * JBoss, Home of Professional Open Source.
+ * Copyright 2012, Red Hat, Inc., and individual contributors
+ * as indicated by the @author tags. See the copyright.txt file in the
+ * distribution for a full listing of individual contributors.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.keycloak.testsuite.admin;
+
+import org.junit.Assert;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.Config;
+import org.keycloak.events.Details;
+import org.keycloak.events.EventType;
+import org.keycloak.models.AdminRoles;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientSessionModel;
+import org.keycloak.models.Constants;
+import org.keycloak.models.ImpersonationConstants;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.protocol.oidc.TokenManager;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.services.managers.RealmManager;
+import org.keycloak.services.resources.admin.AdminRoot;
+import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.rule.KeycloakRule;
+
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.ClientBuilder;
+import javax.ws.rs.client.ClientRequestContext;
+import javax.ws.rs.client.ClientRequestFilter;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.GenericType;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import java.io.IOException;
+import java.util.Map;
+
+/**
+ * Tests Undertow Adapter
+ *
+ * @author Bill Burke
+ */
+public class ImpersonationTest {
+
+ static String impersonatedUserId;
+
+ @ClassRule
+ public static KeycloakRule keycloakRule = new KeycloakRule( new KeycloakRule.KeycloakSetup() {
+
+ @Override
+ public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+ impersonatedUserId = manager.getSession().users().getUserByUsername("test-user@localhost", appRealm).getId();
+ {
+ UserModel masterImpersonator = manager.getSession().users().addUser(adminstrationRealm, "master-impersonator");
+ masterImpersonator.setEnabled(true);
+ ClientModel adminRealmClient = adminstrationRealm.getClientByClientId(KeycloakModelUtils.getMasterRealmAdminApplicationClientId(appRealm));
+ RoleModel masterImpersonatorRole = adminRealmClient.getRole(ImpersonationConstants.IMPERSONATION_ROLE);
+ masterImpersonator.grantRole(masterImpersonatorRole);
+ }
+ {
+ UserModel masterBadImpersonator = manager.getSession().users().addUser(adminstrationRealm, "master-bad-impersonator");
+ masterBadImpersonator.setEnabled(true);
+ }
+
+ {
+ UserModel impersonator = manager.getSession().users().addUser(appRealm, "impersonator");
+ impersonator.setEnabled(true);
+ ClientModel appRealmClient = appRealm.getClientByClientId(Constants.REALM_MANAGEMENT_CLIENT_ID);
+ RoleModel realmImpersonatorRole = appRealmClient.getRole(ImpersonationConstants.IMPERSONATION_ROLE);
+ impersonator.grantRole(realmImpersonatorRole);
+ }
+ {
+ UserModel impersonator = manager.getSession().users().addUser(appRealm, "realm-admin");
+ impersonator.setEnabled(true);
+ ClientModel appRealmClient = appRealm.getClientByClientId(Constants.REALM_MANAGEMENT_CLIENT_ID);
+ RoleModel realmImpersonatorRole = appRealmClient.getRole(AdminRoles.REALM_ADMIN);
+ impersonator.grantRole(realmImpersonatorRole);
+ }
+ {
+ UserModel badimpersonator = manager.getSession().users().addUser(appRealm, "bad-impersonator");
+ badimpersonator.setEnabled(true);
+ }
+ }
+ });
+
+ @Rule
+ public AssertEvents events = new AssertEvents(keycloakRule);
+
+
+ private static String createAdminToken(String username, String realm) {
+ KeycloakSession session = keycloakRule.startSession();
+ try {
+ RealmManager manager = new RealmManager(session);
+
+ RealmModel adminRealm = manager.getRealm(realm);
+ ClientModel adminConsole = adminRealm.getClientByClientId(Constants.ADMIN_CONSOLE_CLIENT_ID);
+ TokenManager tm = new TokenManager();
+ UserModel admin = session.users().getUserByUsername(username, adminRealm);
+ ClientSessionModel clientSession = session.sessions().createClientSession(adminRealm, adminConsole);
+ clientSession.setNote(OIDCLoginProtocol.ISSUER, "http://localhost:8081/auth/realms/" + realm);
+ UserSessionModel userSession = session.sessions().createUserSession(adminRealm, admin, "admin", null, "form", false, null, null);
+ AccessToken token = tm.createClientAccessToken(session, tm.getAccess(null, adminConsole, admin), adminRealm, adminConsole, admin, userSession, clientSession);
+ return tm.encodeToken(adminRealm, token);
+ } finally {
+ keycloakRule.stopSession(session, true);
+ }
+ }
+
+ @Test
+ public void testImpersonateByMasterAdmin() {
+ // test that composite is set up right for impersonation role
+ testSuccessfulImpersonation("admin", Config.getAdminRealm());
+ }
+
+ @Test
+ public void testImpersonateByMasterImpersonator() {
+ testSuccessfulImpersonation("master-impersonator", Config.getAdminRealm());
+ }
+
+ @Test
+ public void testImpersonateByTestImpersonator() {
+ testSuccessfulImpersonation("impersonator", "test");
+ }
+
+ @Test
+ public void testImpersonateByTestAdmin() {
+ // test that composite is set up right for impersonation role
+ testSuccessfulImpersonation("realm-admin", "test");
+ }
+
+ @Test
+ public void testImpersonateByTestBadImpersonator() {
+ testForbiddenImpersonation("bad-impersonator", "test");
+ }
+
+ @Test
+ public void testImpersonateByMastertBadImpersonator() {
+ testForbiddenImpersonation("master-bad-impersonator", Config.getAdminRealm());
+ }
+
+
+
+ protected void testSuccessfulImpersonation(String admin, String adminRealm) {
+ Client client = createClient(admin, adminRealm);
+ WebTarget impersonate = createImpersonateTarget(client);
+ Map data = impersonate.request().post(null, Map.class);
+ Assert.assertNotNull(data);
+ Assert.assertNotNull(data.get("redirect"));
+
+ events.expect(EventType.IMPERSONATE)
+ .session(AssertEvents.isUUID())
+ .user(impersonatedUserId)
+ .detail(Details.IMPERSONATOR, admin)
+ .detail(Details.IMPERSONATOR_REALM, adminRealm)
+ .client((String) null).assertEvent();
+
+ client.close();
+ }
+
+ protected void testForbiddenImpersonation(String admin, String adminRealm) {
+ Client client = createClient(admin, adminRealm);
+ WebTarget impersonate = createImpersonateTarget(client);
+ Response response = impersonate.request().post(null);
+ response.close();
+ client.close();
+ }
+
+
+ protected WebTarget createImpersonateTarget(Client client) {
+ UriBuilder authBase = UriBuilder.fromUri("http://localhost:8081/auth");
+ WebTarget adminRealms = client.target(AdminRoot.realmsUrl(authBase));
+ WebTarget realmTarget = adminRealms.path("test");
+ return realmTarget.path("users").path(impersonatedUserId).path("impersonation");
+ }
+
+ protected Client createClient(String admin, String adminRealm) {
+ String token = createAdminToken(admin, adminRealm);
+ final String authHeader = "Bearer " + token;
+ ClientRequestFilter authFilter = new ClientRequestFilter() {
+ @Override
+ public void filter(ClientRequestContext requestContext) throws IOException {
+ requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, authHeader);
+ }
+ };
+ return ClientBuilder.newBuilder().register(authFilter).build();
+ }
+
+}