group token/assertion and tests

This commit is contained in:
Bill Burke 2015-11-18 09:36:47 -05:00
parent fa7a7d35a9
commit bff334d365
32 changed files with 1248 additions and 174 deletions

View file

@ -40,6 +40,8 @@ public class UserRepresentation {
@Deprecated
protected List<SocialLinkRepresentation> socialLinks;
protected List<String> groups;
public String getSelf() {
return self;
}
@ -216,4 +218,12 @@ public class UserRepresentation {
public void setServiceAccountClientId(String serviceAccountClientId) {
this.serviceAccountClientId = serviceAccountClientId;
}
public List<String> getGroups() {
return groups;
}
public void setGroups(List<String> groups) {
this.groups = groups;
}
}

View file

@ -7,6 +7,7 @@ import org.codehaus.jackson.JsonGenerator;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.SerializationConfig;
import org.keycloak.models.ClientModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleContainerModel;
@ -15,6 +16,7 @@ import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.UserConsentModel;
import org.keycloak.models.UserCredentialValueModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
@ -294,6 +296,12 @@ public class ExportUtils {
}
}
List<String> groups = new LinkedList<>();
for (GroupModel group : user.getGroups()) {
groups.add(ModelToRepresentation.buildGroupPath(group));
}
userRep.setGroups(groups);
return userRep;
}

View file

@ -95,7 +95,7 @@ module.controller('GroupCreateCtrl', function($scope, $route, realm, parentId, G
console.log('save!!!');
if (parentId == 'realm') {
console.log('realm')
Groups.save({realm: realm.realm, groupId: parentId}, $scope.group, function(data, headers) {
Groups.save({realm: realm.realm}, $scope.group, function(data, headers) {
var l = headers().location;

View file

@ -0,0 +1,80 @@
package org.keycloak.admin.client.resource;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface GroupResource {
/**
* Does not expand hierarchy. Subgroups will not be set.
*
* @return
*/
@GET
@NoCache
@Produces(MediaType.APPLICATION_JSON)
public GroupRepresentation toRepresentation();
/**
* Update group
*
* @param rep
*/
@PUT
@Consumes(MediaType.APPLICATION_JSON)
public void update(GroupRepresentation rep);
@DELETE
public void remove();
/**
* Set or create child. This will just set the parent if it exists. Create it and set the parent
* if the group doesn't exist.
*
* @param rep
*/
@POST
@Path("children")
@NoCache
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public Response subGroup(GroupRepresentation rep);
@Path("role-mappings")
public RoleMappingResource roles();
/**
* Get users
* <p/>
* Returns a list of users, filtered according to query parameters
*
* @param firstResult Pagination offset
* @param maxResults Pagination size
* @return
*/
@GET
@NoCache
@Path("/members")
@Produces(MediaType.APPLICATION_JSON)
public List<UserRepresentation> members(@QueryParam("first") Integer firstResult,
@QueryParam("max") Integer maxResults);
}

View file

@ -0,0 +1,39 @@
package org.keycloak.admin.client.resource;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.keycloak.representations.idm.GroupRepresentation;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.List;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface GroupsResource {
@GET
@NoCache
@Produces(MediaType.APPLICATION_JSON)
public List<GroupRepresentation> groups();
/**
* create or add a top level realm groupSet or create child. This will update the group and set the parent if it exists. Create it and set the parent
* if the group doesn't exist.
*
* @param rep
*/
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response add(GroupRepresentation rep);
@Path("{id}")
public GroupResource group(@PathParam("id") String id);
}

View file

@ -1,6 +1,8 @@
package org.keycloak.admin.client.resource;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import javax.ws.rs.*;
@ -36,6 +38,15 @@ public interface RealmResource {
@Path("roles")
RolesResource roles();
@Path("groups")
GroupsResource groups();
@GET
@Path("group-by-path/{path: .*}")
@NoCache
@Produces(MediaType.APPLICATION_JSON)
public GroupRepresentation getGroupByPath(@PathParam("path") String path);
@Path("identity-provider")
IdentityProvidersResource identityProviders();

View file

@ -2,6 +2,7 @@ package org.keycloak.admin.client.resource;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.FederatedIdentityRepresentation;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.idm.UserSessionRepresentation;
@ -34,6 +35,21 @@ public interface UserResource {
@DELETE
public void remove();
@Path("groups")
@GET
List<GroupRepresentation> groups();
@Path("groups/{groupId}")
@PUT
void joinGroup(@PathParam("groupId") String groupId);
@Path("groups/{groupId}")
@DELETE
void leaveGroup(@PathParam("groupId") String groupId);
@POST
@Path("logout")
public void logout();

View file

@ -330,6 +330,7 @@ public interface RealmModel extends RoleContainerModel {
void setDefaultLocale(String locale);
GroupModel createGroup(String name);
GroupModel createGroup(String id, String name);
/**
* Move Group to top realm level. Basically just sets group parent to null. You need to call this though

View file

@ -1,6 +1,7 @@
package org.keycloak.models.utils;
import org.bouncycastle.openssl.PEMWriter;
import org.jboss.resteasy.spi.NotFoundException;
import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
import org.keycloak.models.GroupModel;
@ -416,7 +417,7 @@ public final class KeycloakModelUtils {
public static List<String> resolveAttribute(GroupModel group, String name) {
List<String> values = group.getAttribute(name);
if (!values.isEmpty()) return values;
if (values != null && !values.isEmpty()) return values;
if (group.getParentId() == null) return null;
return resolveAttribute(group.getParent(), name);
@ -434,4 +435,54 @@ public final class KeycloakModelUtils {
}
private static GroupModel findSubGroup(String[] path, int index, GroupModel parent) {
for (GroupModel group : parent.getSubGroups()) {
if (group.getName().equals(path[index])) {
if (path.length == index + 1) {
return group;
}
else {
if (index + 1 < path.length) {
GroupModel found = findSubGroup(path, index + 1, group);
if (found != null) return found;
} else {
return null;
}
}
}
}
return null;
}
public static GroupModel findGroupByPath(RealmModel realm, String path) {
if (path == null) {
return null;
}
if (path.startsWith("/")) {
path = path.substring(1);
}
if (path.endsWith("/")) {
path = path.substring(0, path.length() - 1);
}
String[] split = path.split("/");
if (split.length == 0) return null;
GroupModel found = null;
for (GroupModel group : realm.getTopLevelGroups()) {
if (group.getName().equals(split[0])) {
if (split.length == 1) {
found = group;
break;
}
else {
if (split.length > 1) {
found = findSubGroup(split, 1, group);
if (found != null) break;
}
}
}
}
return found;
}
}

View file

@ -288,10 +288,16 @@ public class ModelToRepresentation {
if (internal) {
exportAuthenticationFlows(realm, rep);
exportRequiredActions(realm, rep);
exportGroups(realm, rep);
}
return rep;
}
public static void exportGroups(RealmModel realm, RealmRepresentation rep) {
List<GroupRepresentation> groups = toGroupHierarchy(realm, true);
rep.setGroups(groups);
}
public static void exportAuthenticationFlows(RealmModel realm, RealmRepresentation rep) {
rep.setAuthenticationFlows(new LinkedList<AuthenticationFlowRepresentation>());
rep.setAuthenticatorConfig(new LinkedList<AuthenticatorConfigRepresentation>());

View file

@ -12,6 +12,7 @@ import org.keycloak.models.BrowserSecurityHeaders;
import org.keycloak.models.ClaimMask;
import org.keycloak.models.ClientModel;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.IdentityProviderMapperModel;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
@ -36,6 +37,7 @@ import org.keycloak.representations.idm.ClaimRepresentation;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.FederatedIdentityRepresentation;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.IdentityProviderMapperRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.OAuthClientRepresentation;
@ -311,6 +313,11 @@ public class RepresentationToModel {
}
}
if (rep.getGroups() != null) {
importGroups(newRealm, rep);
}
// create users and their role mappings and social mappings
if (rep.getUsers() != null) {
@ -330,6 +337,59 @@ public class RepresentationToModel {
}
}
public static void importGroups(RealmModel realm, RealmRepresentation rep) {
List<GroupRepresentation> groups = rep.getGroups();
if (groups == null) return;
Map<String, ClientModel> clientMap = realm.getClientNameMap();
GroupModel parent = null;
for (GroupRepresentation group : groups) {
importGroup(realm, clientMap, parent, group);
}
}
public static void importGroup(RealmModel realm, Map<String, ClientModel> clientMap, GroupModel parent, GroupRepresentation group) {
GroupModel newGroup = realm.createGroup(group.getId(), group.getName());
if (group.getAttributes() != null) {
for (Map.Entry<String, List<String>> attr : group.getAttributes().entrySet()) {
newGroup.setAttribute(attr.getKey(), attr.getValue());
}
}
realm.moveGroup(newGroup, parent);
if (group.getRealmRoles() != null) {
for (String roleString : group.getRealmRoles()) {
RoleModel role = realm.getRole(roleString.trim());
if (role == null) {
role = realm.addRole(roleString.trim());
}
newGroup.grantRole(role);
}
}
if (group.getClientRoles() != null) {
for (Map.Entry<String, List<String>> entry : group.getClientRoles().entrySet()) {
ClientModel client = clientMap.get(entry.getKey());
if (client == null) {
throw new RuntimeException("Unable to find client role mappings for client: " + entry.getKey());
}
List<String> roleNames = entry.getValue();
for (String roleName : roleNames) {
RoleModel role = client.getRole(roleName.trim());
if (role == null) {
role = client.addRole(roleName.trim());
}
newGroup.grantRole(role);
}
}
}
if (group.getSubGroups() != null) {
for (GroupRepresentation subGroup : group.getSubGroups()) {
importGroup(realm, clientMap, newGroup, subGroup);
}
}
}
public static void importAuthenticationFlows(RealmModel newRealm, RealmRepresentation rep) {
if (rep.getAuthenticationFlows() == null) {
// assume this is an old version being imported
@ -999,6 +1059,16 @@ public class RepresentationToModel {
}
user.setServiceAccountClientLink(client.getId());;
}
if (userRep.getGroups() != null) {
for (String path : userRep.getGroups()) {
GroupModel group = KeycloakModelUtils.findGroupByPath(newRealm, path);
if (group == null) {
throw new RuntimeException("Unable to find group specified by path: " + path);
}
user.joinGroup(group);
}
}
return user;
}

View file

@ -1833,6 +1833,11 @@ public class RealmAdapter implements RealmModel {
return null;
}
@Override
public GroupModel createGroup(String id, String name) {
return null;
}
@Override
public void addTopLevelGroup(GroupModel subGroup) {

View file

@ -1307,6 +1307,12 @@ public class RealmAdapter implements RealmModel {
return updated.createGroup(name);
}
@Override
public GroupModel createGroup(String id, String name) {
getDelegateForUpdate();
return updated.createGroup(id, name);
}
@Override
public void addTopLevelGroup(GroupModel subGroup) {
getDelegateForUpdate();

View file

@ -321,7 +321,7 @@ public class UserAdapter implements UserModel {
public Set<GroupModel> getGroups() {
if (updated != null) return updated.getGroups();
Set<GroupModel> groups = new HashSet<GroupModel>();
for (String id : cached.getRoleMappings()) {
for (String id : cached.getGroups()) {
GroupModel groupModel = keycloakSession.realms().getGroupById(id, realm);
if (groupModel == null) {
// chance that role was removed, so just delete to persistence and get user invalidated

View file

@ -99,19 +99,20 @@ public class JpaRealmProvider implements RealmProvider {
RealmAdapter adapter = new RealmAdapter(session, em, realm);
session.users().preRemove(adapter);
for (ClientEntity a : new LinkedList<>(realm.getClients())) {
adapter.removeClient(a.getId());
}
int num = em.createNamedQuery("deleteGroupRoleMappingsByRealm")
.setParameter("realm", realm).executeUpdate();
num = em.createNamedQuery("deleteGroupAttributesByRealm")
.setParameter("realm", realm).executeUpdate();
num = em.createNamedQuery("deleteGroupsByRealm")
.setParameter("realm", realm).executeUpdate();
for (ClientEntity a : new LinkedList<>(realm.getClients())) {
adapter.removeClient(a.getId());
}
em.remove(realm);
em.flush();
em.clear();
return true;
}

View file

@ -174,10 +174,10 @@ public class JpaUserProvider implements UserProvider {
.setParameter("realmId", realm.getId()).executeUpdate();
num = em.createNamedQuery("deleteUserAttributesByRealm")
.setParameter("realmId", realm.getId()).executeUpdate();
num = em.createNamedQuery("deleteUsersByRealm")
.setParameter("realmId", realm.getId()).executeUpdate();
num = em.createNamedQuery("deleteUserGroupMembershipByRealm")
.setParameter("realmId", realm.getId()).executeUpdate();
num = em.createNamedQuery("deleteUsersByRealm")
.setParameter("realmId", realm.getId()).executeUpdate();
}
@Override

View file

@ -1965,7 +1965,7 @@ public class RealmAdapter implements RealmModel {
@Override
public List<GroupModel> getGroups() {
List<GroupModel> list = new LinkedList<>();
Collection<GroupEntity> groups = realm.getGroups();
Collection<GroupEntity> groups = em.createNamedQuery("getAllGroupsByRealm").setParameter("realm", realm).getResultList();
if (groups == null) return list;
for (GroupEntity entity : groups) {
list.add(new GroupAdapter(this, em, entity));
@ -2002,7 +2002,6 @@ public class RealmAdapter implements RealmModel {
session.users().preRemove(this, group);
moveGroup(group, null);
realm.getGroups().remove(groupEntity);
em.createNamedQuery("deleteGroupAttributesByGroup").setParameter("group", groupEntity).executeUpdate();
em.createNamedQuery("deleteGroupRoleMappingsByGroup").setParameter("group", groupEntity).executeUpdate();
em.remove(groupEntity);
@ -2013,8 +2012,15 @@ public class RealmAdapter implements RealmModel {
@Override
public GroupModel createGroup(String name) {
String id = KeycloakModelUtils.generateId();
return createGroup(id, name);
}
@Override
public GroupModel createGroup(String id, String name) {
if (id == null) id = KeycloakModelUtils.generateId();
GroupEntity groupEntity = new GroupEntity();
groupEntity.setId(KeycloakModelUtils.generateId());
groupEntity.setId(id);
groupEntity.setName(name);
groupEntity.setRealm(realm);
em.persist(groupEntity);

View file

@ -133,9 +133,6 @@ public class RealmEntity {
@OneToMany(fetch = FetchType.LAZY, cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "realm")
Collection<RoleEntity> roles = new ArrayList<RoleEntity>();
@OneToMany(fetch = FetchType.LAZY, cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "realm")
Collection<GroupEntity> groups = new ArrayList<GroupEntity>();
@ElementCollection
@MapKeyColumn(name="NAME")
@Column(name="VALUE")
@ -722,20 +719,5 @@ public class RealmEntity {
this.clientAuthenticationFlow = clientAuthenticationFlow;
}
public Collection<GroupEntity> getGroups() {
return groups;
}
public void setGroups(Collection<GroupEntity> groups) {
this.groups = groups;
}
public void addGroup(GroupEntity group) {
if (groups == null) {
groups = new ArrayList<GroupEntity>();
}
groups.add(group);
}
}

View file

@ -611,8 +611,15 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> impleme
@Override
public GroupModel createGroup(String name) {
String id = KeycloakModelUtils.generateId();
return createGroup(id, name);
}
@Override
public GroupModel createGroup(String id, String name) {
if (id == null) id = KeycloakModelUtils.generateId();
MongoGroupEntity group = new MongoGroupEntity();
group.setId(KeycloakModelUtils.generateId());
group.setId(id);
group.setName(name);
group.setRealmId(getId());

View file

@ -0,0 +1,157 @@
package org.keycloak.protocol.saml.mappers;
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
import org.keycloak.dom.saml.v2.assertion.AttributeType;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.protocol.ProtocolMapper;
import org.keycloak.protocol.saml.SamlProtocol;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.services.managers.ClientSessionCode;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class GroupMembershipMapper extends AbstractSAMLProtocolMapper implements SAMLAttributeStatementMapper {
public static final String PROVIDER_ID = "saml-group-membership-mapper";
public static final String SINGLE_GROUP_ATTRIBUTE = "single";
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
static {
ProviderConfigProperty property;
property = new ProviderConfigProperty();
property.setName(AttributeStatementHelper.SAML_ATTRIBUTE_NAME);
property.setLabel("Group attribute name");
property.setDefaultValue("member");
property.setHelpText("Name of the SAML attribute you want to put your groups into. i.e. 'member', 'memberOf'.");
configProperties.add(property);
property = new ProviderConfigProperty();
property.setName(AttributeStatementHelper.FRIENDLY_NAME);
property.setLabel(AttributeStatementHelper.FRIENDLY_NAME_LABEL);
property.setHelpText(AttributeStatementHelper.FRIENDLY_NAME_HELP_TEXT);
configProperties.add(property);
property = new ProviderConfigProperty();
property.setName(AttributeStatementHelper.SAML_ATTRIBUTE_NAMEFORMAT);
property.setLabel("SAML Attribute NameFormat");
property.setHelpText("SAML Attribute NameFormat. Can be basic, URI reference, or unspecified.");
List<String> types = new ArrayList(3);
types.add(AttributeStatementHelper.BASIC);
types.add(AttributeStatementHelper.URI_REFERENCE);
types.add(AttributeStatementHelper.UNSPECIFIED);
property.setType(ProviderConfigProperty.LIST_TYPE);
property.setDefaultValue(types);
configProperties.add(property);
property = new ProviderConfigProperty();
property.setName(SINGLE_GROUP_ATTRIBUTE);
property.setLabel("Single Group Attribute");
property.setType(ProviderConfigProperty.BOOLEAN_TYPE);
property.setDefaultValue("true");
property.setHelpText("If true, all groups will be stored under one attribute with multiple attribute values.");
configProperties.add(property);
property = new ProviderConfigProperty();
property.setName("full.path");
property.setLabel("Full group path");
property.setType(ProviderConfigProperty.BOOLEAN_TYPE);
property.setDefaultValue("true");
property.setHelpText("Include full path to group i.e. /top/level1/level2, false will just specify the group name");
configProperties.add(property);
}
@Override
public String getDisplayCategory() {
return "Group Mapper";
}
@Override
public String getDisplayType() {
return "Group list";
}
@Override
public String getHelpText() {
return "Group names are stored in an attribute value. There is either one attribute with multiple attribute values, or an attribute per group name depending on how you configure it. You can also specify the attribute name i.e. 'member' or 'memberOf' being examples.";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return configProperties;
}
@Override
public String getId() {
return PROVIDER_ID;
}
public static boolean useFullPath(ProtocolMapperModel mappingModel) {
return "true".equals(mappingModel.getConfig().get("full.path"));
}
@Override
public void transformAttributeStatement(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) {
String single = mappingModel.getConfig().get(SINGLE_GROUP_ATTRIBUTE);
boolean singleAttribute = Boolean.parseBoolean(single);
boolean fullPath = useFullPath(mappingModel);
AttributeType singleAttributeType = null;
for (GroupModel group : userSession.getUser().getGroups()) {
String groupName;
if (fullPath) {
groupName = ModelToRepresentation.buildGroupPath(group);
} else {
groupName = group.getName();
}
AttributeType attributeType = null;
if (singleAttribute) {
if (singleAttributeType == null) {
singleAttributeType = AttributeStatementHelper.createAttributeType(mappingModel);
attributeStatement.addAttribute(new AttributeStatementType.ASTChoiceType(singleAttributeType));
}
attributeType = singleAttributeType;
} else {
attributeType = AttributeStatementHelper.createAttributeType(mappingModel);
attributeStatement.addAttribute(new AttributeStatementType.ASTChoiceType(attributeType));
}
attributeType.addAttributeValue(groupName);
}
}
public static ProtocolMapperModel create(String name, String samlAttributeName, String nameFormat, String friendlyName, boolean singleAttribute) {
ProtocolMapperModel mapper = new ProtocolMapperModel();
mapper.setName(name);
mapper.setProtocolMapper(PROVIDER_ID);
mapper.setProtocol(SamlProtocol.LOGIN_PROTOCOL);
mapper.setConsentRequired(false);
Map<String, String> config = new HashMap<String, String>();
config.put(AttributeStatementHelper.SAML_ATTRIBUTE_NAME, samlAttributeName);
if (friendlyName != null) {
config.put(AttributeStatementHelper.FRIENDLY_NAME, friendlyName);
}
if (nameFormat != null) {
config.put(AttributeStatementHelper.SAML_ATTRIBUTE_NAMEFORMAT, nameFormat);
}
config.put(SINGLE_GROUP_ATTRIBUTE, Boolean.toString(singleAttribute));
mapper.setConfig(config);
return mapper;
}
}

View file

@ -0,0 +1,12 @@
package org.keycloak.protocol.saml.mappers;
import org.keycloak.models.GroupModel;
import org.keycloak.models.ProtocolMapperModel;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface SAMLGroupNameMapper {
public String mapName(ProtocolMapperModel model, GroupModel group);
}

View file

@ -5,5 +5,6 @@ org.keycloak.protocol.saml.mappers.HardcodedAttributeMapper
org.keycloak.protocol.saml.mappers.UserAttributeStatementMapper
org.keycloak.protocol.saml.mappers.UserPropertyAttributeStatementMapper
org.keycloak.protocol.saml.mappers.UserSessionNoteStatementMapper
org.keycloak.protocol.saml.mappers.GroupMembershipMapper

View file

@ -0,0 +1,152 @@
package org.keycloak.protocol.oidc.mappers;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.protocol.ProtocolMapperUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.IDToken;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* Maps user group membership
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class GroupMembershipMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper, OIDCIDTokenMapper {
private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
static {
ProviderConfigProperty property;
ProviderConfigProperty property1;
property1 = new ProviderConfigProperty();
property1.setName(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME);
property1.setLabel(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME_LABEL);
property1.setType(ProviderConfigProperty.STRING_TYPE);
property1.setDefaultValue("groups");
property1.setHelpText(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME_TOOLTIP);
configProperties.add(property1);
property1 = new ProviderConfigProperty();
property1.setName("full.path");
property1.setLabel("Full group path");
property1.setType(ProviderConfigProperty.BOOLEAN_TYPE);
property1.setDefaultValue("true");
property1.setHelpText("Include full path to group i.e. /top/level1/level2, false will just specify the group name");
configProperties.add(property1);
property1 = new ProviderConfigProperty();
property1.setName(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN);
property1.setLabel(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN_LABEL);
property1.setType(ProviderConfigProperty.BOOLEAN_TYPE);
property1.setDefaultValue("true");
property1.setHelpText(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN_HELP_TEXT);
configProperties.add(property1);
property1 = new ProviderConfigProperty();
property1.setName(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN);
property1.setLabel(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN_LABEL);
property1.setType(ProviderConfigProperty.BOOLEAN_TYPE);
property1.setDefaultValue("true");
property1.setHelpText(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN_HELP_TEXT);
configProperties.add(property1);
}
public static final String PROVIDER_ID = "oidc-group-membership-mapper";
public List<ProviderConfigProperty> getConfigProperties() {
return configProperties;
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public String getDisplayType() {
return "Group Membership";
}
@Override
public String getDisplayCategory() {
return TOKEN_MAPPER_CATEGORY;
}
@Override
public String getHelpText() {
return "Map user group membership";
}
public static boolean useFullPath(ProtocolMapperModel mappingModel) {
return "true".equals(mappingModel.getConfig().get("full.path"));
}
@Override
public AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session,
UserSessionModel userSession, ClientSessionModel clientSession) {
if (!OIDCAttributeMapperHelper.includeInAccessToken(mappingModel)) return token;
buildMembership(token, mappingModel, userSession);
return token;
}
public void buildMembership(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession) {
List<String> membership = new LinkedList<>();
boolean fullPath = useFullPath(mappingModel);
for (GroupModel group : userSession.getUser().getGroups()) {
if (fullPath) {
membership.add(ModelToRepresentation.buildGroupPath(group));
} else {
membership.add(group.getName());
}
}
String protocolClaim = mappingModel.getConfig().get(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME);
token.getOtherClaims().put(protocolClaim, membership);
}
@Override
public IDToken transformIDToken(IDToken token, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) {
if (!OIDCAttributeMapperHelper.includeInIDToken(mappingModel)) return token;
buildMembership(token, mappingModel, userSession);
return token;
}
public static ProtocolMapperModel create(String name,
String tokenClaimName,
boolean consentRequired, String consentText,
boolean accessToken, boolean idToken) {
ProtocolMapperModel mapper = new ProtocolMapperModel();
mapper.setName(name);
mapper.setProtocolMapper(PROVIDER_ID);
mapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
mapper.setConsentRequired(consentRequired);
mapper.setConsentText(consentText);
Map<String, String> config = new HashMap<String, String>();
config.put(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME, tokenClaimName);
if (accessToken) config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, "true");
if (idToken) config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN, "true");
mapper.setConfig(config);
return mapper;
}
}

View file

@ -45,123 +45,47 @@ public class GroupResource {
private final KeycloakSession session;
private final RealmAuth auth;
private final AdminEventBuilder adminEvent;
private final GroupModel group;
public GroupResource(RealmModel realm, KeycloakSession session, RealmAuth auth, AdminEventBuilder adminEvent) {
public GroupResource(RealmModel realm, GroupModel group, KeycloakSession session, RealmAuth auth, AdminEventBuilder adminEvent) {
this.realm = realm;
this.session = session;
this.auth = auth;
this.adminEvent = adminEvent;
this.group = group;
}
@Context private UriInfo uriInfo;
public GroupResource(RealmAuth auth, RealmModel realm, KeycloakSession session, AdminEventBuilder adminEvent) {
this.realm = realm;
this.session = session;
this.auth = auth;
this.adminEvent = adminEvent;
}
/**
* Get group hierarchy. Only name and ids are returned.
*
*
* @return
*/
@GET
@NoCache
@Produces(MediaType.APPLICATION_JSON)
public List<GroupRepresentation> getGroups() {
public GroupRepresentation getGroup() {
this.auth.requireView();
return ModelToRepresentation.toGroupHierarchy(realm, false);
return ModelToRepresentation.toGroupHierarchy(group, true);
}
/**
* Set or create child as a top level group. This will update the group and set the parent if it exists. Create it and set the parent
* if the group doesn't exist.
*
* @param rep
*/
@POST
@Path("{id}")
@NoCache
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public Response addRealmGroup(@PathParam("id") String parentId, GroupRepresentation rep) {
GroupModel parentModel = realm.getGroupById(parentId);
Response.ResponseBuilder builder = Response.status(204);
if (parentModel == null) {
throw new NotFoundException("Could not find parent by id");
}
GroupModel child = null;
if (rep.getId() != null) {
child = realm.getGroupById(rep.getId());
if (child == null) {
throw new NotFoundException("Could not find child by id");
}
} else {
child = realm.createGroup(rep.getName());
updateGroup(rep, child);
URI uri = uriInfo.getBaseUriBuilder()
.path(uriInfo.getMatchedURIs().get(1))
.path(child.getId()).build();
builder.status(201).location(uri);
}
child.setParent(parentModel);
GroupRepresentation childRep = ModelToRepresentation.toRepresentation(child, true);
return builder.type(MediaType.APPLICATION_JSON_TYPE).entity(childRep).build();
}
/**
* Does not expand hierarchy. Subgroups will not be set.
*
* @param id
* @return
*/
@GET
@Path("{id}")
@NoCache
@Produces(MediaType.APPLICATION_JSON)
public GroupRepresentation getGroupById(@PathParam("id") String id) {
this.auth.requireView();
GroupModel group = realm.getGroupById(id);
if (group == null) {
throw new NotFoundException("Could not find group by id");
}
return ModelToRepresentation.toRepresentation(group, true);
}
/**
* Update group
* Update group, ignores subgroups.
*
* @param rep
*/
@PUT
@Path("{id}")
@Consumes(MediaType.APPLICATION_JSON)
public void updateGroup(@PathParam("id") String id, GroupRepresentation rep) {
GroupModel model = realm.getGroupById(id);
if (model == null) {
throw new NotFoundException("Could not find group by id");
}
updateGroup(rep, model);
public void updateGroup(GroupRepresentation rep) {
updateGroup(rep, group);
}
@DELETE
@Path("{id}")
public void deleteGroup(@PathParam("id") String id) {
GroupModel model = realm.getGroupById(id);
if (model == null) {
throw new NotFoundException("Could not find group by id");
}
realm.removeGroup(model);
public void deleteGroup() {
realm.removeGroup(group);
}
@ -172,16 +96,12 @@ public class GroupResource {
* @param rep
*/
@POST
@Path("{id}/children")
@Path("children")
@NoCache
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public Response addGroup(@PathParam("id") String parentId, GroupRepresentation rep) {
GroupModel parentModel = realm.getGroupById(parentId);
public Response addChild(GroupRepresentation rep) {
Response.ResponseBuilder builder = Response.status(204);
if (parentModel == null) {
throw new NotFoundException("Could not find parent by id");
}
GroupModel child = null;
if (rep.getId() != null) {
child = realm.getGroupById(rep.getId());
@ -197,39 +117,12 @@ public class GroupResource {
builder.status(201).location(uri);
}
realm.moveGroup(child, parentModel);
GroupRepresentation childRep = ModelToRepresentation.toRepresentation(child, true);
realm.moveGroup(child, group);
GroupRepresentation childRep = ModelToRepresentation.toGroupHierarchy(child, true);
return builder.type(MediaType.APPLICATION_JSON_TYPE).entity(childRep).build();
}
/**
* create or add a top level realm groupSet or create child. This will update the group and set the parent if it exists. Create it and set the parent
* if the group doesn't exist.
*
* @param rep
*/
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response addTopLevelGroup(GroupRepresentation rep) {
GroupModel child = null;
Response.ResponseBuilder builder = Response.status(204);
if (rep.getId() != null) {
child = realm.getGroupById(rep.getId());
if (child == null) {
throw new NotFoundException("Could not find child by id");
}
} else {
child = realm.createGroup(rep.getName());
updateGroup(rep, child);
URI uri = uriInfo.getAbsolutePathBuilder()
.path(child.getId()).build();
builder.status(201).location(uri);
}
realm.moveGroup(child, null);
return builder.build();
}
public void updateGroup(GroupRepresentation rep, GroupModel model) {
public static void updateGroup(GroupRepresentation rep, GroupModel model) {
if (rep.getName() != null) model.setName(rep.getName());
if (rep.getAttributes() != null) {
@ -245,13 +138,8 @@ public class GroupResource {
}
}
@Path("{id}/role-mappings")
public RoleMapperResource getRoleMappings(@PathParam("id") String id) {
GroupModel group = session.realms().getGroupById(id, realm);
if (group == null) {
throw new NotFoundException("Group not found");
}
@Path("role-mappings")
public RoleMapperResource getRoleMappings() {
auth.init(RealmAuth.Resource.USER);
RoleMapperResource resource = new RoleMapperResource(realm, auth, group, adminEvent);
@ -271,18 +159,11 @@ public class GroupResource {
*/
@GET
@NoCache
@Path("{id}/members")
@Path("members")
@Produces(MediaType.APPLICATION_JSON)
public List<UserRepresentation> getMembers(@PathParam("id") String id,
@QueryParam("first") Integer firstResult,
public List<UserRepresentation> getMembers(@QueryParam("first") Integer firstResult,
@QueryParam("max") Integer maxResults) {
auth.requireView();
GroupModel group = session.realms().getGroupById(id, realm);
if (group == null) {
throw new NotFoundException("Group not found");
}
firstResult = firstResult != null ? firstResult : -1;
maxResults = maxResults != null ? maxResults : -1;

View file

@ -0,0 +1,120 @@
package org.keycloak.services.resources.admin;
import org.jboss.logging.Logger;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.NotFoundException;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.net.URI;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @author Bill Burke
*/
public class GroupsResource {
private static Logger logger = Logger.getLogger(GroupsResource.class);
private final RealmModel realm;
private final KeycloakSession session;
private final RealmAuth auth;
private final AdminEventBuilder adminEvent;
public GroupsResource(RealmModel realm, KeycloakSession session, RealmAuth auth, AdminEventBuilder adminEvent) {
this.realm = realm;
this.session = session;
this.auth = auth;
this.adminEvent = adminEvent;
}
@Context private UriInfo uriInfo;
public GroupsResource(RealmAuth auth, RealmModel realm, KeycloakSession session, AdminEventBuilder adminEvent) {
this.realm = realm;
this.session = session;
this.auth = auth;
this.adminEvent = adminEvent;
}
/**
* Get group hierarchy. Only name and ids are returned.
*
* @return
*/
@GET
@NoCache
@Produces(MediaType.APPLICATION_JSON)
public List<GroupRepresentation> getGroups() {
this.auth.requireView();
return ModelToRepresentation.toGroupHierarchy(realm, false);
}
/**
* Does not expand hierarchy. Subgroups will not be set.
*
* @param id
* @return
*/
@Path("{id}")
public GroupResource getGroupById(@PathParam("id") String id) {
GroupModel group = realm.getGroupById(id);
if (group == null) {
throw new NotFoundException("Could not find group by id");
}
GroupResource resource = new GroupResource(realm, group, session, this.auth, adminEvent);
ResteasyProviderFactory.getInstance().injectProperties(resource);
return resource;
}
/**
* create or add a top level realm groupSet or create child. This will update the group and set the parent if it exists. Create it and set the parent
* if the group doesn't exist.
*
* @param rep
*/
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response addTopLevelGroup(GroupRepresentation rep) {
GroupModel child = null;
Response.ResponseBuilder builder = Response.status(204);
if (rep.getId() != null) {
child = realm.getGroupById(rep.getId());
if (child == null) {
throw new NotFoundException("Could not find child by id");
}
} else {
child = realm.createGroup(rep.getName());
GroupResource.updateGroup(rep, child);
URI uri = uriInfo.getAbsolutePathBuilder()
.path(child.getId()).build();
builder.status(201).location(uri);
}
realm.moveGroup(child, null);
return builder.build();
}
}

View file

@ -16,6 +16,7 @@ import org.keycloak.events.admin.OperationType;
import org.keycloak.exportimport.ClientDescriptionConverter;
import org.keycloak.exportimport.ClientDescriptionConverterFactory;
import org.keycloak.models.ClientModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.RealmModel;
@ -23,12 +24,14 @@ import org.keycloak.models.UserFederationProviderModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.cache.CacheRealmProvider;
import org.keycloak.models.cache.CacheUserProvider;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.representations.adapters.action.GlobalRequestResult;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.RealmEventsConfigRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.services.managers.AuthenticationManager;
@ -620,10 +623,26 @@ public class RealmAdminResource {
}
@Path("groups")
public GroupResource getGroups() {
GroupResource resource = new GroupResource(realm, session, this.auth, adminEvent);
public GroupsResource getGroups() {
GroupsResource resource = new GroupsResource(realm, session, this.auth, adminEvent);
ResteasyProviderFactory.getInstance().injectProperties(resource);
return resource;
}
@GET
@Path("group-by-path/{path: .*}")
@NoCache
@Produces(MediaType.APPLICATION_JSON)
public GroupRepresentation getGroupByPath(@PathParam("path") String path) {
this.auth.requireView();
GroupModel found = KeycloakModelUtils.findGroupByPath(realm, path);
if (found == null) {
throw new NotFoundException("Group path does not exist");
}
return ModelToRepresentation.toGroupHierarchy(found, true);
}
}

View file

@ -6,5 +6,6 @@ org.keycloak.protocol.oidc.mappers.HardcodedClaim
org.keycloak.protocol.oidc.mappers.HardcodedRole
org.keycloak.protocol.oidc.mappers.RoleNameMapper
org.keycloak.protocol.oidc.mappers.UserSessionNoteMapper
org.keycloak.protocol.oidc.mappers.GroupMembershipMapper

View file

@ -1,5 +1,6 @@
package org.keycloak.testsuite.keycloaksaml;
import com.mongodb.util.Hash;
import org.apache.commons.io.IOUtils;
import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataOutput;
import org.junit.Assert;
@ -22,10 +23,12 @@ import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.protocol.saml.mappers.AttributeStatementHelper;
import org.keycloak.protocol.saml.mappers.GroupMembershipMapper;
import org.keycloak.protocol.saml.mappers.HardcodedAttributeMapper;
import org.keycloak.protocol.saml.mappers.HardcodedRole;
import org.keycloak.protocol.saml.mappers.RoleListMapper;
import org.keycloak.protocol.saml.mappers.RoleNameMapper;
import org.keycloak.protocol.saml.mappers.UserAttributeStatementMapper;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
@ -53,8 +56,10 @@ import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import static org.junit.Assert.assertEquals;
@ -202,6 +207,40 @@ public class SamlAdapterTestStrategy extends ExternalResource {
}
public void testAttributes() throws Exception {
keycloakRule.update(new KeycloakRule.KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
ClientModel app = appRealm.getClientByClientId(APP_SERVER_BASE_URL + "/employee2/");
app.addProtocolMapper(GroupMembershipMapper.create("groups", "group", null, null, true));
app.addProtocolMapper(UserAttributeStatementMapper.createAttributeMapper("topAttribute", "topAttribute", "topAttribute", "Basic", null, false, null));
app.addProtocolMapper(UserAttributeStatementMapper.createAttributeMapper("level2Attribute", "level2Attribute", "level2Attribute", "Basic", null, false, null));
}
}, "demo");
{
SendUsernameServlet.sentPrincipal = null;
SendUsernameServlet.checkRoles = null;
driver.navigate().to(APP_SERVER_BASE_URL + "/employee2/");
Assert.assertTrue(driver.getCurrentUrl().startsWith(AUTH_SERVER_URL + "/realms/demo/protocol/saml"));
List<String> requiredRoles = new LinkedList<>();
requiredRoles.add("manager");
requiredRoles.add("user");
SendUsernameServlet.checkRoles = requiredRoles;
loginPage.login("level2GroupUser", "password");
assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/employee2/");
SendUsernameServlet.checkRoles = null;
SamlPrincipal principal = (SamlPrincipal) SendUsernameServlet.sentPrincipal;
Assert.assertNotNull(principal);
assertEquals("level2@redhat.com", principal.getAttribute(X500SAMLProfileConstants.EMAIL.get()));
assertEquals("true", principal.getAttribute("topAttribute"));
assertEquals("true", principal.getAttribute("level2Attribute"));
List<String> groups = principal.getAttributes("group");
Assert.assertNotNull(groups);
Set<String> groupSet = new HashSet<>();
assertEquals("level2@redhat.com", principal.getFriendlyAttribute("email"));
driver.navigate().to(APP_SERVER_BASE_URL + "/employee2/?GLO=true");
checkLoggedOut(APP_SERVER_BASE_URL + "/employee2/");
}
{
SendUsernameServlet.sentPrincipal = null;
SendUsernameServlet.checkRoles = null;

View file

@ -0,0 +1,252 @@
package org.keycloak.testsuite.model;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
import org.junit.Assert;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.resource.GroupResource;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator;
import org.keycloak.common.util.Time;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.models.ClientModel;
import org.keycloak.models.Constants;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.protocol.oidc.mappers.GroupMembershipMapper;
import org.keycloak.protocol.oidc.mappers.UserAttributeMapper;
import org.keycloak.protocol.saml.mappers.AttributeStatementHelper;
import org.keycloak.protocol.saml.mappers.HardcodedAttributeMapper;
import org.keycloak.protocol.saml.mappers.HardcodedRole;
import org.keycloak.protocol.saml.mappers.RoleListMapper;
import org.keycloak.protocol.saml.mappers.RoleNameMapper;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.RefreshToken;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.managers.ClientManager;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.OAuthClient;
import org.keycloak.testsuite.rule.KeycloakRule;
import org.keycloak.testsuite.rule.WebResource;
import org.keycloak.testsuite.rule.WebRule;
import org.openqa.selenium.WebDriver;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.core.Response;
import java.util.LinkedList;
import java.util.List;
import static org.junit.Assert.assertEquals;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class GroupTest {
@ClassRule
public static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
ClientModel app = new ClientManager(manager).createClient(appRealm, "resource-owner");
app.setSecret("secret");
UserModel user = session.users().addUser(appRealm, "direct-login");
user.setEmail("direct-login@localhost");
user.setEnabled(true);
session.users().updateCredential(appRealm, user, UserCredentialModel.password("password"));
keycloak = Keycloak.getInstance("http://localhost:8081/auth", "master", "admin", "admin", Constants.ADMIN_CONSOLE_CLIENT_ID);
}
});
protected static Keycloak keycloak;
@Rule
public AssertEvents events = new AssertEvents(keycloakRule);
@Rule
public WebRule webRule = new WebRule(this);
@WebResource
protected WebDriver driver;
@WebResource
protected OAuthClient oauth;
@Test
public void createAndTestGroups() throws Exception {
RealmResource realm = keycloak.realms().realm("test");
{
RoleRepresentation groupRole = new RoleRepresentation();
groupRole.setName("topRole");
realm.roles().create(groupRole);
}
RoleRepresentation topRole = realm.roles().get("topRole").toRepresentation();
{
RoleRepresentation groupRole = new RoleRepresentation();
groupRole.setName("level2Role");
realm.roles().create(groupRole);
}
RoleRepresentation level2Role = realm.roles().get("level2Role").toRepresentation();
{
RoleRepresentation groupRole = new RoleRepresentation();
groupRole.setName("level3Role");
realm.roles().create(groupRole);
}
RoleRepresentation level3Role = realm.roles().get("level3Role").toRepresentation();
GroupRepresentation topGroup = new GroupRepresentation();
topGroup.setName("top");
Response response = realm.groups().add(topGroup);
response.close();
topGroup = realm.getGroupByPath("/top");
Assert.assertNotNull(topGroup);
List<RoleRepresentation> roles = new LinkedList<>();
roles.add(topRole);
realm.groups().group(topGroup.getId()).roles().realmLevel().add(roles);
GroupRepresentation level2Group = new GroupRepresentation();
level2Group.setName("level2");
response = realm.groups().group(topGroup.getId()).subGroup(level2Group);
response.close();
level2Group = realm.getGroupByPath("/top/level2");
Assert.assertNotNull(level2Group);
roles.clear();
roles.add(level2Role);
realm.groups().group(level2Group.getId()).roles().realmLevel().add(roles);
GroupRepresentation level3Group = new GroupRepresentation();
level3Group.setName("level3");
response = realm.groups().group(level2Group.getId()).subGroup(level3Group);
response.close();
level3Group = realm.getGroupByPath("/top/level2/level3");
Assert.assertNotNull(level3Group);
roles.clear();
roles.add(level3Role);
realm.groups().group(level3Group.getId()).roles().realmLevel().add(roles);
topGroup = realm.getGroupByPath("/top");
Assert.assertEquals(1, topGroup.getRealmRoles().size());
Assert.assertTrue(topGroup.getRealmRoles().contains("topRole"));
Assert.assertEquals(1, topGroup.getSubGroups().size());
level2Group = topGroup.getSubGroups().get(0);
Assert.assertEquals("level2", level2Group.getName());
Assert.assertEquals(1, level2Group.getRealmRoles().size());
Assert.assertTrue(level2Group.getRealmRoles().contains("level2Role"));
Assert.assertEquals(1, level2Group.getSubGroups().size());
level3Group = level2Group.getSubGroups().get(0);
Assert.assertEquals("level3", level3Group.getName());
Assert.assertEquals(1, level3Group.getRealmRoles().size());
Assert.assertTrue(level3Group.getRealmRoles().contains("level3Role"));
try {
GroupRepresentation notFound = realm.getGroupByPath("/notFound");
Assert.fail();
} catch (NotFoundException e) {
}
try {
GroupRepresentation notFound = realm.getGroupByPath("/top/notFound");
Assert.fail();
} catch (NotFoundException e) {
}
UserRepresentation user = realm.users().search("direct-login", -1, -1).get(0);
realm.users().get(user.getId()).joinGroup(level3Group.getId());
List<GroupRepresentation> membership = realm.users().get(user.getId()).groups();
Assert.assertEquals(1, membership.size());
Assert.assertEquals("level3", membership.get(0).getName());
AccessToken token = login("direct-login", "resource-owner", "secret", user.getId());
Assert.assertTrue(token.getRealmAccess().getRoles().contains("topRole"));
Assert.assertTrue(token.getRealmAccess().getRoles().contains("level2Role"));
Assert.assertTrue(token.getRealmAccess().getRoles().contains("level3Role"));
}
@Test
public void testGroupMappers() throws Exception {
keycloakRule.update(new KeycloakRule.KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
ClientModel app = appRealm.getClientByClientId("test-app");
app.addProtocolMapper(GroupMembershipMapper.create("groups", "groups", false, null, true, true));
app.addProtocolMapper(UserAttributeMapper.createClaimMapper("topAttribute", "topAttribute", "topAttribute", ProviderConfigProperty.STRING_TYPE, false, null, true, true, false));
app.addProtocolMapper(UserAttributeMapper.createClaimMapper("level2Attribute", "level2Attribute", "level2Attribute", ProviderConfigProperty.STRING_TYPE, false, null, true, true, false));
}
}, "test");
RealmResource realm = keycloak.realms().realm("test");
{
UserRepresentation user = realm.users().search("topGroupUser", -1, -1).get(0);
AccessToken token = login(user.getUsername(), "test-app", "password", user.getId());
Assert.assertTrue(token.getRealmAccess().getRoles().contains("user"));
List<String> groups = (List<String>) token.getOtherClaims().get("groups");
Assert.assertNotNull(groups);
Assert.assertTrue(groups.size() == 1);
Assert.assertEquals("topGroup", groups.get(0));
Assert.assertEquals("true", token.getOtherClaims().get("topAttribute"));
}
{
UserRepresentation user = realm.users().search("level2GroupUser", -1, -1).get(0);
AccessToken token = login(user.getUsername(), "test-app", "password", user.getId());
Assert.assertTrue(token.getRealmAccess().getRoles().contains("user"));
Assert.assertTrue(token.getRealmAccess().getRoles().contains("admin"));
Assert.assertTrue(token.getResourceAccess("test-app").getRoles().contains("customer-user"));
List<String> groups = (List<String>) token.getOtherClaims().get("groups");
Assert.assertNotNull(groups);
Assert.assertTrue(groups.size() == 1);
Assert.assertEquals("level2group", groups.get(0));
Assert.assertEquals("true", token.getOtherClaims().get("topAttribute"));
Assert.assertEquals("true", token.getOtherClaims().get("level2Attribute"));
}
}
protected AccessToken login(String login, String clientId, String clientSecret, String userId) throws Exception {
oauth.clientId(clientId);
OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest(clientSecret, login, "password");
assertEquals(200, response.getStatusCode());
AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
RefreshToken refreshToken = oauth.verifyRefreshToken(response.getRefreshToken());
events.expectLogin()
.client(clientId)
.user(userId)
.session(accessToken.getSessionState())
.detail(Details.RESPONSE_TYPE, "token")
.detail(Details.TOKEN_ID, accessToken.getId())
.detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
.detail(Details.USERNAME, login)
.removeDetail(Details.CODE_ID)
.removeDetail(Details.REDIRECT_URI)
.removeDetail(Details.CONSENT)
.assertEvent();
return accessToken;
}
}

View file

@ -34,12 +34,36 @@
"lastName": "Posolda",
"credentials" : [
{ "type" : "password",
"value" : "password" }
"value" : "password" }
],
"realmRoles": [ "user" ],
"applicationRoles": {
"account": [ "manage-account" ]
}
},
{
"username" : "topGroupUser",
"enabled": true,
"email" : "top@redhat.com",
"credentials" : [
{ "type" : "password",
"value" : "password" }
],
"groups": [
"/top"
]
},
{
"username" : "level2GroupUser",
"enabled": true,
"email" : "level2@redhat.com",
"credentials" : [
{ "type" : "password",
"value" : "password" }
],
"groups": [
"/top/level2"
]
}
],
"roles" : {
@ -54,6 +78,29 @@
}
]
},
"groups" : [
{
"name": "top",
"attributes": {
"topAttribute": ["true"]
},
"realmRoles": ["user"],
"clientRoles": {
"account": ["manage-account"]
},
"subGroups": [
{
"name": "level2",
"realmRoles": ["admin"],
"attributes": {
"level2Attribute": ["true"]
}
}
]
}
],
"scopeMappings": [
{
"client": "third-party",

View file

@ -40,6 +40,30 @@
{ "type" : "password",
"value" : "password" }
]
},
{
"username" : "topGroupUser",
"enabled": true,
"email" : "top@redhat.com",
"credentials" : [
{ "type" : "password",
"value" : "password" }
],
"groups": [
"/top"
]
},
{
"username" : "level2GroupUser",
"enabled": true,
"email" : "level2@redhat.com",
"credentials" : [
{ "type" : "password",
"value" : "password" }
],
"groups": [
"/top/level2"
]
}
],
"applications": [
@ -347,6 +371,27 @@
}
}
],
"groups" : [
{
"name": "top",
"attributes": {
"topAttribute": ["true"]
},
"realmRoles": ["manager"],
"subGroups": [
{
"name": "level2",
"realmRoles": ["user"],
"attributes": {
"level2Attribute": ["true"]
}
}
]
}
],
"roles" : {
"realm" : [
{

View file

@ -61,6 +61,30 @@
"test-app": [ "customer-user" ],
"account": [ "view-profile", "manage-account" ]
}
},
{
"username" : "topGroupUser",
"enabled": true,
"email" : "top@redhat.com",
"credentials" : [
{ "type" : "password",
"value" : "password" }
],
"groups": [
"/topGroup"
]
},
{
"username" : "level2GroupUser",
"enabled": true,
"email" : "level2@redhat.com",
"credentials" : [
{ "type" : "password",
"value" : "password" }
],
"groups": [
"/topGroup/level2group"
]
}
],
"scopeMappings": [
@ -120,6 +144,31 @@
}
},
"groups" : [
{
"name": "topGroup",
"attributes": {
"topAttribute": ["true"]
},
"realmRoles": ["user"],
"subGroups": [
{
"name": "level2group",
"realmRoles": ["admin"],
"clientRoles": {
"test-app": ["customer-user"]
},
"attributes": {
"level2Attribute": ["true"]
}
}
]
}
],
"clientScopeMappings": {
"test-app": [