keycloak-uncached

Merge pull request #1838 from patriot1burke/master groups:

11/18/2015 7:19:17 PM

Changes

Details

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 ae9701b..67cb127 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 07865db..8635014 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<SocialLinkRepresentation> socialLinks;
 
+    protected List<String> groups;
+
     public String getSelf() {
         return self;
     }
@@ -216,4 +218,12 @@ public class UserRepresentation {
     public void setServiceAccountClientId(String serviceAccountClientId) {
         this.serviceAccountClientId = serviceAccountClientId;
     }
+
+    public List<String> getGroups() {
+        return groups;
+    }
+
+    public void setGroups(List<String> groups) {
+        this.groups = groups;
+    }
 }
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 30eb055..12b358e 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<String> 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 4e3b986..811343b 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 6c20930..50bc11b 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 @@
             <th>Last Name</th>
             <th>First Name</th>
             <th>Email</th>
-            <th>Actions</th>
+            <th></th>
         </tr>
         </tr>
         </thead>
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 0000000..84af76e
--- /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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public interface GroupResource {
+
+    /**
+     * Does not expand hierarchy.  Subgroups will not be set.
+     *
+     * @return
+     */
+    @GET
+    @NoCache
+    @Produces(MediaType.APPLICATION_JSON)
+    public GroupRepresentation toRepresentation();
+
+    /**
+     * Update group
+     *
+     * @param rep
+     */
+    @PUT
+    @Consumes(MediaType.APPLICATION_JSON)
+    public void update(GroupRepresentation rep);
+
+    @DELETE
+    public void remove();
+
+
+    /**
+     * Set or create child.  This will just set the parent if it exists.  Create it and set the parent
+     * if the group doesn't exist.
+     *
+     * @param rep
+     */
+    @POST
+    @Path("children")
+    @NoCache
+    @Produces(MediaType.APPLICATION_JSON)
+    @Consumes(MediaType.APPLICATION_JSON)
+    public Response subGroup(GroupRepresentation rep);
+
+
+    @Path("role-mappings")
+    public RoleMappingResource roles();
+
+    /**
+     * Get users
+     * <p/>
+     * Returns a list of users, filtered according to query parameters
+     *
+     * @param firstResult Pagination offset
+     * @param maxResults  Pagination size
+     * @return
+     */
+    @GET
+    @NoCache
+    @Path("/members")
+    @Produces(MediaType.APPLICATION_JSON)
+    public List<UserRepresentation> members(@QueryParam("first") Integer firstResult,
+                                            @QueryParam("max") Integer maxResults);
+}
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 0000000..f917d49
--- /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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public interface GroupsResource {
+    @GET
+    @NoCache
+    @Produces(MediaType.APPLICATION_JSON)
+    public List<GroupRepresentation> groups();
+
+    /**
+     * create or add a top level realm groupSet or create child.  This will update the group and set the parent if it exists.  Create it and set the parent
+     * if the group doesn't exist.
+     *
+     * @param rep
+     */
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON)
+    public Response add(GroupRepresentation rep);
+
+    @Path("{id}")
+    public GroupResource group(@PathParam("id") String id);
+
+}
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 82b023b..8ac199c
--- 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 a2490b6..cc70e24 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<GroupRepresentation> groups();
+
+    @Path("groups/{groupId}")
+    @PUT
+    void joinGroup(@PathParam("groupId") String groupId);
+
+    @Path("groups/{groupId}")
+    @DELETE
+    void leaveGroup(@PathParam("groupId") String groupId);
+
+
+
+
     @POST
     @Path("logout")
     public void logout();
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 c3bb5c2..641f37e 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 d75ed95..1a1709b 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 02caa74..4126daa 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<String>  resolveAttribute(GroupModel group, String name) {
+        List<String> 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<String> resolveAttribute(UserModel user, String name) {
+        List<String> 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 32ac72a..b1d71ec 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<GroupRepresentation> groups = toGroupHierarchy(realm, true);
+        rep.setGroups(groups);
+    }
+
     public static void exportAuthenticationFlows(RealmModel realm, RealmRepresentation rep) {
         rep.setAuthenticationFlows(new LinkedList<AuthenticationFlowRepresentation>());
         rep.setAuthenticatorConfig(new LinkedList<AuthenticatorConfigRepresentation>());
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 a31d355..bb9dbc8 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<GroupRepresentation> groups = rep.getGroups();
+        if (groups == null) return;
+
+        Map<String, ClientModel> clientMap = realm.getClientNameMap();
+        GroupModel parent = null;
+        for (GroupRepresentation group : groups) {
+            importGroup(realm, clientMap, parent, group);
+        }
+    }
+
+    public static void importGroup(RealmModel realm, Map<String, ClientModel> clientMap, GroupModel parent, GroupRepresentation group) {
+        GroupModel newGroup = realm.createGroup(group.getId(), group.getName());
+        if (group.getAttributes() != null) {
+            for (Map.Entry<String, List<String>> attr : group.getAttributes().entrySet()) {
+                newGroup.setAttribute(attr.getKey(), attr.getValue());
+            }
+        }
+        realm.moveGroup(newGroup, parent);
+
+        if (group.getRealmRoles() != null) {
+            for (String roleString : group.getRealmRoles()) {
+                RoleModel role = realm.getRole(roleString.trim());
+                if (role == null) {
+                    role = realm.addRole(roleString.trim());
+                }
+                newGroup.grantRole(role);
+            }
+        }
+        if (group.getClientRoles() != null) {
+            for (Map.Entry<String, List<String>> entry : group.getClientRoles().entrySet()) {
+                ClientModel client = clientMap.get(entry.getKey());
+                if (client == null) {
+                    throw new RuntimeException("Unable to find client role mappings for client: " + entry.getKey());
+                }
+                List<String> roleNames = entry.getValue();
+                for (String roleName : roleNames) {
+                    RoleModel role = client.getRole(roleName.trim());
+                    if (role == null) {
+                        role = client.addRole(roleName.trim());
+                    }
+                    newGroup.grantRole(role);
+
+                }
+            }
+        }
+        if (group.getSubGroups() != null) {
+            for (GroupRepresentation subGroup : group.getSubGroups()) {
+                importGroup(realm, clientMap, newGroup, subGroup);
+            }
+        }
+    }
+
     public static void importAuthenticationFlows(RealmModel newRealm, RealmRepresentation rep) {
         if (rep.getAuthenticationFlows() == null) {
             // assume this is an old version being imported
@@ -999,6 +1059,16 @@ public class RepresentationToModel {
             }
             user.setServiceAccountClientLink(client.getId());;
         }
+        if (userRep.getGroups() != null) {
+            for (String path : userRep.getGroups()) {
+                GroupModel group = KeycloakModelUtils.findGroupByPath(newRealm, path);
+                if (group == null) {
+                    throw new RuntimeException("Unable to find group specified by path: " + path);
+
+                }
+                user.joinGroup(group);
+            }
+        }
         return user;
     }
 
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 be9fe7c..68e62bf 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
@@ -1308,6 +1308,12 @@ public class RealmAdapter implements RealmModel {
     }
 
     @Override
+    public GroupModel createGroup(String id, String name) {
+        getDelegateForUpdate();
+        return updated.createGroup(id, name);
+    }
+
+    @Override
     public void addTopLevelGroup(GroupModel subGroup) {
         getDelegateForUpdate();
         updated.addTopLevelGroup(subGroup);
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 7113fde..8240005 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<GroupModel> getGroups() {
         if (updated != null) return updated.getGroups();
         Set<GroupModel> groups = new HashSet<GroupModel>();
-        for (String id : cached.getRoleMappings()) {
+        for (String id : cached.getGroups()) {
             GroupModel groupModel = keycloakSession.realms().getGroupById(id, realm);
             if (groupModel == null) {
                 // chance that role was removed, so just delete to persistence and get user invalidated
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 78de054..40c2568 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<RoleEntity> roles = new ArrayList<RoleEntity>();
 
-    @OneToMany(fetch = FetchType.LAZY, cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "realm")
-    Collection<GroupEntity> groups = new ArrayList<GroupEntity>();
-
     @ElementCollection
     @MapKeyColumn(name="NAME")
     @Column(name="VALUE")
@@ -722,20 +719,5 @@ public class RealmEntity {
         this.clientAuthenticationFlow = clientAuthenticationFlow;
     }
 
-    public Collection<GroupEntity> getGroups() {
-        return groups;
-    }
-
-    public void setGroups(Collection<GroupEntity> groups) {
-        this.groups = groups;
-    }
-
-    public void addGroup(GroupEntity group) {
-        if (groups == null) {
-            groups = new ArrayList<GroupEntity>();
-        }
-        groups.add(group);
-    }
-
 }
 
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 209d2ae..0fb9c2f 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 4ec5d66..09b4fa2 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 5f8ce7f..61e4982 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 7b8e3a6..eae002b 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<GroupModel> getGroups() {
         List<GroupModel> list = new LinkedList<>();
-        Collection<GroupEntity> groups = realm.getGroups();
+        Collection<GroupEntity> groups =  em.createNamedQuery("getAllGroupsByRealm").setParameter("realm", realm).getResultList();
         if (groups == null) return list;
         for (GroupEntity entity : groups) {
             list.add(new GroupAdapter(this, em, entity));
@@ -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/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 d261c6d..180eb9d 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<MongoGroupEntity> impleme
         if (group.getRoleIds() == null || group.getRoleIds().isEmpty()) return Collections.EMPTY_SET;
         Set<RoleModel> 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<MongoGroupEntity> impleme
 
     @Override
     public Set<GroupModel> getSubGroups() {
+        DBObject query = new QueryBuilder()
+                .and("realmId").is(realm.getId())
+                .and("parentId").is(getId())
+                .get();
+        List<MongoGroupEntity> groups = getMongoStore().loadEntities(MongoGroupEntity.class, query, invocationContext);
+
         Set<GroupModel> 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 30266af..2fe85a1 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<MongoRealmEntity> impleme
 
     @Override
     public GroupModel createGroup(String name) {
+        String id = KeycloakModelUtils.generateId();
+        return createGroup(id, name);
+    }
+
+    @Override
+    public GroupModel createGroup(String id, String name) {
+        if (id == null) id = KeycloakModelUtils.generateId();
         MongoGroupEntity group = new MongoGroupEntity();
-        group.setId(KeycloakModelUtils.generateId());
+        group.setId(id);
         group.setName(name);
         group.setRealmId(getId());
 
@@ -653,7 +660,7 @@ public class RealmAdapter extends AbstractMongoAdapter<MongoRealmEntity> 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<MongoRealmEntity> impleme
         Iterator<GroupModel> 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 8269360..78f1076 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
@@ -20,6 +20,10 @@ public class MongoRealmEntity extends RealmEntity implements MongoIdentifiableEn
                 .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);
 
         // Remove all clients of this realm
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 29491f8..520f678 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<MongoGroupEntity> 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 0000000..2253e5c
--- /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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class GroupMembershipMapper extends AbstractSAMLProtocolMapper implements SAMLAttributeStatementMapper {
+    public static final String PROVIDER_ID = "saml-group-membership-mapper";
+    public static final String SINGLE_GROUP_ATTRIBUTE = "single";
+
+    private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
+
+    static {
+        ProviderConfigProperty property;
+        property = new ProviderConfigProperty();
+        property.setName(AttributeStatementHelper.SAML_ATTRIBUTE_NAME);
+        property.setLabel("Group attribute name");
+        property.setDefaultValue("member");
+        property.setHelpText("Name of the SAML attribute you want to put your groups into.  i.e. 'member', 'memberOf'.");
+        configProperties.add(property);
+        property = new ProviderConfigProperty();
+        property.setName(AttributeStatementHelper.FRIENDLY_NAME);
+        property.setLabel(AttributeStatementHelper.FRIENDLY_NAME_LABEL);
+        property.setHelpText(AttributeStatementHelper.FRIENDLY_NAME_HELP_TEXT);
+        configProperties.add(property);
+        property = new ProviderConfigProperty();
+        property.setName(AttributeStatementHelper.SAML_ATTRIBUTE_NAMEFORMAT);
+        property.setLabel("SAML Attribute NameFormat");
+        property.setHelpText("SAML Attribute NameFormat.  Can be basic, URI reference, or unspecified.");
+        List<String> types = new ArrayList(3);
+        types.add(AttributeStatementHelper.BASIC);
+        types.add(AttributeStatementHelper.URI_REFERENCE);
+        types.add(AttributeStatementHelper.UNSPECIFIED);
+        property.setType(ProviderConfigProperty.LIST_TYPE);
+        property.setDefaultValue(types);
+        configProperties.add(property);
+        property = new ProviderConfigProperty();
+        property.setName(SINGLE_GROUP_ATTRIBUTE);
+        property.setLabel("Single Group Attribute");
+        property.setType(ProviderConfigProperty.BOOLEAN_TYPE);
+        property.setDefaultValue("true");
+        property.setHelpText("If true, all groups will be stored under one attribute with multiple attribute values.");
+        configProperties.add(property);
+        property = new ProviderConfigProperty();
+        property.setName("full.path");
+        property.setLabel("Full group path");
+        property.setType(ProviderConfigProperty.BOOLEAN_TYPE);
+        property.setDefaultValue("true");
+        property.setHelpText("Include full path to group i.e. /top/level1/level2, false will just specify the group name");
+        configProperties.add(property);
+
+
+    }
+
+
+    @Override
+    public String getDisplayCategory() {
+        return "Group Mapper";
+    }
+
+    @Override
+    public String getDisplayType() {
+        return "Group list";
+    }
+
+    @Override
+    public String getHelpText() {
+        return "Group names are stored in an attribute value.  There is either one attribute with multiple attribute values, or an attribute per group name depending on how you configure it.  You can also specify the attribute name i.e. 'member' or 'memberOf' being examples.";
+    }
+
+    @Override
+    public List<ProviderConfigProperty> getConfigProperties() {
+        return configProperties;
+    }
+
+    @Override
+    public String getId() {
+        return PROVIDER_ID;
+    }
+
+    public static boolean useFullPath(ProtocolMapperModel mappingModel) {
+        return "true".equals(mappingModel.getConfig().get("full.path"));
+    }
+
+
+    @Override
+    public void transformAttributeStatement(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) {
+        String single = mappingModel.getConfig().get(SINGLE_GROUP_ATTRIBUTE);
+        boolean singleAttribute = Boolean.parseBoolean(single);
+
+        boolean fullPath = useFullPath(mappingModel);
+        AttributeType singleAttributeType = null;
+        for (GroupModel group : userSession.getUser().getGroups()) {
+            String groupName;
+            if (fullPath) {
+                groupName = ModelToRepresentation.buildGroupPath(group);
+            } else {
+                groupName = group.getName();
+            }
+            AttributeType attributeType = null;
+            if (singleAttribute) {
+                if (singleAttributeType == null) {
+                    singleAttributeType = AttributeStatementHelper.createAttributeType(mappingModel);
+                    attributeStatement.addAttribute(new AttributeStatementType.ASTChoiceType(singleAttributeType));
+                }
+                attributeType = singleAttributeType;
+            } else {
+                attributeType = AttributeStatementHelper.createAttributeType(mappingModel);
+                attributeStatement.addAttribute(new AttributeStatementType.ASTChoiceType(attributeType));
+            }
+            attributeType.addAttributeValue(groupName);
+        }
+    }
+
+    public static ProtocolMapperModel create(String name, String samlAttributeName, String nameFormat, String friendlyName, boolean singleAttribute) {
+        ProtocolMapperModel mapper = new ProtocolMapperModel();
+        mapper.setName(name);
+        mapper.setProtocolMapper(PROVIDER_ID);
+        mapper.setProtocol(SamlProtocol.LOGIN_PROTOCOL);
+        mapper.setConsentRequired(false);
+        Map<String, String> config = new HashMap<String, String>();
+        config.put(AttributeStatementHelper.SAML_ATTRIBUTE_NAME, samlAttributeName);
+        if (friendlyName != null) {
+            config.put(AttributeStatementHelper.FRIENDLY_NAME, friendlyName);
+        }
+        if (nameFormat != null) {
+            config.put(AttributeStatementHelper.SAML_ATTRIBUTE_NAMEFORMAT, nameFormat);
+        }
+        config.put(SINGLE_GROUP_ATTRIBUTE, Boolean.toString(singleAttribute));
+        mapper.setConfig(config);
+
+        return mapper;
+    }
+
+}
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 0000000..87a48c9
--- /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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @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 dc80760..8f5e3b8 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 163fed5..df62632 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/mappers/GroupMembershipMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/GroupMembershipMapper.java
new file mode 100755
index 0000000..848a8e8
--- /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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class GroupMembershipMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper, OIDCIDTokenMapper {
+
+    private static final List<ProviderConfigProperty> configProperties = new ArrayList<ProviderConfigProperty>();
+
+    static {
+        ProviderConfigProperty property;
+        ProviderConfigProperty property1;
+        property1 = new ProviderConfigProperty();
+        property1.setName(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME);
+        property1.setLabel(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME_LABEL);
+        property1.setType(ProviderConfigProperty.STRING_TYPE);
+        property1.setDefaultValue("groups");
+        property1.setHelpText(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME_TOOLTIP);
+        configProperties.add(property1);
+        property1 = new ProviderConfigProperty();
+        property1.setName("full.path");
+        property1.setLabel("Full group path");
+        property1.setType(ProviderConfigProperty.BOOLEAN_TYPE);
+        property1.setDefaultValue("true");
+        property1.setHelpText("Include full path to group i.e. /top/level1/level2, false will just specify the group name");
+        configProperties.add(property1);
+
+        property1 = new ProviderConfigProperty();
+        property1.setName(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN);
+        property1.setLabel(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN_LABEL);
+        property1.setType(ProviderConfigProperty.BOOLEAN_TYPE);
+        property1.setDefaultValue("true");
+        property1.setHelpText(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN_HELP_TEXT);
+        configProperties.add(property1);
+        property1 = new ProviderConfigProperty();
+        property1.setName(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN);
+        property1.setLabel(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN_LABEL);
+        property1.setType(ProviderConfigProperty.BOOLEAN_TYPE);
+        property1.setDefaultValue("true");
+        property1.setHelpText(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN_HELP_TEXT);
+        configProperties.add(property1);
+
+
+
+    }
+
+    public static final String PROVIDER_ID = "oidc-group-membership-mapper";
+
+
+    public List<ProviderConfigProperty> getConfigProperties() {
+        return configProperties;
+    }
+
+    @Override
+    public String getId() {
+        return PROVIDER_ID;
+    }
+
+    @Override
+    public String getDisplayType() {
+        return "Group Membership";
+    }
+
+    @Override
+    public String getDisplayCategory() {
+        return TOKEN_MAPPER_CATEGORY;
+    }
+
+    @Override
+    public String getHelpText() {
+        return "Map user group membership";
+    }
+
+    public static boolean useFullPath(ProtocolMapperModel mappingModel) {
+        return "true".equals(mappingModel.getConfig().get("full.path"));
+    }
+
+
+    @Override
+    public AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session,
+                                            UserSessionModel userSession, ClientSessionModel clientSession) {
+        if (!OIDCAttributeMapperHelper.includeInAccessToken(mappingModel)) return token;
+        buildMembership(token, mappingModel, userSession);
+        return token;
+    }
+
+    public void buildMembership(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession) {
+        List<String> membership = new LinkedList<>();
+        boolean fullPath = useFullPath(mappingModel);
+        for (GroupModel group : userSession.getUser().getGroups()) {
+            if (fullPath) {
+                membership.add(ModelToRepresentation.buildGroupPath(group));
+            } else {
+                membership.add(group.getName());
+            }
+        }
+        String protocolClaim = mappingModel.getConfig().get(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME);
+
+        token.getOtherClaims().put(protocolClaim, membership);
+    }
+
+    @Override
+    public IDToken transformIDToken(IDToken token, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession) {
+        if (!OIDCAttributeMapperHelper.includeInIDToken(mappingModel)) return token;
+        buildMembership(token, mappingModel, userSession);
+        return token;
+    }
+
+    public static ProtocolMapperModel create(String name,
+                                      String tokenClaimName,
+                                      boolean consentRequired, String consentText,
+                                      boolean accessToken, boolean idToken) {
+        ProtocolMapperModel mapper = new ProtocolMapperModel();
+        mapper.setName(name);
+        mapper.setProtocolMapper(PROVIDER_ID);
+        mapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+        mapper.setConsentRequired(consentRequired);
+        mapper.setConsentText(consentText);
+        Map<String, String> config = new HashMap<String, String>();
+        config.put(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME, tokenClaimName);
+        if (accessToken) config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, "true");
+        if (idToken) config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN, "true");
+        mapper.setConfig(config);
+        
+        return mapper;
+    }
+
+
+}
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 246b82e..3e96692 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<String> attributeValue = user.getAttribute(attributeName);
+        List<String> attributeValue = KeycloakModelUtils.resolveAttribute(user, attributeName);
         if (attributeValue == null) return;
         OIDCAttributeMapperHelper.mapClaim(token, mappingModel, attributeValue);
     }
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 17d5b23..32a9a6d 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<RoleModel> roleMappings) {
+        roleMappings.addAll(group.getRoleMappings());
+        if (group.getParentId() == null) return;
+        addGroupRoles(group.getParent(), roleMappings);
+    }
+
     public static Set<RoleModel> getAccess(String scopeParam, boolean applyScopeParam, ClientModel client, UserModel user) {
         Set<RoleModel> requestedRoles = new HashSet<RoleModel>();
 
-        Set<RoleModel> roleMappings = user.getRoleMappings();
+        Set<RoleModel> mappings = user.getRoleMappings();
+        Set<RoleModel> 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/services/resources/admin/GroupResource.java b/services/src/main/java/org/keycloak/services/resources/admin/GroupResource.java
index 87253c1..668057e 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<GroupRepresentation> getGroups() {
-        this.auth.requireView();
-        return ModelToRepresentation.toGroupHierarchy(realm, false);
-    }
-
-    /**
-     * 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) {
+    public GroupRepresentation getGroup() {
         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);
+        return ModelToRepresentation.toGroupHierarchy(group, true);
     }
 
     /**
-     * Update group
+     * Update group, ignores subgroups.
      *
      * @param rep
      */
     @PUT
-    @Path("{id}")
     @Consumes(MediaType.APPLICATION_JSON)
-    public void updateGroup(@PathParam("id") String id, GroupRepresentation rep) {
-        GroupModel model = realm.getGroupById(id);
-        if (model == null) {
-            throw new NotFoundException("Could not find group by id");
-        }
-
-        updateGroup(rep, model);
+    public void updateGroup(GroupRepresentation rep) {
+        updateGroup(rep, group);
 
 
     }
 
     @DELETE
-    @Path("{id}")
-    public void deleteGroup(@PathParam("id") String id) {
-        GroupModel model = realm.getGroupById(id);
-        if (model == null) {
-            throw new NotFoundException("Could not find group by id");
-        }
-        realm.removeGroup(model);
+    public void deleteGroup() {
+        realm.removeGroup(group);
     }
 
 
@@ -172,16 +96,12 @@ public class GroupResource {
      * @param rep
      */
     @POST
-    @Path("{id}/children")
+    @Path("children")
     @NoCache
     @Produces(MediaType.APPLICATION_JSON)
     @Consumes(MediaType.APPLICATION_JSON)
-    public Response addGroup(@PathParam("id") String parentId, GroupRepresentation rep) {
-        GroupModel parentModel = realm.getGroupById(parentId);
+    public Response addChild(GroupRepresentation rep) {
         Response.ResponseBuilder builder = Response.status(204);
-        if (parentModel == null) {
-            throw new NotFoundException("Could not find parent by id");
-        }
         GroupModel child = null;
         if (rep.getId() != null) {
             child = realm.getGroupById(rep.getId());
@@ -197,39 +117,12 @@ public class GroupResource {
             builder.status(201).location(uri);
 
         }
-        realm.moveGroup(child, parentModel);
-        GroupRepresentation childRep = ModelToRepresentation.toRepresentation(child, true);
+        realm.moveGroup(child, group);
+        GroupRepresentation childRep = ModelToRepresentation.toGroupHierarchy(child, true);
         return builder.type(MediaType.APPLICATION_JSON_TYPE).entity(childRep).build();
     }
 
-    /**
-     * create or add a top level realm groupSet or create child.  This will update the group and set the parent if it exists.  Create it and set the parent
-     * if the group doesn't exist.
-     *
-     * @param rep
-     */
-    @POST
-    @Consumes(MediaType.APPLICATION_JSON)
-    public Response addTopLevelGroup(GroupRepresentation rep) {
-        GroupModel child = null;
-        Response.ResponseBuilder builder = Response.status(204);
-        if (rep.getId() != null) {
-            child = realm.getGroupById(rep.getId());
-            if (child == null) {
-                throw new NotFoundException("Could not find child by id");
-            }
-        } else {
-            child = realm.createGroup(rep.getName());
-            updateGroup(rep, child);
-            URI uri = uriInfo.getAbsolutePathBuilder()
-                    .path(child.getId()).build();
-            builder.status(201).location(uri);
-        }
-        realm.moveGroup(child, null);
-        return builder.build();
-    }
-
-    public void updateGroup(GroupRepresentation rep, GroupModel model) {
+    public static void updateGroup(GroupRepresentation rep, GroupModel model) {
         if (rep.getName() != null) model.setName(rep.getName());
 
         if (rep.getAttributes() != null) {
@@ -245,13 +138,8 @@ public class GroupResource {
         }
     }
 
-    @Path("{id}/role-mappings")
-    public RoleMapperResource getRoleMappings(@PathParam("id") String id) {
-
-        GroupModel group = session.realms().getGroupById(id, realm);
-        if (group == null) {
-            throw new NotFoundException("Group not found");
-        }
+    @Path("role-mappings")
+    public RoleMapperResource getRoleMappings() {
         auth.init(RealmAuth.Resource.USER);
 
         RoleMapperResource resource =  new RoleMapperResource(realm, auth, group, adminEvent);
@@ -271,18 +159,11 @@ public class GroupResource {
      */
     @GET
     @NoCache
-    @Path("{id}/members")
+    @Path("members")
     @Produces(MediaType.APPLICATION_JSON)
-    public List<UserRepresentation> getMembers(@PathParam("id") String id,
-                                               @QueryParam("first") Integer firstResult,
+    public List<UserRepresentation> getMembers(@QueryParam("first") Integer firstResult,
                                                @QueryParam("max") Integer maxResults) {
         auth.requireView();
-
-        GroupModel group = session.realms().getGroupById(id, realm);
-        if (group == null) {
-            throw new NotFoundException("Group not found");
-        }
-
         firstResult = firstResult != null ? firstResult : -1;
         maxResults = maxResults != null ? maxResults : -1;
 
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 0000000..6e3709d
--- /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<GroupRepresentation> getGroups() {
+        this.auth.requireView();
+        return ModelToRepresentation.toGroupHierarchy(realm, false);
+    }
+
+    /**
+     * Does not expand hierarchy.  Subgroups will not be set.
+     *
+     * @param id
+     * @return
+     */
+    @Path("{id}")
+    public GroupResource getGroupById(@PathParam("id") String id) {
+        GroupModel group = realm.getGroupById(id);
+        if (group == null) {
+            throw new NotFoundException("Could not find group by id");
+        }
+
+        GroupResource resource =  new GroupResource(realm, group, session, this.auth, adminEvent);
+        ResteasyProviderFactory.getInstance().injectProperties(resource);
+        return resource;
+    }
+
+    /**
+     * create or add a top level realm groupSet or create child.  This will update the group and set the parent if it exists.  Create it and set the parent
+     * if the group doesn't exist.
+     *
+     * @param rep
+     */
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON)
+    public Response addTopLevelGroup(GroupRepresentation rep) {
+        GroupModel child = null;
+        Response.ResponseBuilder builder = Response.status(204);
+        if (rep.getId() != null) {
+            child = realm.getGroupById(rep.getId());
+            if (child == null) {
+                throw new NotFoundException("Could not find child by id");
+            }
+        } else {
+            child = realm.createGroup(rep.getName());
+            GroupResource.updateGroup(rep, child);
+            URI uri = uriInfo.getAbsolutePathBuilder()
+                    .path(child.getId()).build();
+            builder.status(201).location(uri);
+        }
+        realm.moveGroup(child, null);
+        return builder.build();
+    }
+}
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 36af035..9845031 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 e7a6450..59f0f29 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 1fc4fc2..2f89194 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 2c32d51..fbced9f 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<String> requiredRoles = new LinkedList<>();
+            requiredRoles.add("manager");
+            requiredRoles.add("user");
+            SendUsernameServlet.checkRoles = requiredRoles;
+            loginPage.login("level2GroupUser", "password");
+            assertEquals(driver.getCurrentUrl(), APP_SERVER_BASE_URL + "/employee2/");
+            SendUsernameServlet.checkRoles = null;
+            SamlPrincipal principal = (SamlPrincipal) SendUsernameServlet.sentPrincipal;
+            Assert.assertNotNull(principal);
+            assertEquals("level2@redhat.com", principal.getAttribute(X500SAMLProfileConstants.EMAIL.get()));
+            assertEquals("true", principal.getAttribute("topAttribute"));
+            assertEquals("true", principal.getAttribute("level2Attribute"));
+            List<String> groups = principal.getAttributes("group");
+            Assert.assertNotNull(groups);
+            Set<String> groupSet = new HashSet<>();
+            assertEquals("level2@redhat.com", principal.getFriendlyAttribute("email"));
+            driver.navigate().to(APP_SERVER_BASE_URL + "/employee2/?GLO=true");
+            checkLoggedOut(APP_SERVER_BASE_URL + "/employee2/");
+
+        }
         {
             SendUsernameServlet.sentPrincipal = null;
             SendUsernameServlet.checkRoles = null;
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 0000000..d2882dc
--- /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 <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
+ */
+public class GroupTest {
+
+    @ClassRule
+    public static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() {
+        @Override
+        public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+            ClientModel app = new ClientManager(manager).createClient(appRealm, "resource-owner");
+            app.setSecret("secret");
+
+            UserModel user = session.users().addUser(appRealm, "direct-login");
+            user.setEmail("direct-login@localhost");
+            user.setEnabled(true);
+
+
+            session.users().updateCredential(appRealm, user, UserCredentialModel.password("password"));
+            keycloak = Keycloak.getInstance("http://localhost:8081/auth", "master", "admin", "admin", Constants.ADMIN_CONSOLE_CLIENT_ID);
+        }
+    });
+
+    protected static Keycloak keycloak;
+
+    @Rule
+    public AssertEvents events = new AssertEvents(keycloakRule);
+
+    @Rule
+    public WebRule webRule = new WebRule(this);
+
+    @WebResource
+    protected WebDriver driver;
+
+    @WebResource
+    protected OAuthClient oauth;
+
+    @Test
+    public void createAndTestGroups() throws Exception {
+        RealmResource realm = keycloak.realms().realm("test");
+        {
+            RoleRepresentation groupRole = new RoleRepresentation();
+            groupRole.setName("topRole");
+            realm.roles().create(groupRole);
+        }
+        RoleRepresentation topRole = realm.roles().get("topRole").toRepresentation();
+        {
+            RoleRepresentation groupRole = new RoleRepresentation();
+            groupRole.setName("level2Role");
+            realm.roles().create(groupRole);
+        }
+        RoleRepresentation level2Role = realm.roles().get("level2Role").toRepresentation();
+        {
+            RoleRepresentation groupRole = new RoleRepresentation();
+            groupRole.setName("level3Role");
+            realm.roles().create(groupRole);
+        }
+        RoleRepresentation level3Role = realm.roles().get("level3Role").toRepresentation();
+
+
+        GroupRepresentation topGroup = new GroupRepresentation();
+        topGroup.setName("top");
+        Response response = realm.groups().add(topGroup);
+        response.close();
+        topGroup = realm.getGroupByPath("/top");
+        Assert.assertNotNull(topGroup);
+        List<RoleRepresentation> roles = new LinkedList<>();
+        roles.add(topRole);
+        realm.groups().group(topGroup.getId()).roles().realmLevel().add(roles);
+
+        GroupRepresentation level2Group = new GroupRepresentation();
+        level2Group.setName("level2");
+        response = realm.groups().group(topGroup.getId()).subGroup(level2Group);
+        response.close();
+        level2Group = realm.getGroupByPath("/top/level2");
+        Assert.assertNotNull(level2Group);
+        roles.clear();
+        roles.add(level2Role);
+        realm.groups().group(level2Group.getId()).roles().realmLevel().add(roles);
+
+        GroupRepresentation level3Group = new GroupRepresentation();
+        level3Group.setName("level3");
+        response = realm.groups().group(level2Group.getId()).subGroup(level3Group);
+        response.close();
+        level3Group = realm.getGroupByPath("/top/level2/level3");
+        Assert.assertNotNull(level3Group);
+        roles.clear();
+        roles.add(level3Role);
+        realm.groups().group(level3Group.getId()).roles().realmLevel().add(roles);
+
+        topGroup = realm.getGroupByPath("/top");
+        Assert.assertEquals(1, topGroup.getRealmRoles().size());
+        Assert.assertTrue(topGroup.getRealmRoles().contains("topRole"));
+        Assert.assertEquals(1, topGroup.getSubGroups().size());
+
+        level2Group = topGroup.getSubGroups().get(0);
+        Assert.assertEquals("level2", level2Group.getName());
+        Assert.assertEquals(1, level2Group.getRealmRoles().size());
+        Assert.assertTrue(level2Group.getRealmRoles().contains("level2Role"));
+        Assert.assertEquals(1, level2Group.getSubGroups().size());
+
+        level3Group = level2Group.getSubGroups().get(0);
+        Assert.assertEquals("level3", level3Group.getName());
+        Assert.assertEquals(1, level3Group.getRealmRoles().size());
+        Assert.assertTrue(level3Group.getRealmRoles().contains("level3Role"));
+
+        try {
+            GroupRepresentation notFound = realm.getGroupByPath("/notFound");
+            Assert.fail();
+        } catch (NotFoundException e) {
+
+        }
+        try {
+            GroupRepresentation notFound = realm.getGroupByPath("/top/notFound");
+            Assert.fail();
+        } catch (NotFoundException e) {
+
+        }
+
+        UserRepresentation user = realm.users().search("direct-login", -1, -1).get(0);
+        realm.users().get(user.getId()).joinGroup(level3Group.getId());
+        List<GroupRepresentation> membership = realm.users().get(user.getId()).groups();
+        Assert.assertEquals(1, membership.size());
+        Assert.assertEquals("level3", membership.get(0).getName());
+
+        AccessToken token = login("direct-login", "resource-owner", "secret", user.getId());
+        Assert.assertTrue(token.getRealmAccess().getRoles().contains("topRole"));
+        Assert.assertTrue(token.getRealmAccess().getRoles().contains("level2Role"));
+        Assert.assertTrue(token.getRealmAccess().getRoles().contains("level3Role"));
+
+
+    }
+
+    @Test
+    public void testGroupMappers() throws Exception {
+        keycloakRule.update(new KeycloakRule.KeycloakSetup() {
+            @Override
+            public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
+                ClientModel app = appRealm.getClientByClientId("test-app");
+                app.addProtocolMapper(GroupMembershipMapper.create("groups", "groups", false, null, true, true));
+                app.addProtocolMapper(UserAttributeMapper.createClaimMapper("topAttribute", "topAttribute", "topAttribute", ProviderConfigProperty.STRING_TYPE, false, null, true, true, false));
+                app.addProtocolMapper(UserAttributeMapper.createClaimMapper("level2Attribute", "level2Attribute", "level2Attribute", ProviderConfigProperty.STRING_TYPE, false, null, true, true, false));
+            }
+        }, "test");
+        RealmResource realm = keycloak.realms().realm("test");
+        {
+            UserRepresentation user = realm.users().search("topGroupUser", -1, -1).get(0);
+
+            AccessToken token = login(user.getUsername(), "test-app", "password", user.getId());
+            Assert.assertTrue(token.getRealmAccess().getRoles().contains("user"));
+            List<String> groups = (List<String>) token.getOtherClaims().get("groups");
+            Assert.assertNotNull(groups);
+            Assert.assertTrue(groups.size() == 1);
+            Assert.assertEquals("topGroup", groups.get(0));
+            Assert.assertEquals("true", token.getOtherClaims().get("topAttribute"));
+        }
+        {
+            UserRepresentation user = realm.users().search("level2GroupUser", -1, -1).get(0);
+
+            AccessToken token = login(user.getUsername(), "test-app", "password", user.getId());
+            Assert.assertTrue(token.getRealmAccess().getRoles().contains("user"));
+            Assert.assertTrue(token.getRealmAccess().getRoles().contains("admin"));
+            Assert.assertTrue(token.getResourceAccess("test-app").getRoles().contains("customer-user"));
+            List<String> groups = (List<String>) token.getOtherClaims().get("groups");
+            Assert.assertNotNull(groups);
+            Assert.assertTrue(groups.size() == 1);
+            Assert.assertEquals("level2group", groups.get(0));
+            Assert.assertEquals("true", token.getOtherClaims().get("topAttribute"));
+            Assert.assertEquals("true", token.getOtherClaims().get("level2Attribute"));
+        }
+
+    }
+
+    protected AccessToken login(String login, String clientId, String clientSecret, String userId) throws Exception {
+        oauth.clientId(clientId);
+
+        OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest(clientSecret, login, "password");
+
+        assertEquals(200, response.getStatusCode());
+
+        AccessToken accessToken = oauth.verifyToken(response.getAccessToken());
+        RefreshToken refreshToken = oauth.verifyRefreshToken(response.getRefreshToken());
+
+        events.expectLogin()
+                .client(clientId)
+                .user(userId)
+                .session(accessToken.getSessionState())
+                .detail(Details.RESPONSE_TYPE, "token")
+                .detail(Details.TOKEN_ID, accessToken.getId())
+                .detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
+                .detail(Details.USERNAME, login)
+                .removeDetail(Details.CODE_ID)
+                .removeDetail(Details.REDIRECT_URI)
+                .removeDetail(Details.CONSENT)
+                .assertEvent();
+        return accessToken;
+    }
+
+
+}
diff --git a/testsuite/integration/src/test/resources/adapter-test/demorealm.json b/testsuite/integration/src/test/resources/adapter-test/demorealm.json
index 5bf2bdd..b5cd399 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 8d5576e..95b9fb9 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 e16e3e2..b4718dd 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 0b13fb9..04c5dcd 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 0b13fb9..04c5dcd 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 0b13fb9..04c5dcd 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 0b13fb9..04c5dcd 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 0b13fb9..04c5dcd 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 0b13fb9..04c5dcd 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" : [
             {