KEYCLOAK-2339 Adding impersonator details to user session notes and supporting built-in protocol mappers.

This commit is contained in:
Corey McGregor 2018-03-05 08:43:22 +10:00 committed by Marek Posolda
parent 231db059b2
commit be77fd9459
8 changed files with 155 additions and 17 deletions

View file

@ -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('_', '.');
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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