KEYCLOAK-2339 Adding impersonator details to user session notes and supporting built-in protocol mappers.
This commit is contained in:
parent
231db059b2
commit
be77fd9459
8 changed files with 155 additions and 17 deletions
|
@ -0,0 +1,23 @@
|
|||
package org.keycloak.models;
|
||||
|
||||
/**
|
||||
* Session note metadata for impersonation details stored in user session notes.
|
||||
*/
|
||||
public enum ImpersonationSessionNote implements UserSessionNoteDescriptor {
|
||||
IMPERSONATOR_ID("Impersonator User ID"),
|
||||
IMPERSONATOR_USERNAME("Impersonator Username");
|
||||
|
||||
final String displayName;
|
||||
|
||||
ImpersonationSessionNote(String displayName) {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public String getTokenClaim() {
|
||||
return this.toString().toLowerCase().replace('_', '.');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package org.keycloak.models;
|
||||
|
||||
/**
|
||||
* Describes a user session note for simple and generic {@link ProtocolMapperModel} creation.
|
||||
*/
|
||||
public interface UserSessionNoteDescriptor {
|
||||
/**
|
||||
* @return A human-readable name for the session note. This should tell the end user what the session note contains
|
||||
*/
|
||||
String getDisplayName();
|
||||
|
||||
/**
|
||||
* @return Token claim name/path to store the user session note value in.
|
||||
*/
|
||||
String getTokenClaim();
|
||||
}
|
|
@ -50,6 +50,9 @@ import java.util.HashSet;
|
|||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_ID;
|
||||
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_USERNAME;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
|
@ -173,6 +176,9 @@ public class OIDCLoginProtocolFactory extends AbstractLoginProtocolFactory {
|
|||
|
||||
model = AllowedWebOriginsProtocolMapper.createClaimMapper(ALLOWED_WEB_ORIGINS);
|
||||
builtins.put(ALLOWED_WEB_ORIGINS, model);
|
||||
|
||||
builtins.put(IMPERSONATOR_ID.getDisplayName(), UserSessionNoteMapper.createUserSessionNoteMapper(IMPERSONATOR_ID));
|
||||
builtins.put(IMPERSONATOR_USERNAME.getDisplayName(), UserSessionNoteMapper.createUserSessionNoteMapper(IMPERSONATOR_USERNAME));
|
||||
}
|
||||
|
||||
private static void createUserAttributeMapper(String name, String attrName, String claimName, String type) {
|
||||
|
|
|
@ -29,8 +29,8 @@ import org.keycloak.authorization.authorization.AuthorizationTokenService;
|
|||
import org.keycloak.authorization.util.Tokens;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.broker.provider.ExchangeExternalToken;
|
||||
import org.keycloak.broker.provider.IdentityProvider;
|
||||
import org.keycloak.broker.provider.ExchangeTokenToIdentityProviderToken;
|
||||
import org.keycloak.broker.provider.IdentityProvider;
|
||||
import org.keycloak.broker.provider.IdentityProviderFactory;
|
||||
import org.keycloak.broker.provider.IdentityProviderMapper;
|
||||
import org.keycloak.common.ClientConnection;
|
||||
|
@ -45,8 +45,8 @@ import org.keycloak.events.EventBuilder;
|
|||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.jose.jws.JWSInputException;
|
||||
import org.keycloak.models.AuthenticationFlowModel;
|
||||
import org.keycloak.models.AuthenticatedClientSessionModel;
|
||||
import org.keycloak.models.AuthenticationFlowModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientScopeModel;
|
||||
import org.keycloak.models.ClientSessionContext;
|
||||
|
@ -99,6 +99,7 @@ import javax.ws.rs.core.MediaType;
|
|||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.Response.Status;
|
||||
import java.security.MessageDigest;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
@ -106,7 +107,9 @@ import java.util.Objects;
|
|||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.security.MessageDigest;
|
||||
|
||||
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_ID;
|
||||
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_USERNAME;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
|
@ -755,12 +758,16 @@ public class TokenEndpoint {
|
|||
}
|
||||
}
|
||||
|
||||
tokenUser = requestedUser;
|
||||
tokenSession = session.sessions().createUserSession(realm, requestedUser, requestedUser.getUsername(), clientConnection.getRemoteAddr(), "impersonate", false, null, null);
|
||||
if (tokenUser != null) {
|
||||
tokenSession.setNote(IMPERSONATOR_ID.toString(), tokenUser.getId());
|
||||
tokenSession.setNote(IMPERSONATOR_USERNAME.toString(), tokenUser.getUsername());
|
||||
}
|
||||
|
||||
tokenUser = requestedUser;
|
||||
}
|
||||
|
||||
String requestedIssuer = formParams.getFirst(OAuth2Constants.REQUESTED_ISSUER);
|
||||
|
||||
if (requestedIssuer == null) {
|
||||
return exchangeClientToClient(tokenUser, tokenSession);
|
||||
} else {
|
||||
|
@ -825,7 +832,6 @@ public class TokenEndpoint {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
if (targetClient.isConsentRequired()) {
|
||||
event.detail(Details.REASON, "audience requires consent");
|
||||
event.error(Errors.CONSENT_DENIED);
|
||||
|
@ -924,8 +930,6 @@ public class TokenEndpoint {
|
|||
userSession.setNote(IdentityProvider.FEDERATED_ACCESS_TOKEN, subjectToken);
|
||||
|
||||
return exchangeClientToClient(user, userSession);
|
||||
|
||||
|
||||
}
|
||||
|
||||
protected UserModel importUserFromExternalIdentity(BrokeredIdentityContext context) {
|
||||
|
|
|
@ -19,6 +19,7 @@ package org.keycloak.protocol.oidc.mappers;
|
|||
|
||||
import org.keycloak.models.ProtocolMapperModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.UserSessionNoteDescriptor;
|
||||
import org.keycloak.protocol.ProtocolMapperUtils;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
@ -101,4 +102,20 @@ public class UserSessionNoteMapper extends AbstractOIDCProtocolMapper implements
|
|||
mapper.setConfig(config);
|
||||
return mapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* For session notes defined using a {@link UserSessionNoteDescriptor} enum
|
||||
*
|
||||
* @param userSessionNoteDescriptor User session note descriptor for which to create a protocol mapper model.
|
||||
*/
|
||||
public static ProtocolMapperModel createUserSessionNoteMapper(UserSessionNoteDescriptor userSessionNoteDescriptor) {
|
||||
return UserSessionNoteMapper.createClaimMapper(
|
||||
userSessionNoteDescriptor.getDisplayName(),
|
||||
userSessionNoteDescriptor.toString(),
|
||||
userSessionNoteDescriptor.getTokenClaim(),
|
||||
"String",
|
||||
true, true
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -40,6 +40,7 @@ import org.keycloak.models.Constants;
|
|||
import org.keycloak.models.FederatedIdentityModel;
|
||||
import org.keycloak.models.GroupModel;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.ImpersonationSessionNote;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.ModelDuplicateException;
|
||||
import org.keycloak.models.ModelException;
|
||||
|
@ -105,6 +106,9 @@ import java.util.Properties;
|
|||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_ID;
|
||||
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_USERNAME;
|
||||
|
||||
/**
|
||||
* Base resource for managing users
|
||||
*
|
||||
|
@ -281,6 +285,13 @@ public class UserResource {
|
|||
EventBuilder event = new EventBuilder(realm, session, clientConnection);
|
||||
|
||||
UserSessionModel userSession = session.sessions().createUserSession(realm, user, user.getUsername(), clientConnection.getRemoteAddr(), "impersonate", false, null, null);
|
||||
|
||||
UserModel adminUser = auth.adminAuth().getUser();
|
||||
String impersonatorId = adminUser.getId();
|
||||
String impersonator = adminUser.getUsername();
|
||||
userSession.setNote(IMPERSONATOR_ID.toString(), impersonatorId);
|
||||
userSession.setNote(IMPERSONATOR_USERNAME.toString(), impersonator);
|
||||
|
||||
AuthenticationManager.createLoginCookie(session, realm, userSession.getUser(), userSession, session.getContext().getUri(), clientConnection);
|
||||
URI redirect = AccountFormService.accountServiceApplicationPage(session.getContext().getUri()).build(realm.getName());
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
@ -289,8 +300,8 @@ public class UserResource {
|
|||
event.event(EventType.IMPERSONATE)
|
||||
.session(userSession)
|
||||
.user(user)
|
||||
.detail(Details.IMPERSONATOR_REALM,authenticatedRealm.getName())
|
||||
.detail(Details.IMPERSONATOR, auth.adminAuth().getUser().getUsername()).success();
|
||||
.detail(Details.IMPERSONATOR_REALM, authenticatedRealm.getName())
|
||||
.detail(Details.IMPERSONATOR, impersonator).success();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -17,11 +17,15 @@
|
|||
|
||||
package org.keycloak.testsuite.admin;
|
||||
|
||||
import org.jboss.arquillian.container.test.api.Deployment;
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.jboss.resteasy.client.jaxrs.ResteasyClient;
|
||||
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
|
||||
import org.jboss.shrinkwrap.api.spec.WebArchive;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Assume;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.Config;
|
||||
|
@ -35,6 +39,10 @@ import org.keycloak.events.EventType;
|
|||
import org.keycloak.models.AdminRoles;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.ImpersonationConstants;
|
||||
import org.keycloak.models.ImpersonationSessionNote;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.representations.idm.EventRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
|
@ -46,24 +54,24 @@ import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
|
|||
import org.keycloak.testsuite.auth.page.AuthRealm;
|
||||
import org.keycloak.testsuite.pages.AppPage;
|
||||
import org.keycloak.testsuite.pages.LoginPage;
|
||||
import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
|
||||
import org.keycloak.testsuite.util.AdminClientUtil;
|
||||
import org.keycloak.testsuite.util.ClientBuilder;
|
||||
import org.keycloak.testsuite.util.CredentialBuilder;
|
||||
import org.keycloak.testsuite.util.OAuthClient;
|
||||
import org.keycloak.testsuite.util.RealmBuilder;
|
||||
import org.keycloak.testsuite.util.UserBuilder;
|
||||
import org.openqa.selenium.Cookie;
|
||||
|
||||
import javax.ws.rs.ClientErrorException;
|
||||
import javax.ws.rs.client.Client;
|
||||
import javax.ws.rs.core.HttpHeaders;
|
||||
import javax.ws.rs.core.NewCookie;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.junit.Assume;
|
||||
import org.junit.BeforeClass;
|
||||
import org.openqa.selenium.Cookie;
|
||||
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
|
||||
|
@ -74,6 +82,25 @@ import static org.hamcrest.Matchers.containsString;
|
|||
*/
|
||||
public class ImpersonationTest extends AbstractKeycloakTest {
|
||||
|
||||
static class UserSessionNotesHolder {
|
||||
private Map<String, String> notes = new HashMap<>();
|
||||
|
||||
public UserSessionNotesHolder() {
|
||||
}
|
||||
|
||||
public UserSessionNotesHolder(final Map<String, String> notes) {
|
||||
this.notes = notes;
|
||||
}
|
||||
|
||||
public void setNotes(final Map<String, String> notes) {
|
||||
this.notes = notes;
|
||||
}
|
||||
|
||||
public Map<String, String> getNotes() {
|
||||
return notes;
|
||||
}
|
||||
}
|
||||
|
||||
@Rule
|
||||
public AssertEvents events = new AssertEvents(this);
|
||||
|
||||
|
@ -85,6 +112,12 @@ public class ImpersonationTest extends AbstractKeycloakTest {
|
|||
|
||||
private String impersonatedUserId;
|
||||
|
||||
@Deployment
|
||||
public static WebArchive deploy() {
|
||||
return RunOnServerDeployment.create(ImpersonationTest.class, AbstractKeycloakTest.class, UserResource.class)
|
||||
.addPackages(true, "org.keycloak.testsuite", "org.keycloak.admin.client", "org.openqa.selenium");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addTestRealms(List<RealmRepresentation> testRealms) {
|
||||
RealmBuilder realm = RealmBuilder.create().name("test").testEventListener();
|
||||
|
@ -234,8 +267,21 @@ public class ImpersonationTest extends AbstractKeycloakTest {
|
|||
.detail(Details.IMPERSONATOR_REALM, adminRealm)
|
||||
.client((String) null).assertEvent();
|
||||
|
||||
NewCookie cookie = response.getCookies().get(AuthenticationManager.KEYCLOAK_IDENTITY_COOKIE);
|
||||
// Fetch user session notes
|
||||
final String userId = impersonatedUserId;
|
||||
final UserSessionNotesHolder notesHolder = testingClient.server("test").fetch(session -> {
|
||||
final RealmModel realm = session.realms().getRealmByName("test");
|
||||
final UserModel user = session.users().getUserById(userId, realm);
|
||||
final UserSessionModel userSession = session.sessions().getUserSessions(realm, user).get(0);
|
||||
return new UserSessionNotesHolder(userSession.getNotes());
|
||||
}, UserSessionNotesHolder.class);
|
||||
|
||||
// Check impersonation details
|
||||
final Map<String, String> notes = notesHolder.getNotes();
|
||||
Assert.assertNotNull(notes.get(ImpersonationSessionNote.IMPERSONATOR_ID.toString()));
|
||||
Assert.assertEquals(admin, notes.get(ImpersonationSessionNote.IMPERSONATOR_USERNAME.toString()));
|
||||
|
||||
NewCookie cookie = response.getCookies().get(AuthenticationManager.KEYCLOAK_IDENTITY_COOKIE);
|
||||
Assert.assertNotNull(cookie);
|
||||
|
||||
return new Cookie(cookie.getName(), cookie.getValue(), cookie.getDomain(), cookie.getPath(), cookie.getExpiry(), cookie.isSecure(), cookie.isHttpOnly());
|
||||
|
|
|
@ -34,6 +34,8 @@ import org.keycloak.models.RoleModel;
|
|||
import org.keycloak.models.UserCredentialModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
|
||||
import org.keycloak.protocol.oidc.mappers.UserSessionNoteMapper;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.AccessTokenResponse;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
|
@ -57,8 +59,12 @@ import javax.ws.rs.core.Form;
|
|||
import javax.ws.rs.core.HttpHeaders;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.hamcrest.Matchers.instanceOf;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_ID;
|
||||
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_USERNAME;
|
||||
import static org.keycloak.testsuite.auth.page.AuthRealm.TEST;
|
||||
|
||||
/**
|
||||
|
@ -110,6 +116,8 @@ public class ClientTokenExchangeTest extends AbstractKeycloakTest {
|
|||
clientExchanger.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||
clientExchanger.setFullScopeAllowed(false);
|
||||
clientExchanger.addScopeMapping(impersonateRole);
|
||||
clientExchanger.addProtocolMapper(UserSessionNoteMapper.createUserSessionNoteMapper(IMPERSONATOR_ID));
|
||||
clientExchanger.addProtocolMapper(UserSessionNoteMapper.createUserSessionNoteMapper(IMPERSONATOR_USERNAME));
|
||||
|
||||
|
||||
ClientModel directExchanger = realm.addClient("direct-exchanger");
|
||||
|
@ -302,8 +310,15 @@ public class ClientTokenExchangeTest extends AbstractKeycloakTest {
|
|||
AccessToken exchangedToken = verifier.parse().getToken();
|
||||
Assert.assertEquals("client-exchanger", exchangedToken.getIssuedFor());
|
||||
Assert.assertNull(exchangedToken.getAudience());
|
||||
Assert.assertEquals(exchangedToken.getPreferredUsername(), "impersonated-user");
|
||||
Assert.assertEquals("impersonated-user", exchangedToken.getPreferredUsername());
|
||||
Assert.assertNull(exchangedToken.getRealmAccess());
|
||||
|
||||
Object impersonatorRaw = exchangedToken.getOtherClaims().get("impersonator");
|
||||
Assert.assertThat(impersonatorRaw, instanceOf(Map.class));
|
||||
Map impersonatorClaim = (Map) impersonatorRaw;
|
||||
|
||||
Assert.assertEquals(token.getSubject(), impersonatorClaim.get("id"));
|
||||
Assert.assertEquals("user", impersonatorClaim.get("username"));
|
||||
}
|
||||
|
||||
// client-exchanger can impersonate from token "user" to user "impersonated-user" and to "target" client
|
||||
|
@ -449,7 +464,7 @@ public class ClientTokenExchangeTest extends AbstractKeycloakTest {
|
|||
.param(OAuth2Constants.AUDIENCE, "target")
|
||||
|
||||
));
|
||||
org.junit.Assert.assertEquals(403, response.getStatus());
|
||||
Assert.assertEquals(403, response.getStatus());
|
||||
response.close();
|
||||
}
|
||||
|
||||
|
@ -464,7 +479,7 @@ public class ClientTokenExchangeTest extends AbstractKeycloakTest {
|
|||
.param(OAuth2Constants.AUDIENCE, "target")
|
||||
|
||||
));
|
||||
org.junit.Assert.assertTrue(response.getStatus() >= 400);
|
||||
Assert.assertTrue(response.getStatus() >= 400);
|
||||
response.close();
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue