diff --git a/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java b/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java index ae9701b475..67cb127a90 100755 --- a/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java +++ b/connections/mongo/src/main/java/org/keycloak/connections/mongo/DefaultMongoConnectionFactoryProvider.java @@ -35,6 +35,7 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro "org.keycloak.models.mongo.keycloak.entities.MongoRealmEntity", "org.keycloak.models.mongo.keycloak.entities.MongoUserEntity", "org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity", + "org.keycloak.models.mongo.keycloak.entities.MongoGroupEntity", "org.keycloak.models.mongo.keycloak.entities.MongoClientEntity", "org.keycloak.models.mongo.keycloak.entities.MongoUserConsentEntity", "org.keycloak.models.mongo.keycloak.entities.MongoMigrationModelEntity", diff --git a/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java index 07865db1a8..8635014c11 100755 --- a/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java @@ -40,6 +40,8 @@ public class UserRepresentation { @Deprecated protected List socialLinks; + protected List groups; + public String getSelf() { return self; } @@ -216,4 +218,12 @@ public class UserRepresentation { public void setServiceAccountClientId(String serviceAccountClientId) { this.serviceAccountClientId = serviceAccountClientId; } + + public List getGroups() { + return groups; + } + + public void setGroups(List groups) { + this.groups = groups; + } } diff --git a/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java index 30eb0558fe..12b358e2db 100755 --- a/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java +++ b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java @@ -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 groups = new LinkedList<>(); + for (GroupModel group : user.getGroups()) { + groups.add(ModelToRepresentation.buildGroupPath(group)); + } + userRep.setGroups(groups); + return userRep; } diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/groups.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/groups.js index 4e3b98634c..811343beb5 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/groups.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/groups.js @@ -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; diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/group-members.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/group-members.html index 6c20930275..50bc11b008 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/group-members.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/group-members.html @@ -14,7 +14,7 @@ Last Name First Name Email - Actions + diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupResource.java new file mode 100755 index 0000000000..84af76e9d5 --- /dev/null +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupResource.java @@ -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 Bill Burke + * @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 + *

+ * 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 members(@QueryParam("first") Integer firstResult, + @QueryParam("max") Integer maxResults); +} diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupsResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupsResource.java new file mode 100755 index 0000000000..f917d49a8f --- /dev/null +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupsResource.java @@ -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 Bill Burke + * @version $Revision: 1 $ + */ +public interface GroupsResource { + @GET + @NoCache + @Produces(MediaType.APPLICATION_JSON) + public List 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); + +} diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java old mode 100644 new mode 100755 index 82b023b73e..8ac199cdc6 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/RealmResource.java @@ -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(); diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UserResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UserResource.java index a2490b6120..cc70e2429e 100755 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UserResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/UserResource.java @@ -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 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(); diff --git a/model/api/src/main/java/org/keycloak/models/RealmModel.java b/model/api/src/main/java/org/keycloak/models/RealmModel.java index c3bb5c208d..641f37e419 100755 --- a/model/api/src/main/java/org/keycloak/models/RealmModel.java +++ b/model/api/src/main/java/org/keycloak/models/RealmModel.java @@ -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 diff --git a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java index d75ed95136..1a1709b8dd 100755 --- a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java +++ b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java @@ -462,6 +462,9 @@ public class UserFederationManager implements UserProvider { } + + + @Override public boolean validCredentials(RealmModel realm, UserModel user, UserCredentialModel... input) { return validCredentials(realm, user, Arrays.asList(input)); diff --git a/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java b/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java index 02caa74c95..4126daad6e 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java +++ b/model/api/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java @@ -27,8 +27,15 @@ import org.keycloak.common.util.PemUtils; import javax.crypto.spec.SecretKeySpec; import java.io.IOException; import java.io.StringWriter; -import java.security.*; +import java.security.Key; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SecureRandom; import java.security.cert.X509Certificate; +import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; @@ -406,4 +413,101 @@ public final class KeycloakModelUtils { } } } + + public static String resolveFirstAttribute(GroupModel group, String name) { + String value = group.getFirstAttribute(name); + if (value != null) return value; + if (group.getParentId() == null) return null; + return resolveFirstAttribute(group.getParent(), name); + + } + + /** + * + * + * @param user + * @param name + * @return + */ + public static String resolveFirstAttribute(UserModel user, String name) { + String value = user.getFirstAttribute(name); + if (value != null) return value; + for (GroupModel group : user.getGroups()) { + value = resolveFirstAttribute(group, name); + if (value != null) return value; + } + return null; + + } + + public static List resolveAttribute(GroupModel group, String name) { + List values = group.getAttribute(name); + if (values != null && !values.isEmpty()) return values; + if (group.getParentId() == null) return null; + return resolveAttribute(group.getParent(), name); + + } + + + public static List resolveAttribute(UserModel user, String name) { + List values = user.getAttribute(name); + if (!values.isEmpty()) return values; + for (GroupModel group : user.getGroups()) { + values = resolveAttribute(group, name); + if (values != null) return values; + } + return Collections.emptyList(); + } + + + 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; + } } diff --git a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java index 32ac72a842..b1d71ec64a 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java +++ b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java @@ -279,10 +279,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 groups = toGroupHierarchy(realm, true); + rep.setGroups(groups); + } + public static void exportAuthenticationFlows(RealmModel realm, RealmRepresentation rep) { rep.setAuthenticationFlows(new LinkedList()); rep.setAuthenticatorConfig(new LinkedList()); diff --git a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index a31d35592b..bb9dbc8d3d 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -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 groups = rep.getGroups(); + if (groups == null) return; + + Map clientMap = realm.getClientNameMap(); + GroupModel parent = null; + for (GroupRepresentation group : groups) { + importGroup(realm, clientMap, parent, group); + } + } + + public static void importGroup(RealmModel realm, Map clientMap, GroupModel parent, GroupRepresentation group) { + GroupModel newGroup = realm.createGroup(group.getId(), group.getName()); + if (group.getAttributes() != null) { + for (Map.Entry> 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> 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 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; } diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java index be9fe7c12e..68e62bfee3 100755 --- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java +++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/RealmAdapter.java @@ -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(); diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java index 7113fde9a5..8240005643 100755 --- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java +++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java @@ -321,7 +321,7 @@ public class UserAdapter implements UserModel { public Set getGroups() { if (updated != null) return updated.getGroups(); Set groups = new HashSet(); - 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 diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/GroupAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/GroupAdapter.java index 209d2ae694..0fb9c2f22e 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/GroupAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/GroupAdapter.java @@ -312,9 +312,9 @@ public class GroupAdapter implements GroupModel { @Override public boolean equals(Object o) { if (this == o) return true; - if (o == null || !(o instanceof UserModel)) return false; + if (o == null || !(o instanceof GroupModel)) return false; - UserModel that = (UserModel) o; + GroupModel that = (GroupModel) o; return that.getId().equals(getId()); } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java index 4ec5d664db..09b4fa2ad1 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java @@ -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; } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java index 5f8ce7f815..61e4982e88 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java @@ -97,6 +97,7 @@ public class JpaUserProvider implements UserProvider { private void removeUser(UserEntity user) { String id = user.getId(); em.createNamedQuery("deleteUserRoleMappingsByUser").setParameter("user", user).executeUpdate(); + em.createNamedQuery("deleteUserGroupMembershipsByUser").setParameter("user", user).executeUpdate(); em.createNamedQuery("deleteFederatedIdentityByUser").setParameter("user", user).executeUpdate(); em.createNamedQuery("deleteUserConsentRolesByUser").setParameter("user", user).executeUpdate(); em.createNamedQuery("deleteUserConsentProtMappersByUser").setParameter("user", user).executeUpdate(); @@ -174,10 +175,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 diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java index 7b8e3a6cca..eae002b545 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java @@ -1000,6 +1000,7 @@ public class RealmAdapter implements RealmModel { String compositeRoleTable = JpaUtils.getTableNameForNativeQuery("COMPOSITE_ROLE", em); em.createNativeQuery("delete from " + compositeRoleTable + " where CHILD_ROLE = :role").setParameter("role", roleEntity).executeUpdate(); em.createNamedQuery("deleteScopeMappingByRole").setParameter("role", roleEntity).executeUpdate(); + em.createNamedQuery("deleteGroupRoleMappingsByRole").setParameter("roleId", roleEntity.getId()).executeUpdate(); em.remove(roleEntity); @@ -1971,7 +1972,7 @@ public class RealmAdapter implements RealmModel { @Override public List getGroups() { List list = new LinkedList<>(); - Collection groups = realm.getGroups(); + Collection groups = em.createNamedQuery("getAllGroupsByRealm").setParameter("realm", realm).getResultList(); if (groups == null) return list; for (GroupEntity entity : groups) { list.add(new GroupAdapter(this, em, entity)); @@ -2008,7 +2009,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); @@ -2019,8 +2019,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); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java index 78de054388..40c2568a39 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java @@ -133,9 +133,6 @@ public class RealmEntity { @OneToMany(fetch = FetchType.LAZY, cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "realm") Collection roles = new ArrayList(); - @OneToMany(fetch = FetchType.LAZY, cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "realm") - Collection groups = new ArrayList(); - @ElementCollection @MapKeyColumn(name="NAME") @Column(name="VALUE") @@ -722,20 +719,5 @@ public class RealmEntity { this.clientAuthenticationFlow = clientAuthenticationFlow; } - public Collection getGroups() { - return groups; - } - - public void setGroups(Collection groups) { - this.groups = groups; - } - - public void addGroup(GroupEntity group) { - if (groups == null) { - groups = new ArrayList(); - } - groups.add(group); - } - } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/GroupAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/GroupAdapter.java index d261c6daa0..180eb9d399 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/GroupAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/GroupAdapter.java @@ -6,6 +6,7 @@ import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; import org.keycloak.models.ClientModel; import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; +import org.keycloak.models.ModelException; import org.keycloak.models.RealmModel; import org.keycloak.models.RoleContainerModel; import org.keycloak.models.RoleModel; @@ -20,6 +21,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; @@ -146,7 +148,11 @@ public class GroupAdapter extends AbstractMongoAdapter impleme if (group.getRoleIds() == null || group.getRoleIds().isEmpty()) return Collections.EMPTY_SET; Set roles = new HashSet<>(); for (String id : group.getRoleIds()) { - roles.add(realm.getRoleById(id)); + RoleModel roleById = realm.getRoleById(id); + if (roleById == null) { + throw new ModelException("role does not exist in group role mappings"); + } + roles.add(roleById); } return roles; } @@ -198,18 +204,28 @@ public class GroupAdapter extends AbstractMongoAdapter impleme @Override public Set getSubGroups() { + DBObject query = new QueryBuilder() + .and("realmId").is(realm.getId()) + .and("parentId").is(getId()) + .get(); + List groups = getMongoStore().loadEntities(MongoGroupEntity.class, query, invocationContext); + Set subGroups = new HashSet<>(); - for (GroupModel groupModel : realm.getGroups()) { - if (groupModel.getParent().equals(this)) { - subGroups.add(groupModel); - } + + if (groups == null) return subGroups; + for (MongoGroupEntity group : groups) { + subGroups.add(realm.getGroupById(group.getId())); } + return subGroups; } @Override - public void setParent(GroupModel group) { - this.group.setParentId(group.getId()); + public void setParent(GroupModel parent) { + if (parent == null) group.setParentId(null); + else { + group.setParentId(parent.getId()); + } updateGroup(); } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java index 30266af27a..2fe85a11a9 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java @@ -611,8 +611,15 @@ public class RealmAdapter extends AbstractMongoAdapter 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()); @@ -653,7 +660,7 @@ public class RealmAdapter extends AbstractMongoAdapter impleme if (groups == null) return result; for (MongoGroupEntity group : groups) { - result.add(new GroupAdapter(session, this, group, invocationContext)); + result.add(model.getGroupById(group.getId(), this)); } return result; @@ -665,7 +672,7 @@ public class RealmAdapter extends AbstractMongoAdapter impleme Iterator it = all.iterator(); while (it.hasNext()) { GroupModel group = it.next(); - if (group.getParent() != null) { + if (group.getParentId() != null) { it.remove(); } } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoRealmEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoRealmEntity.java index 8269360085..78f107692c 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoRealmEntity.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoRealmEntity.java @@ -19,6 +19,10 @@ public class MongoRealmEntity extends RealmEntity implements MongoIdentifiableEn .and("realmId").is(getId()) .get(); + // Remove all roles of this realm + context.getMongoStore().removeEntities(MongoGroupEntity.class, query, true, context); + + // Remove all roles of this realm context.getMongoStore().removeEntities(MongoRoleEntity.class, query, true, context); diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoRoleEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoRoleEntity.java index 29491f8533..520f678bf3 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoRoleEntity.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoRoleEntity.java @@ -41,6 +41,18 @@ public class MongoRoleEntity extends RoleEntity implements MongoIdentifiableEnti public void afterRemove(MongoStoreInvocationContext invContext) { MongoStore mongoStore = invContext.getMongoStore(); + { + DBObject query = new QueryBuilder() + .and("roleIds").is(getId()) + .get(); + + List groups = mongoStore.loadEntities(MongoGroupEntity.class, query, invContext); + for (MongoGroupEntity group : groups) { + mongoStore.pullItemFromList(group, "roleIds", getId(), invContext); + } + + } + // Remove this scope from all clients, which has it DBObject query = new QueryBuilder() .and("scopeIds").is(getId()) diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/mappers/GroupMembershipMapper.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/mappers/GroupMembershipMapper.java new file mode 100755 index 0000000000..2253e5c405 --- /dev/null +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/mappers/GroupMembershipMapper.java @@ -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 Bill Burke + * @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 configProperties = new ArrayList(); + + 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 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 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 config = new HashMap(); + 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; + } + +} diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/mappers/SAMLGroupNameMapper.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/mappers/SAMLGroupNameMapper.java new file mode 100755 index 0000000000..87a48c96be --- /dev/null +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/mappers/SAMLGroupNameMapper.java @@ -0,0 +1,12 @@ +package org.keycloak.protocol.saml.mappers; + +import org.keycloak.models.GroupModel; +import org.keycloak.models.ProtocolMapperModel; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public interface SAMLGroupNameMapper { + public String mapName(ProtocolMapperModel model, GroupModel group); +} diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/mappers/UserAttributeStatementMapper.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/mappers/UserAttributeStatementMapper.java index dc80760747..8f5e3b80d5 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/mappers/UserAttributeStatementMapper.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/mappers/UserAttributeStatementMapper.java @@ -5,6 +5,7 @@ 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.protocol.ProtocolMapperUtils; import org.keycloak.dom.saml.v2.assertion.AttributeStatementType; import org.keycloak.provider.ProviderConfigProperty; @@ -62,7 +63,7 @@ public class UserAttributeStatementMapper extends AbstractSAMLProtocolMapper imp public void transformAttributeStatement(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) { UserModel user = userSession.getUser(); String attributeName = mappingModel.getConfig().get(ProtocolMapperUtils.USER_ATTRIBUTE); - String attributeValue = user.getFirstAttribute(attributeName); + String attributeValue = KeycloakModelUtils.resolveFirstAttribute(user, attributeName); if (attributeValue == null) return; AttributeStatementHelper.addAttribute(attributeStatement, mappingModel, attributeValue); diff --git a/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper b/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper index 163fed5081..df626325d1 100755 --- a/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper +++ b/saml/saml-protocol/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper @@ -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 diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java index 17d5b230f4..32a9a6df24 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java @@ -12,6 +12,7 @@ import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.crypto.RSAProvider; import org.keycloak.models.ClientModel; 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; @@ -289,10 +290,23 @@ public class TokenManager { } } + public static void addGroupRoles(GroupModel group, Set roleMappings) { + roleMappings.addAll(group.getRoleMappings()); + if (group.getParentId() == null) return; + addGroupRoles(group.getParent(), roleMappings); + } + public static Set getAccess(String scopeParam, boolean applyScopeParam, ClientModel client, UserModel user) { Set requestedRoles = new HashSet(); - Set roleMappings = user.getRoleMappings(); + Set mappings = user.getRoleMappings(); + Set roleMappings = new HashSet<>(); + roleMappings.addAll(mappings); + for (GroupModel group : user.getGroups()) { + addGroupRoles(group, roleMappings); + } + + if (client.isFullScopeAllowed()) { requestedRoles = roleMappings; diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/GroupMembershipMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/GroupMembershipMapper.java new file mode 100755 index 0000000000..848a8e83a3 --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/GroupMembershipMapper.java @@ -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 Bill Burke + * @version $Revision: 1 $ + */ +public class GroupMembershipMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper, OIDCIDTokenMapper { + + private static final List configProperties = new ArrayList(); + + 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 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 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 config = new HashMap(); + 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; + } + + +} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserAttributeMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserAttributeMapper.java index 246b82ee75..3e96692f59 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserAttributeMapper.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/UserAttributeMapper.java @@ -6,6 +6,7 @@ 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.protocol.ProtocolMapperUtils; import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.representations.AccessToken; @@ -84,7 +85,7 @@ public class UserAttributeMapper extends AbstractOIDCProtocolMapper implements O protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession) { UserModel user = userSession.getUser(); String attributeName = mappingModel.getConfig().get(ProtocolMapperUtils.USER_ATTRIBUTE); - List attributeValue = user.getAttribute(attributeName); + List attributeValue = KeycloakModelUtils.resolveAttribute(user, attributeName); if (attributeValue == null) return; OIDCAttributeMapperHelper.mapClaim(token, mappingModel, attributeValue); } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/GroupResource.java b/services/src/main/java/org/keycloak/services/resources/admin/GroupResource.java index 87253c103f..668057ebcf 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/GroupResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/GroupResource.java @@ -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 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 getMembers(@PathParam("id") String id, - @QueryParam("first") Integer firstResult, + public List 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; diff --git a/services/src/main/java/org/keycloak/services/resources/admin/GroupsResource.java b/services/src/main/java/org/keycloak/services/resources/admin/GroupsResource.java new file mode 100755 index 0000000000..6e3709da2f --- /dev/null +++ b/services/src/main/java/org/keycloak/services/resources/admin/GroupsResource.java @@ -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 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(); + } +} diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java index 36af035e3d..9845031cc5 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java @@ -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; @@ -632,10 +635,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); + } + + } diff --git a/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper b/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper index e7a64502ba..59f0f29628 100755 --- a/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper +++ b/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper @@ -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 diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java index 1fc4fc22a0..2f89194d35 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/exportimport/ExportImportTest.java @@ -163,8 +163,9 @@ public class ExportImportTest { testRealmExportImport(); - // There should be 3 files in target directory (1 realm, 2 user, 1 version) - Assert.assertEquals(4, new File(targetDirPath).listFiles().length); + // There should be 3 files in target directory (1 realm, 3 user, 1 version) + File[] files = new File(targetDirPath).listFiles(); + Assert.assertEquals(5, files.length); } @Test diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlAdapterTestStrategy.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlAdapterTestStrategy.java index 2c32d5161f..fbced9fce2 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlAdapterTestStrategy.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlAdapterTestStrategy.java @@ -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 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 groups = principal.getAttributes("group"); + Assert.assertNotNull(groups); + Set 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; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/GroupTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/GroupTest.java new file mode 100755 index 0000000000..d2882dc42f --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/GroupTest.java @@ -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 Stian Thorgersen + */ +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 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 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 groups = (List) 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 groups = (List) 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; + } + + +} diff --git a/testsuite/integration/src/test/resources/adapter-test/demorealm.json b/testsuite/integration/src/test/resources/adapter-test/demorealm.json index 5bf2bdd953..b5cd399810 100755 --- a/testsuite/integration/src/test/resources/adapter-test/demorealm.json +++ b/testsuite/integration/src/test/resources/adapter-test/demorealm.json @@ -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", diff --git a/testsuite/integration/src/test/resources/keycloak-saml/testsaml.json b/testsuite/integration/src/test/resources/keycloak-saml/testsaml.json index 8d5576e30f..95b9fb96a4 100755 --- a/testsuite/integration/src/test/resources/keycloak-saml/testsaml.json +++ b/testsuite/integration/src/test/resources/keycloak-saml/testsaml.json @@ -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" : [ { diff --git a/testsuite/integration/src/test/resources/testrealm.json b/testsuite/integration/src/test/resources/testrealm.json index e16e3e2f88..b4718dde46 100755 --- a/testsuite/integration/src/test/resources/testrealm.json +++ b/testsuite/integration/src/test/resources/testrealm.json @@ -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": [ diff --git a/testsuite/jetty/jetty81/src/test/resources/keycloak-saml/testsaml.json b/testsuite/jetty/jetty81/src/test/resources/keycloak-saml/testsaml.json index 0b13fb9e9e..04c5dcdf0f 100755 --- a/testsuite/jetty/jetty81/src/test/resources/keycloak-saml/testsaml.json +++ b/testsuite/jetty/jetty81/src/test/resources/keycloak-saml/testsaml.json @@ -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,26 @@ } } ], + "groups" : [ + { + "name": "top", + "attributes": { + "topAttribute": ["true"] + + }, + "realmRoles": ["manager"], + "subGroups": [ + { + "name": "level2", + "realmRoles": ["user"], + "attributes": { + "level2Attribute": ["true"] + + } + } + ] + } + ], "roles" : { "realm" : [ { diff --git a/testsuite/jetty/jetty91/src/test/resources/keycloak-saml/testsaml.json b/testsuite/jetty/jetty91/src/test/resources/keycloak-saml/testsaml.json index 0b13fb9e9e..04c5dcdf0f 100755 --- a/testsuite/jetty/jetty91/src/test/resources/keycloak-saml/testsaml.json +++ b/testsuite/jetty/jetty91/src/test/resources/keycloak-saml/testsaml.json @@ -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,26 @@ } } ], + "groups" : [ + { + "name": "top", + "attributes": { + "topAttribute": ["true"] + + }, + "realmRoles": ["manager"], + "subGroups": [ + { + "name": "level2", + "realmRoles": ["user"], + "attributes": { + "level2Attribute": ["true"] + + } + } + ] + } + ], "roles" : { "realm" : [ { diff --git a/testsuite/jetty/jetty92/src/test/resources/keycloak-saml/testsaml.json b/testsuite/jetty/jetty92/src/test/resources/keycloak-saml/testsaml.json index 0b13fb9e9e..04c5dcdf0f 100755 --- a/testsuite/jetty/jetty92/src/test/resources/keycloak-saml/testsaml.json +++ b/testsuite/jetty/jetty92/src/test/resources/keycloak-saml/testsaml.json @@ -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,26 @@ } } ], + "groups" : [ + { + "name": "top", + "attributes": { + "topAttribute": ["true"] + + }, + "realmRoles": ["manager"], + "subGroups": [ + { + "name": "level2", + "realmRoles": ["user"], + "attributes": { + "level2Attribute": ["true"] + + } + } + ] + } + ], "roles" : { "realm" : [ { diff --git a/testsuite/tomcat6/src/test/resources/keycloak-saml/testsaml.json b/testsuite/tomcat6/src/test/resources/keycloak-saml/testsaml.json index 0b13fb9e9e..04c5dcdf0f 100755 --- a/testsuite/tomcat6/src/test/resources/keycloak-saml/testsaml.json +++ b/testsuite/tomcat6/src/test/resources/keycloak-saml/testsaml.json @@ -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,26 @@ } } ], + "groups" : [ + { + "name": "top", + "attributes": { + "topAttribute": ["true"] + + }, + "realmRoles": ["manager"], + "subGroups": [ + { + "name": "level2", + "realmRoles": ["user"], + "attributes": { + "level2Attribute": ["true"] + + } + } + ] + } + ], "roles" : { "realm" : [ { diff --git a/testsuite/tomcat7/src/test/resources/keycloak-saml/testsaml.json b/testsuite/tomcat7/src/test/resources/keycloak-saml/testsaml.json index 0b13fb9e9e..04c5dcdf0f 100755 --- a/testsuite/tomcat7/src/test/resources/keycloak-saml/testsaml.json +++ b/testsuite/tomcat7/src/test/resources/keycloak-saml/testsaml.json @@ -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,26 @@ } } ], + "groups" : [ + { + "name": "top", + "attributes": { + "topAttribute": ["true"] + + }, + "realmRoles": ["manager"], + "subGroups": [ + { + "name": "level2", + "realmRoles": ["user"], + "attributes": { + "level2Attribute": ["true"] + + } + } + ] + } + ], "roles" : { "realm" : [ { diff --git a/testsuite/tomcat8/src/test/resources/keycloak-saml/testsaml.json b/testsuite/tomcat8/src/test/resources/keycloak-saml/testsaml.json index 0b13fb9e9e..04c5dcdf0f 100755 --- a/testsuite/tomcat8/src/test/resources/keycloak-saml/testsaml.json +++ b/testsuite/tomcat8/src/test/resources/keycloak-saml/testsaml.json @@ -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,26 @@ } } ], + "groups" : [ + { + "name": "top", + "attributes": { + "topAttribute": ["true"] + + }, + "realmRoles": ["manager"], + "subGroups": [ + { + "name": "level2", + "realmRoles": ["user"], + "attributes": { + "level2Attribute": ["true"] + + } + } + ] + } + ], "roles" : { "realm" : [ {