keycloak-aplcache

Changes

Details

diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/group/GroupPolicyProvider.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/group/GroupPolicyProvider.java
new file mode 100644
index 0000000..2ae1bdc
--- /dev/null
+++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/group/GroupPolicyProvider.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.authorization.policy.provider.group;
+
+import static org.keycloak.models.utils.ModelToRepresentation.buildGroupPath;
+
+import java.util.function.Function;
+
+import org.keycloak.authorization.attribute.Attributes;
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.policy.evaluation.Evaluation;
+import org.keycloak.authorization.policy.provider.PolicyProvider;
+import org.keycloak.models.GroupModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.utils.ModelToRepresentation;
+import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class GroupPolicyProvider implements PolicyProvider {
+
+    private final Function<Policy, GroupPolicyRepresentation> representationFunction;
+
+    public GroupPolicyProvider(Function<Policy, GroupPolicyRepresentation> representationFunction) {
+        this.representationFunction = representationFunction;
+    }
+
+    @Override
+    public void evaluate(Evaluation evaluation) {
+        GroupPolicyRepresentation policy = representationFunction.apply(evaluation.getPolicy());
+        RealmModel realm = evaluation.getAuthorizationProvider().getRealm();
+        Attributes.Entry groupsClaim = evaluation.getContext().getIdentity().getAttributes().getValue(policy.getGroupsClaim());
+
+        if (groupsClaim == null || groupsClaim.isEmpty()) {
+            return;
+        }
+
+        for (GroupPolicyRepresentation.GroupDefinition definition : policy.getGroups()) {
+            GroupModel allowedGroup = realm.getGroupById(definition.getId());
+
+            for (int i = 0; i < groupsClaim.size(); i++) {
+                String group = groupsClaim.asString(i);
+
+                if (group.indexOf('/') != -1) {
+                    String allowedGroupPath = buildGroupPath(allowedGroup);
+                    if (group.equals(allowedGroupPath) || (definition.isExtendChildren() && group.startsWith(allowedGroupPath))) {
+                        evaluation.grant();
+                        return;
+                    }
+                }
+
+                // in case the group from the claim does not represent a path, we just check an exact name match
+                if (group.equals(allowedGroup.getName())) {
+                    evaluation.grant();
+                    return;
+                }
+            }
+        }
+    }
+
+    @Override
+    public void close() {
+
+    }
+}
\ No newline at end of file
diff --git a/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/group/GroupPolicyProviderFactory.java b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/group/GroupPolicyProviderFactory.java
new file mode 100644
index 0000000..d101c79
--- /dev/null
+++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/group/GroupPolicyProviderFactory.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.authorization.policy.provider.group;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.keycloak.Config;
+import org.keycloak.authorization.AuthorizationProvider;
+import org.keycloak.authorization.model.Policy;
+import org.keycloak.authorization.policy.provider.PolicyProvider;
+import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
+import org.keycloak.models.GroupModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.PolicyRepresentation;
+import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
+import org.keycloak.util.JsonSerialization;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class GroupPolicyProviderFactory implements PolicyProviderFactory<GroupPolicyRepresentation> {
+
+    private GroupPolicyProvider provider = new GroupPolicyProvider(policy -> toRepresentation(policy, new GroupPolicyRepresentation()));
+
+    @Override
+    public String getId() {
+        return "group";
+    }
+
+    @Override
+    public String getName() {
+        return "Group";
+    }
+
+    @Override
+    public String getGroup() {
+        return "Identity Based";
+    }
+
+    @Override
+    public PolicyProvider create(AuthorizationProvider authorization) {
+        return provider;
+    }
+
+    @Override
+    public PolicyProvider create(KeycloakSession session) {
+        return provider;
+    }
+
+    @Override
+    public GroupPolicyRepresentation toRepresentation(Policy policy, GroupPolicyRepresentation representation) {
+        representation.setGroupsClaim(policy.getConfig().get("groupsClaim"));
+        try {
+            representation.setGroups(new HashSet<>(Arrays.asList(JsonSerialization.readValue(policy.getConfig().get("groups"), GroupPolicyRepresentation.GroupDefinition[].class))));
+        } catch (IOException cause) {
+            throw new RuntimeException("Failed to deserialize groups", cause);
+        }
+        return representation;
+    }
+
+    @Override
+    public Class<GroupPolicyRepresentation> getRepresentationType() {
+        return GroupPolicyRepresentation.class;
+    }
+
+    @Override
+    public void onCreate(Policy policy, GroupPolicyRepresentation representation, AuthorizationProvider authorization) {
+        updatePolicy(policy, representation.getGroupsClaim(), representation.getGroups(), authorization);
+    }
+
+    @Override
+    public void onUpdate(Policy policy, GroupPolicyRepresentation representation, AuthorizationProvider authorization) {
+        updatePolicy(policy, representation.getGroupsClaim(), representation.getGroups(), authorization);
+    }
+
+    @Override
+    public void onImport(Policy policy, PolicyRepresentation representation, AuthorizationProvider authorization) {
+        try {
+            updatePolicy(policy, representation.getConfig().get("groupsClaim"), JsonSerialization.readValue(representation.getConfig().get("groups"), Set.class), authorization);
+        } catch (IOException cause) {
+            throw new RuntimeException("Failed to deserialize groups", cause);
+        }
+    }
+
+    @Override
+    public void onExport(Policy policy, PolicyRepresentation representation, AuthorizationProvider authorizationProvider) {
+
+    }
+
+    @Override
+    public void init(Config.Scope config) {
+
+    }
+
+    @Override
+    public void postInit(KeycloakSessionFactory factory) {
+        factory.register(event -> {
+        });
+    }
+
+    @Override
+    public void close() {
+
+    }
+
+    private void updatePolicy(Policy policy, String groupsClaim, Set<GroupPolicyRepresentation.GroupDefinition> groups, AuthorizationProvider authorization) {
+        if (groupsClaim == null) {
+            throw new RuntimeException("Group claims property not provided");
+        }
+
+        if (groups == null || groups.isEmpty()) {
+            throw new RuntimeException("You must provide at least one group");
+        }
+
+        Map<String, String> config = new HashMap<>(policy.getConfig());
+
+        config.put("groupsClaim", groupsClaim);
+
+        List<GroupModel> topLevelGroups = authorization.getRealm().getTopLevelGroups();
+
+        for (GroupPolicyRepresentation.GroupDefinition definition : groups) {
+            GroupModel group = null;
+
+            if (definition.getId() != null) {
+                group = authorization.getRealm().getGroupById(definition.getId());
+            }
+
+            if (group == null) {
+                String path = definition.getPath();
+                String canonicalPath = path.startsWith("/") ? path.substring(1, path.length()) : path;
+
+                if (canonicalPath != null) {
+                    String[] parts = canonicalPath.split("/");
+                    GroupModel parent = null;
+
+                    for (String part : parts) {
+                        if ("".trim().equals(part)) {
+                            continue;
+                        }
+                        if (parent == null) {
+                            parent = topLevelGroups.stream().filter(groupModel -> groupModel.getName().equals(part)).findFirst().orElseThrow(() -> new RuntimeException("Top level group with name [" + part + "] not found"));
+                        } else {
+                            group = parent.getSubGroups().stream().filter(groupModel -> groupModel.getName().equals(part)).findFirst().orElseThrow(() -> new RuntimeException("Group with name [" + part + "] not found"));
+                            parent = group;
+                        }
+                    }
+
+                    if (parts.length == 1) {
+                        group = parent;
+                    }
+                }
+            }
+
+            if (group == null) {
+                throw new RuntimeException("Group with id [" + definition.getId() + "] not found");
+            }
+
+            definition.setId(group.getId());
+            definition.setPath(null);
+        }
+
+        try {
+            config.put("groups", JsonSerialization.writeValueAsString(groups));
+        } catch (IOException cause) {
+            throw new RuntimeException("Failed to serialize groups", cause);
+        }
+
+        policy.setConfig(config);
+    }
+}
diff --git a/authz/policy/common/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory b/authz/policy/common/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory
index e4588f8..e6fa1cc 100644
--- a/authz/policy/common/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory
+++ b/authz/policy/common/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory
@@ -41,4 +41,5 @@ org.keycloak.authorization.policy.provider.role.RolePolicyProviderFactory
 org.keycloak.authorization.policy.provider.scope.ScopePolicyProviderFactory
 org.keycloak.authorization.policy.provider.time.TimePolicyProviderFactory
 org.keycloak.authorization.policy.provider.user.UserPolicyProviderFactory
-org.keycloak.authorization.policy.provider.client.ClientPolicyProviderFactory
\ No newline at end of file
+org.keycloak.authorization.policy.provider.client.ClientPolicyProviderFactory
+org.keycloak.authorization.policy.provider.group.GroupPolicyProviderFactory
\ No newline at end of file
diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/GroupPolicyRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/authorization/GroupPolicyRepresentation.java
new file mode 100644
index 0000000..c063f8f
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/idm/authorization/GroupPolicyRepresentation.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.representations.idm.authorization;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class GroupPolicyRepresentation extends AbstractPolicyRepresentation {
+
+    private String groupsClaim;
+    private Set<GroupDefinition> groups;
+
+    @Override
+    public String getType() {
+        return "group";
+    }
+
+    public String getGroupsClaim() {
+        return groupsClaim;
+    }
+
+    public void setGroupsClaim(String groupsClaim) {
+        this.groupsClaim = groupsClaim;
+    }
+
+    public Set<GroupDefinition> getGroups() {
+        return groups;
+    }
+
+    public void setGroups(Set<GroupDefinition> groups) {
+        this.groups = groups;
+    }
+
+    public void addGroup(String... ids) {
+        for (String id : ids) {
+            addGroup(id, false);
+        }
+    }
+
+    public void addGroup(String id, boolean extendChildren) {
+        if (groups == null) {
+            groups = new HashSet<>();
+        }
+        groups.add(new GroupDefinition(id, extendChildren));
+    }
+
+    public void addGroupPath(String... paths) {
+        for (String path : paths) {
+            addGroupPath(path, false);
+        }
+    }
+
+    public void addGroupPath(String path, boolean extendChildren) {
+        if (groups == null) {
+            groups = new HashSet<>();
+        }
+        groups.add(new GroupDefinition(null, path, extendChildren));
+    }
+
+    public void removeGroup(String... ids) {
+        if (groups != null) {
+            for (final String id : ids) {
+                if (!groups.remove(id)) {
+                    for (GroupDefinition group : new HashSet<>(groups)) {
+                        if (group.getPath().startsWith(id)) {
+                            groups.remove(group);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    public static class GroupDefinition {
+
+        private String id;
+        private String path;
+        private boolean extendChildren;
+
+        public GroupDefinition() {
+            this(null);
+        }
+
+        public GroupDefinition(String id) {
+            this(id, false);
+        }
+
+        public GroupDefinition(String id, boolean extendChildren) {
+            this(id, null, extendChildren);
+        }
+
+        public GroupDefinition(String id, String path, boolean extendChildren) {
+            this.id = id;
+            this.path = path;
+            this.extendChildren = extendChildren;
+        }
+
+        public String getId() {
+            return id;
+        }
+
+        public void setId(String id) {
+            this.id = id;
+        }
+
+        public String getPath() {
+            return path;
+        }
+
+        public void setPath(String path) {
+            this.path = path;
+        }
+
+        public boolean isExtendChildren() {
+            return extendChildren;
+        }
+
+        public void setExtendChildren(boolean extendChildren) {
+            this.extendChildren = extendChildren;
+        }
+    }
+}
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupPoliciesResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupPoliciesResource.java
new file mode 100644
index 0000000..1cc51b0
--- /dev/null
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupPoliciesResource.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.admin.client.resource;
+
+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.QueryParam;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import org.jboss.resteasy.annotations.cache.NoCache;
+import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface GroupPoliciesResource {
+
+    @POST
+    @Consumes(MediaType.APPLICATION_JSON)
+    @Produces(MediaType.APPLICATION_JSON)
+    Response create(GroupPolicyRepresentation representation);
+
+    @Path("{id}")
+    GroupPolicyResource findById(@PathParam("id") String id);
+
+    @Path("/search")
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    @NoCache
+    GroupPolicyRepresentation findByName(@QueryParam("name") String name);
+}
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupPolicyResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupPolicyResource.java
new file mode 100644
index 0000000..6171868
--- /dev/null
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupPolicyResource.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.admin.client.resource;
+
+import java.util.List;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+import org.jboss.resteasy.annotations.cache.NoCache;
+import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.PolicyRepresentation;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public interface GroupPolicyResource {
+
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    @NoCache
+    GroupPolicyRepresentation toRepresentation();
+
+    @PUT
+    @Consumes(MediaType.APPLICATION_JSON)
+    void update(GroupPolicyRepresentation representation);
+
+    @DELETE
+    void remove();
+
+    @Path("/associatedPolicies")
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    @NoCache
+    List<PolicyRepresentation> associatedPolicies();
+
+    @Path("/dependentPolicies")
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    @NoCache
+    List<PolicyRepresentation> dependentPolicies();
+
+    @Path("/resources")
+    @GET
+    @Produces("application/json")
+    @NoCache
+    List<ResourceRepresentation> resources();
+
+}
diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/PoliciesResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/PoliciesResource.java
index a0af5d4..9ced12c 100644
--- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/PoliciesResource.java
+++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/PoliciesResource.java
@@ -89,4 +89,7 @@ public interface PoliciesResource {
 
     @Path("client")
     ClientPoliciesResource client();
+
+    @Path("group")
+    GroupPoliciesResource group();
 }
diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/attribute/Attributes.java b/server-spi-private/src/main/java/org/keycloak/authorization/attribute/Attributes.java
index c83d9f8..0719bab 100644
--- a/server-spi-private/src/main/java/org/keycloak/authorization/attribute/Attributes.java
+++ b/server-spi-private/src/main/java/org/keycloak/authorization/attribute/Attributes.java
@@ -107,6 +107,10 @@ public interface Attributes {
             return values.length;
         }
 
+        public boolean isEmpty() {
+            return values.length == 0;
+        }
+
         public String asString(int idx) {
             if (idx >= values.length) {
                 throw new IllegalArgumentException("Invalid index [" + idx + "]. Values are [" + values + "].");
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/GroupPolicyManagementTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/GroupPolicyManagementTest.java
new file mode 100644
index 0000000..57d86a7
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/GroupPolicyManagementTest.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.admin.client.authorization;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import javax.ws.rs.NotFoundException;
+import javax.ws.rs.core.Response;
+
+import org.junit.Test;
+import org.keycloak.admin.client.resource.AuthorizationResource;
+import org.keycloak.admin.client.resource.GroupPoliciesResource;
+import org.keycloak.admin.client.resource.GroupPolicyResource;
+import org.keycloak.admin.client.resource.PolicyResource;
+import org.keycloak.admin.client.resource.RolePoliciesResource;
+import org.keycloak.admin.client.resource.RolePolicyResource;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.GroupRepresentation;
+import org.keycloak.representations.idm.RoleRepresentation;
+import org.keycloak.representations.idm.authorization.DecisionStrategy;
+import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.Logic;
+import org.keycloak.representations.idm.authorization.PolicyRepresentation;
+import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
+import org.keycloak.testsuite.util.GroupBuilder;
+import org.keycloak.testsuite.util.RealmBuilder;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class GroupPolicyManagementTest extends AbstractPolicyManagementTest {
+
+    @Override
+    protected RealmBuilder createTestRealm() {
+        return super.createTestRealm().group(GroupBuilder.create().name("Group A")
+                .subGroups(Arrays.asList("Group B", "Group D").stream().map(name -> {
+                    if ("Group B".equals(name)) {
+                        return GroupBuilder.create().name(name).subGroups(Arrays.asList("Group C", "Group E").stream().map(new Function<String, GroupRepresentation>() {
+                            @Override
+                            public GroupRepresentation apply(String name) {
+                                return GroupBuilder.create().name(name).build();
+                            }
+                        }).collect(Collectors.toList())).build();
+                    }
+                    return GroupBuilder.create().name(name).build();
+                }).collect(Collectors.toList()))
+                .build()).group(GroupBuilder.create().name("Group E").build());
+    }
+
+    @Test
+    public void testCreate() {
+        AuthorizationResource authorization = getClient().authorization();
+        GroupPolicyRepresentation representation = new GroupPolicyRepresentation();
+
+        representation.setName("Group Policy");
+        representation.setDescription("description");
+        representation.setDecisionStrategy(DecisionStrategy.CONSENSUS);
+        representation.setLogic(Logic.NEGATIVE);
+        representation.setGroupsClaim("groups");
+        representation.addGroupPath("/Group A/Group B/Group C", true);
+        representation.addGroupPath("Group E");
+
+        assertCreated(authorization, representation);
+    }
+
+    @Test
+    public void testUpdate() {
+        AuthorizationResource authorization = getClient().authorization();
+        GroupPolicyRepresentation representation = new GroupPolicyRepresentation();
+
+        representation.setName("Update Group Policy");
+        representation.setDescription("description");
+        representation.setDecisionStrategy(DecisionStrategy.CONSENSUS);
+        representation.setLogic(Logic.NEGATIVE);
+        representation.setGroupsClaim("groups");
+        representation.addGroupPath("/Group A/Group B/Group C", true);
+        representation.addGroupPath("Group E");
+
+        assertCreated(authorization, representation);
+
+        representation.setName("changed");
+        representation.setDescription("changed");
+        representation.setDecisionStrategy(DecisionStrategy.AFFIRMATIVE);
+        representation.setLogic(Logic.POSITIVE);
+        representation.removeGroup("/Group A/Group B");
+
+        GroupPoliciesResource policies = authorization.policies().group();
+        GroupPolicyResource permission = policies.findById(representation.getId());
+
+        permission.update(representation);
+        assertRepresentation(representation, permission);
+
+        for (GroupPolicyRepresentation.GroupDefinition roleDefinition : representation.getGroups()) {
+            if (roleDefinition.getPath().equals("Group E")) {
+                roleDefinition.setExtendChildren(true);
+            }
+        }
+
+        permission.update(representation);
+        assertRepresentation(representation, permission);
+
+        representation.getGroups().clear();
+        representation.addGroupPath("/Group A/Group B");
+
+        permission.update(representation);
+        assertRepresentation(representation, permission);
+    }
+
+    @Test
+    public void testDelete() {
+        AuthorizationResource authorization = getClient().authorization();
+        GroupPolicyRepresentation representation = new GroupPolicyRepresentation();
+
+        representation.setName("Delete Group Policy");
+        representation.setGroupsClaim("groups");
+        representation.addGroupPath("/Group A/Group B/Group C", true);
+        representation.addGroupPath("Group E");
+
+        GroupPoliciesResource policies = authorization.policies().group();
+        Response response = policies.create(representation);
+        GroupPolicyRepresentation created = response.readEntity(GroupPolicyRepresentation.class);
+
+        policies.findById(created.getId()).remove();
+
+        GroupPolicyResource removed = policies.findById(created.getId());
+
+        try {
+            removed.toRepresentation();
+            fail("Permission not removed");
+        } catch (NotFoundException ignore) {
+
+        }
+    }
+
+    @Test
+    public void testGenericConfig() {
+        AuthorizationResource authorization = getClient().authorization();
+        GroupPolicyRepresentation representation = new GroupPolicyRepresentation();
+
+        representation.setName("Test Generic Config Permission");
+        representation.setGroupsClaim("groups");
+        representation.addGroupPath("/Group A");
+
+        GroupPoliciesResource policies = authorization.policies().group();
+        Response response = policies.create(representation);
+        GroupPolicyRepresentation created = response.readEntity(GroupPolicyRepresentation.class);
+
+        PolicyResource policy = authorization.policies().policy(created.getId());
+        PolicyRepresentation genericConfig = policy.toRepresentation();
+
+        assertNotNull(genericConfig.getConfig());
+        assertNotNull(genericConfig.getConfig().get("groups"));
+
+        GroupRepresentation group = getRealm().groups().groups().stream().filter(groupRepresentation -> groupRepresentation.getName().equals("Group A")).findFirst().get();
+
+        assertTrue(genericConfig.getConfig().get("groups").contains(group.getId()));
+    }
+
+    private void assertCreated(AuthorizationResource authorization, GroupPolicyRepresentation representation) {
+        GroupPoliciesResource policies = authorization.policies().group();
+        Response response = policies.create(representation);
+        GroupPolicyRepresentation created = response.readEntity(GroupPolicyRepresentation.class);
+        GroupPolicyResource policy = policies.findById(created.getId());
+        assertRepresentation(representation, policy);
+    }
+
+    private void assertRepresentation(GroupPolicyRepresentation representation, GroupPolicyResource permission) {
+        GroupPolicyRepresentation actual = permission.toRepresentation();
+        assertRepresentation(representation, actual, () -> permission.resources(), () -> Collections.emptyList(), () -> permission.associatedPolicies());
+        assertEquals(representation.getGroups().size(), actual.getGroups().size());
+        assertEquals(0, actual.getGroups().stream().filter(actualDefinition -> !representation.getGroups().stream()
+                .filter(groupDefinition -> getGroupPath(actualDefinition.getId()).equals(getCanonicalGroupPath(groupDefinition.getPath())) && actualDefinition.isExtendChildren() == groupDefinition.isExtendChildren())
+                .findFirst().isPresent())
+                .count());
+    }
+
+    private String getGroupPath(String id) {
+        return getRealm().groups().group(id).toRepresentation().getPath();
+    }
+
+    private String getCanonicalGroupPath(String path) {
+        if (path.charAt(0) == '/') {
+            return path;
+        }
+        return "/" + path;
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/GroupNamePolicyTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/GroupNamePolicyTest.java
new file mode 100644
index 0000000..cc4b911
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/GroupNamePolicyTest.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.authz;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.admin.client.resource.AuthorizationResource;
+import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.admin.client.resource.ClientsResource;
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.authorization.client.AuthorizationDeniedException;
+import org.keycloak.authorization.client.AuthzClient;
+import org.keycloak.authorization.client.Configuration;
+import org.keycloak.authorization.client.representation.AuthorizationRequest;
+import org.keycloak.authorization.client.representation.AuthorizationResponse;
+import org.keycloak.authorization.client.representation.PermissionRequest;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.protocol.oidc.mappers.GroupMembershipMapper;
+import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
+import org.keycloak.representations.idm.GroupRepresentation;
+import org.keycloak.representations.idm.ProtocolMapperRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.util.AdminClientUtil;
+import org.keycloak.testsuite.util.ClientBuilder;
+import org.keycloak.testsuite.util.GroupBuilder;
+import org.keycloak.testsuite.util.RealmBuilder;
+import org.keycloak.testsuite.util.RoleBuilder;
+import org.keycloak.testsuite.util.RolesBuilder;
+import org.keycloak.testsuite.util.UserBuilder;
+import org.keycloak.util.JsonSerialization;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class GroupNamePolicyTest extends AbstractKeycloakTest {
+
+    @Override
+    public void addTestRealms(List<RealmRepresentation> testRealms) {
+        ProtocolMapperRepresentation groupProtocolMapper = new ProtocolMapperRepresentation();
+
+        groupProtocolMapper.setName("groups");
+        groupProtocolMapper.setProtocolMapper(GroupMembershipMapper.PROVIDER_ID);
+        groupProtocolMapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+        groupProtocolMapper.setConsentRequired(false);
+        Map<String, String> config = new HashMap<>();
+        config.put(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME, "groups");
+        config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, "true");
+        config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN, "true");
+        groupProtocolMapper.setConfig(config);
+
+        testRealms.add(RealmBuilder.create().name("authz-test")
+                .roles(RolesBuilder.create()
+                        .realmRole(RoleBuilder.create().name("uma_authorization").build())
+                )
+                .group(GroupBuilder.create().name("Group A")
+                    .subGroups(Arrays.asList("Group B", "Group D").stream().map(name -> {
+                        if ("Group B".equals(name)) {
+                            return GroupBuilder.create().name(name).subGroups(Arrays.asList("Group C", "Group E").stream().map(new Function<String, GroupRepresentation>() {
+                                @Override
+                                public GroupRepresentation apply(String name) {
+                                    return GroupBuilder.create().name(name).build();
+                                }
+                            }).collect(Collectors.toList())).build();
+                        }
+                        return GroupBuilder.create().name(name).build();
+                    }).collect(Collectors.toList())).build())
+                .group(GroupBuilder.create().name("Group E").build())
+                .user(UserBuilder.create().username("marta").password("password").addRoles("uma_authorization").addGroups("Group A"))
+                .user(UserBuilder.create().username("alice").password("password").addRoles("uma_authorization"))
+                .user(UserBuilder.create().username("kolo").password("password").addRoles("uma_authorization"))
+                .client(ClientBuilder.create().clientId("resource-server-test")
+                    .secret("secret")
+                    .authorizationServicesEnabled(true)
+                    .redirectUris("http://localhost/resource-server-test")
+                    .defaultRoles("uma_protection")
+                    .directAccessGrants()
+                    .protocolMapper(groupProtocolMapper))
+                .build());
+    }
+
+    @Before
+    public void configureAuthorization() throws Exception {
+        createResource("Resource A");
+        createResource("Resource B");
+        createResource("Resource C");
+
+        createGroupPolicy("Only Group A Policy", "/Group A", true);
+        createGroupPolicy("Only Group B Policy", "/Group A/Group B", false);
+        createGroupPolicy("Only Group C Policy", "/Group A/Group B/Group C", false);
+
+        createResourcePermission("Resource A Permission", "Resource A", "Only Group A Policy");
+        createResourcePermission("Resource B Permission", "Resource B", "Only Group B Policy");
+        createResourcePermission("Resource C Permission", "Resource C", "Only Group C Policy");
+
+        RealmResource realm = getRealm();
+        GroupRepresentation group = getGroup("/Group A/Group B/Group C");
+        UserRepresentation user = realm.users().search("kolo").get(0);
+
+        realm.users().get(user.getId()).joinGroup(group.getId());
+
+        group = getGroup("/Group A/Group B");
+        user = realm.users().search("alice").get(0);
+
+        realm.users().get(user.getId()).joinGroup(group.getId());
+    }
+
+    @Test
+    public void testExactNameMatch() {
+        AuthzClient authzClient = getAuthzClient();
+        PermissionRequest request = new PermissionRequest();
+
+        request.setResourceSetName("Resource A");
+
+        String ticket = authzClient.protection().permission().forResource(request).getTicket();
+        AuthorizationResponse response = authzClient.authorization("marta", "password").authorize(new AuthorizationRequest(ticket));
+
+        assertNotNull(response.getRpt());
+
+        try {
+            authzClient.authorization("kolo", "password").authorize(new AuthorizationRequest(ticket));
+            fail("Should fail because user is not granted with expected group");
+        } catch (AuthorizationDeniedException ignore) {
+
+        }
+
+        try {
+            authzClient.authorization("alice", "password").authorize(new AuthorizationRequest(ticket));
+            fail("Should fail because user is not granted with expected group");
+        } catch (AuthorizationDeniedException ignore) {
+
+        }
+    }
+
+    @Test
+    public void testOnlyChildrenPolicy() throws Exception {
+        RealmResource realm = getRealm();
+        AuthzClient authzClient = getAuthzClient();
+        PermissionRequest request = new PermissionRequest();
+
+        request.setResourceSetName("Resource B");
+
+        String ticket = authzClient.protection().permission().forResource(request).getTicket();
+
+        try {
+            authzClient.authorization("kolo", "password").authorize(new AuthorizationRequest(ticket));
+            fail("Should fail because user is not granted with expected group");
+        } catch (AuthorizationDeniedException ignore) {
+
+        }
+
+        AuthorizationResponse response = authzClient.authorization("alice", "password").authorize(new AuthorizationRequest(ticket));
+
+        assertNotNull(response.getRpt());
+
+        try {
+            authzClient.authorization("marta", "password").authorize(new AuthorizationRequest(ticket));
+            fail("Should fail because user is not granted with expected role");
+        } catch (AuthorizationDeniedException ignore) {
+
+        }
+
+        request = new PermissionRequest();
+
+        request.setResourceSetName("Resource C");
+
+        ticket = authzClient.protection().permission().forResource(request).getTicket();
+
+        response = authzClient.authorization("kolo", "password").authorize(new AuthorizationRequest(ticket));
+
+        assertNotNull(response.getRpt());
+    }
+
+    private void createGroupPolicy(String name, String groupPath, boolean extendChildren) {
+        GroupPolicyRepresentation policy = new GroupPolicyRepresentation();
+
+        policy.setName(name);
+        policy.setGroupsClaim("groups");
+        policy.addGroupPath(groupPath, extendChildren);
+
+        getClient().authorization().policies().group().create(policy);
+    }
+
+    private void createResourcePermission(String name, String resource, String... policies) {
+        ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation();
+
+        permission.setName(name);
+        permission.addResource(resource);
+        permission.addPolicy(policies);
+
+        getClient().authorization().permissions().resource().create(permission);
+    }
+
+    private void createResource(String name) {
+        AuthorizationResource authorization = getClient().authorization();
+        ResourceRepresentation resource = new ResourceRepresentation(name);
+
+        authorization.resources().create(resource);
+    }
+
+    private RealmResource getRealm() {
+        try {
+            return AdminClientUtil.createAdminClient().realm("authz-test");
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to create admin client");
+        }
+    }
+
+    private ClientResource getClient(RealmResource realm) {
+        ClientsResource clients = realm.clients();
+        return clients.findByClientId("resource-server-test").stream().map(representation -> clients.get(representation.getId())).findFirst().orElseThrow(() -> new RuntimeException("Expected client [resource-server-test]"));
+    }
+
+    private AuthzClient getAuthzClient() {
+        try {
+            return AuthzClient.create(JsonSerialization.readValue(getClass().getResourceAsStream("/authorization-test/default-keycloak.json"), Configuration.class));
+        } catch (IOException cause) {
+            throw new RuntimeException("Failed to create authz client", cause);
+        }
+    }
+
+    private ClientResource getClient() {
+        return getClient(getRealm());
+    }
+
+    private GroupRepresentation getGroup(String path) {
+        String[] parts = path.split("/");
+        RealmResource realm = getRealm();
+        GroupRepresentation parent = null;
+
+        for (String part : parts) {
+            if ("".equals(part)) {
+                continue;
+            }
+            if (parent == null) {
+                parent = realm.groups().groups().stream().filter(new Predicate<GroupRepresentation>() {
+                    @Override
+                    public boolean test(GroupRepresentation groupRepresentation) {
+                        return part.equals(groupRepresentation.getName());
+                    }
+                }).findFirst().get();
+                continue;
+            }
+
+            GroupRepresentation group = getGroup(part, parent.getSubGroups());
+
+            if (path.endsWith(group.getName())) {
+                return group;
+            }
+
+            parent = group;
+        }
+
+        return null;
+    }
+
+    private GroupRepresentation getGroup(String name, List<GroupRepresentation> groups) {
+        for (GroupRepresentation group : groups) {
+            if (name.equals(group.getName())) {
+                return group;
+            }
+
+            GroupRepresentation child = getGroup(name, group.getSubGroups());
+
+            if (child != null && name.equals(child.getName())) {
+                return child;
+            }
+        }
+
+        return null;
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/GroupPathPolicyTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/GroupPathPolicyTest.java
new file mode 100644
index 0000000..19f74b4
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/GroupPathPolicyTest.java
@@ -0,0 +1,284 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.authz;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.admin.client.resource.AuthorizationResource;
+import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.admin.client.resource.ClientsResource;
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.authorization.client.AuthorizationDeniedException;
+import org.keycloak.authorization.client.AuthzClient;
+import org.keycloak.authorization.client.Configuration;
+import org.keycloak.authorization.client.representation.AuthorizationRequest;
+import org.keycloak.authorization.client.representation.AuthorizationResponse;
+import org.keycloak.authorization.client.representation.PermissionRequest;
+import org.keycloak.protocol.oidc.OIDCLoginProtocol;
+import org.keycloak.protocol.oidc.mappers.GroupMembershipMapper;
+import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
+import org.keycloak.representations.idm.GroupRepresentation;
+import org.keycloak.representations.idm.ProtocolMapperRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.RoleRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation;
+import org.keycloak.representations.idm.authorization.ResourceRepresentation;
+import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
+import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.util.AdminClientUtil;
+import org.keycloak.testsuite.util.ClientBuilder;
+import org.keycloak.testsuite.util.GroupBuilder;
+import org.keycloak.testsuite.util.RealmBuilder;
+import org.keycloak.testsuite.util.RoleBuilder;
+import org.keycloak.testsuite.util.RolesBuilder;
+import org.keycloak.testsuite.util.UserBuilder;
+import org.keycloak.util.JsonSerialization;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class GroupPathPolicyTest extends AbstractKeycloakTest {
+
+    @Override
+    public void addTestRealms(List<RealmRepresentation> testRealms) {
+        ProtocolMapperRepresentation groupProtocolMapper = new ProtocolMapperRepresentation();
+
+        groupProtocolMapper.setName("groups");
+        groupProtocolMapper.setProtocolMapper(GroupMembershipMapper.PROVIDER_ID);
+        groupProtocolMapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
+        groupProtocolMapper.setConsentRequired(false);
+        Map<String, String> config = new HashMap<>();
+        config.put(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME, "groups");
+        config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, "true");
+        config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN, "true");
+        config.put("full.path", "true");
+        groupProtocolMapper.setConfig(config);
+
+        testRealms.add(RealmBuilder.create().name("authz-test")
+                .roles(RolesBuilder.create()
+                        .realmRole(RoleBuilder.create().name("uma_authorization").build())
+                )
+                .group(GroupBuilder.create().name("Group A")
+                    .subGroups(Arrays.asList("Group B", "Group D").stream().map(name -> {
+                        if ("Group B".equals(name)) {
+                            return GroupBuilder.create().name(name).subGroups(Arrays.asList("Group C", "Group E").stream().map(new Function<String, GroupRepresentation>() {
+                                @Override
+                                public GroupRepresentation apply(String name) {
+                                    return GroupBuilder.create().name(name).build();
+                                }
+                            }).collect(Collectors.toList())).build();
+                        }
+                        return GroupBuilder.create().name(name).build();
+                    }).collect(Collectors.toList())).build())
+                .group(GroupBuilder.create().name("Group E").build())
+                .user(UserBuilder.create().username("marta").password("password").addRoles("uma_authorization").addGroups("Group A"))
+                .user(UserBuilder.create().username("alice").password("password").addRoles("uma_authorization"))
+                .user(UserBuilder.create().username("kolo").password("password").addRoles("uma_authorization"))
+                .client(ClientBuilder.create().clientId("resource-server-test")
+                    .secret("secret")
+                    .authorizationServicesEnabled(true)
+                    .redirectUris("http://localhost/resource-server-test")
+                    .defaultRoles("uma_protection")
+                    .directAccessGrants()
+                    .protocolMapper(groupProtocolMapper))
+                .build());
+    }
+
+    @Before
+    public void configureAuthorization() throws Exception {
+        createResource("Resource A");
+        createResource("Resource B");
+
+        createGroupPolicy("Parent And Children Policy", "/Group A", true);
+        createGroupPolicy("Only Children Policy", "/Group A/Group B/Group C", false);
+
+        createResourcePermission("Resource A Permission", "Resource A", "Parent And Children Policy");
+        createResourcePermission("Resource B Permission", "Resource B", "Only Children Policy");
+    }
+
+    @Test
+    public void testAllowParentAndChildren() {
+        AuthzClient authzClient = getAuthzClient();
+        PermissionRequest request = new PermissionRequest();
+
+        request.setResourceSetName("Resource A");
+
+        String ticket = authzClient.protection().permission().forResource(request).getTicket();
+        AuthorizationResponse response = authzClient.authorization("marta", "password").authorize(new AuthorizationRequest(ticket));
+
+        assertNotNull(response.getRpt());
+
+        RealmResource realm = getRealm();
+        GroupRepresentation group = getGroup("/Group A/Group B/Group C");
+        UserRepresentation user = realm.users().search("kolo").get(0);
+
+        realm.users().get(user.getId()).joinGroup(group.getId());
+
+        ticket = authzClient.protection().permission().forResource(request).getTicket();
+        response = authzClient.authorization("kolo", "password").authorize(new AuthorizationRequest(ticket));
+
+        assertNotNull(response.getRpt());
+    }
+
+    @Test
+    public void testOnlyChildrenPolicy() throws Exception {
+        RealmResource realm = getRealm();
+        AuthzClient authzClient = getAuthzClient();
+        PermissionRequest request = new PermissionRequest();
+
+        request.setResourceSetName("Resource B");
+
+        String ticket = authzClient.protection().permission().forResource(request).getTicket();
+
+        try {
+            authzClient.authorization("kolo", "password").authorize(new AuthorizationRequest(ticket));
+            fail("Should fail because user is not granted with expected role");
+        } catch (AuthorizationDeniedException ignore) {
+
+        }
+
+        GroupRepresentation group = getGroup("/Group A/Group B/Group C");
+        UserRepresentation user = realm.users().search("kolo").get(0);
+
+        realm.users().get(user.getId()).joinGroup(group.getId());
+
+        AuthorizationResponse response = authzClient.authorization("kolo", "password").authorize(new AuthorizationRequest(ticket));
+
+        assertNotNull(response.getRpt());
+
+        try {
+            authzClient.authorization("marta", "password").authorize(new AuthorizationRequest(ticket));
+            fail("Should fail because user is not granted with expected role");
+        } catch (AuthorizationDeniedException ignore) {
+
+        }
+    }
+
+    private void createGroupPolicy(String name, String groupPath, boolean extendChildren) {
+        GroupPolicyRepresentation policy = new GroupPolicyRepresentation();
+
+        policy.setName(name);
+        policy.setGroupsClaim("groups");
+        policy.addGroupPath(groupPath, extendChildren);
+
+        getClient().authorization().policies().group().create(policy);
+    }
+
+    private void createResourcePermission(String name, String resource, String... policies) {
+        ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation();
+
+        permission.setName(name);
+        permission.addResource(resource);
+        permission.addPolicy(policies);
+
+        getClient().authorization().permissions().resource().create(permission);
+    }
+
+    private void createResource(String name) {
+        AuthorizationResource authorization = getClient().authorization();
+        ResourceRepresentation resource = new ResourceRepresentation(name);
+
+        authorization.resources().create(resource);
+    }
+
+    private RealmResource getRealm() {
+        try {
+            return AdminClientUtil.createAdminClient().realm("authz-test");
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to create admin client");
+        }
+    }
+
+    private ClientResource getClient(RealmResource realm) {
+        ClientsResource clients = realm.clients();
+        return clients.findByClientId("resource-server-test").stream().map(representation -> clients.get(representation.getId())).findFirst().orElseThrow(() -> new RuntimeException("Expected client [resource-server-test]"));
+    }
+
+    private AuthzClient getAuthzClient() {
+        try {
+            return AuthzClient.create(JsonSerialization.readValue(getClass().getResourceAsStream("/authorization-test/default-keycloak.json"), Configuration.class));
+        } catch (IOException cause) {
+            throw new RuntimeException("Failed to create authz client", cause);
+        }
+    }
+
+    private ClientResource getClient() {
+        return getClient(getRealm());
+    }
+
+    private GroupRepresentation getGroup(String path) {
+        String[] parts = path.split("/");
+        RealmResource realm = getRealm();
+        GroupRepresentation parent = null;
+
+        for (String part : parts) {
+            if ("".equals(part)) {
+                continue;
+            }
+            if (parent == null) {
+                parent = realm.groups().groups().stream().filter(new Predicate<GroupRepresentation>() {
+                    @Override
+                    public boolean test(GroupRepresentation groupRepresentation) {
+                        return part.equals(groupRepresentation.getName());
+                    }
+                }).findFirst().get();
+                continue;
+            }
+
+            GroupRepresentation group = getGroup(part, parent.getSubGroups());
+
+            if (path.endsWith(group.getName())) {
+                return group;
+            }
+
+            parent = group;
+        }
+
+        return null;
+    }
+
+    private GroupRepresentation getGroup(String name, List<GroupRepresentation> groups) {
+        for (GroupRepresentation group : groups) {
+            if (name.equals(group.getName())) {
+                return group;
+            }
+
+            GroupRepresentation child = getGroup(name, group.getSubGroups());
+
+            if (child != null && name.equals(child.getName())) {
+                return child;
+            }
+        }
+
+        return null;
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientBuilder.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientBuilder.java
index b4f3130..842e406 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientBuilder.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientBuilder.java
@@ -18,7 +18,9 @@
 package org.keycloak.testsuite.util;
 
 import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.ProtocolMapperRepresentation;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.LinkedList;
@@ -175,7 +177,15 @@ public class ClientBuilder {
     }
 
     public ClientBuilder authorizationServicesEnabled(boolean enable) {
-        rep.setAuthorizationServicesEnabled(true);
+        rep.setAuthorizationServicesEnabled(enable);
+        return this;
+    }
+
+    public ClientBuilder protocolMapper(ProtocolMapperRepresentation... mappers) {
+        if (rep.getProtocolMappers() == null) {
+            rep.setProtocolMappers(new ArrayList<>());
+        }
+        rep.getProtocolMappers().addAll(Arrays.asList(mappers));
         return this;
     }
 }
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/GroupPolicy.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/GroupPolicy.java
new file mode 100644
index 0000000..2fd68f4
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/GroupPolicy.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.console.page.clients.authorization.policy;
+
+import org.jboss.arquillian.graphene.page.Page;
+import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class GroupPolicy implements PolicyTypeUI {
+
+    @Page
+    private GroupPolicyForm form;
+
+    public GroupPolicyForm form() {
+        return form;
+    }
+
+    public GroupPolicyRepresentation toRepresentation() {
+        return form.toRepresentation();
+    }
+
+    public void update(GroupPolicyRepresentation expected) {
+        form().populate(expected);
+    }
+}
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/GroupPolicyForm.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/GroupPolicyForm.java
new file mode 100644
index 0000000..389a214
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/GroupPolicyForm.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.console.page.clients.authorization.policy;
+
+import static org.openqa.selenium.By.tagName;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+import org.jboss.arquillian.drone.api.annotation.Drone;
+import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.Logic;
+import org.keycloak.testsuite.page.Form;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.FindBy;
+import org.openqa.selenium.support.ui.Select;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class GroupPolicyForm extends Form {
+
+    @FindBy(id = "name")
+    private WebElement name;
+
+    @FindBy(id = "description")
+    private WebElement description;
+
+    @FindBy(id = "groupsClaim")
+    private WebElement groupsClaim;
+
+    @FindBy(id = "logic")
+    private Select logic;
+
+    @FindBy(xpath = "//i[contains(@class,'pficon-delete')]")
+    private WebElement deleteButton;
+
+    @FindBy(xpath = ACTIVE_DIV_XPATH + "/button[text()='Delete']")
+    private WebElement confirmDelete;
+
+    @FindBy(id = "selectGroup")
+    private WebElement selectGroupButton;
+
+    @Drone
+    private WebDriver driver;
+
+    public void populate(GroupPolicyRepresentation expected) {
+        setInputValue(name, expected.getName());
+        setInputValue(description, expected.getDescription());
+        setInputValue(groupsClaim, expected.getGroupsClaim());
+        logic.selectByValue(expected.getLogic().name());
+
+
+        for (GroupPolicyRepresentation.GroupDefinition definition : toRepresentation().getGroups()) {
+            boolean isExpected = false;
+
+            for (GroupPolicyRepresentation.GroupDefinition expectedDef : expected.getGroups()) {
+                if (definition.getPath().equals(expectedDef.getPath())) {
+                    isExpected = true;
+                    break;
+                }
+            }
+
+            if (!isExpected) {
+                unselect(definition.getPath());
+            }
+        }
+
+        for (GroupPolicyRepresentation.GroupDefinition definition : expected.getGroups()) {
+            String path = definition.getPath();
+            String groupName = path.substring(path.lastIndexOf('/') + 1);
+            WebElement element = driver.findElement(By.xpath("//span[text()='" + groupName + "']"));
+            element.click();
+            selectGroupButton.click();
+            driver.findElements(By.xpath("(//table[@id='selected-groups'])/tbody/tr")).stream()
+                    .filter(webElement -> webElement.findElements(tagName("td")).size() > 1)
+                    .map(webElement -> webElement.findElements(tagName("td")))
+                    .filter(tds -> tds.get(0).getText().equals(definition.getPath()))
+                    .forEach(tds -> {
+                        if (!tds.get(1).findElement(By.tagName("input")).isSelected()) {
+                            if (definition.isExtendChildren()) {
+                                tds.get(1).findElement(By.tagName("input")).click();
+                            }
+                        } else {
+                            if (!definition.isExtendChildren() && tds.get(1).findElement(By.tagName("input")).isSelected()) {
+                                tds.get(1).findElement(By.tagName("input")).click();
+                            }
+                        }
+                    });
+        }
+
+        save();
+    }
+
+    private void unselect(String path) {
+        for (WebElement webElement : driver.findElements(By.xpath("(//table[@id='selected-groups'])/tbody/tr"))) {
+            List<WebElement> tds = webElement.findElements(tagName("td"));
+
+            if (tds.size() > 1) {
+                if (tds.get(0).getText().equals(path)) {
+                    tds.get(2).findElement(By.tagName("button")).click();
+                    return;
+                }
+            }
+        }
+    }
+
+    public void delete() {
+        deleteButton.click();
+        confirmDelete.click();
+    }
+
+    public GroupPolicyRepresentation toRepresentation() {
+        GroupPolicyRepresentation representation = new GroupPolicyRepresentation();
+
+        representation.setName(getInputValue(name));
+        representation.setDescription(getInputValue(description));
+        representation.setGroupsClaim(getInputValue(groupsClaim));
+        representation.setLogic(Logic.valueOf(logic.getFirstSelectedOption().getText().toUpperCase()));
+        representation.setGroups(new HashSet<>());
+
+        driver.findElements(By.xpath("(//table[@id='selected-groups'])/tbody/tr")).stream()
+                .filter(webElement -> webElement.findElements(tagName("td")).size() > 1)
+                .forEach(webElement -> {
+                    List<WebElement> tds = webElement.findElements(tagName("td"));
+                    representation.addGroupPath(tds.get(0).getText(), tds.get(1).findElement(By.tagName("input")).isSelected());
+                });
+
+        return representation;
+    }
+}
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/Policies.java b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/Policies.java
index af2a540..7be563e 100644
--- a/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/Policies.java
+++ b/testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/Policies.java
@@ -22,6 +22,7 @@ import org.jboss.arquillian.graphene.page.Page;
 import org.keycloak.representations.idm.authorization.AbstractPolicyRepresentation;
 import org.keycloak.representations.idm.authorization.AggregatePolicyRepresentation;
 import org.keycloak.representations.idm.authorization.ClientPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
 import org.keycloak.representations.idm.authorization.JSPolicyRepresentation;
 import org.keycloak.representations.idm.authorization.PolicyRepresentation;
 import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
@@ -66,6 +67,9 @@ public class Policies extends Form {
     @Page
     private ClientPolicy clientPolicy;
 
+    @Page
+    private GroupPolicy groupPolicy;
+
     public PoliciesTable policies() {
         return table;
     }
@@ -103,6 +107,10 @@ public class Policies extends Form {
             clientPolicy.form().populate((ClientPolicyRepresentation) expected);
             clientPolicy.form().save();
             return (P) clientPolicy;
+        } else if ("group".equals(type)) {
+            groupPolicy.form().populate((GroupPolicyRepresentation) expected);
+            groupPolicy.form().save();
+            return (P) groupPolicy;
         }
 
         return null;
@@ -130,6 +138,8 @@ public class Policies extends Form {
                     rulePolicy.form().populate((RulePolicyRepresentation) representation);
                 } else if ("client".equals(type)) {
                     clientPolicy.form().populate((ClientPolicyRepresentation) representation);
+                } else if ("group".equals(type)) {
+                    groupPolicy.form().populate((GroupPolicyRepresentation) representation);
                 }
 
                 return;
@@ -158,6 +168,8 @@ public class Policies extends Form {
                     return (P) rulePolicy;
                 } else if ("client".equals(type)) {
                     return (P) clientPolicy;
+                } else if ("group".equals(type)) {
+                    return (P) groupPolicy;
                 }
             }
         }
@@ -187,6 +199,8 @@ public class Policies extends Form {
                     rulePolicy.form().delete();
                 } else if ("client".equals(type)) {
                     clientPolicy.form().delete();
+                } else if ("group".equals(type)) {
+                    groupPolicy.form().delete();
                 }
 
                 return;
diff --git a/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/GroupPolicyManagementTest.java b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/GroupPolicyManagementTest.java
new file mode 100644
index 0000000..e8b05bf
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/other/console/src/test/java/org/keycloak/testsuite/console/authorization/GroupPolicyManagementTest.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.console.authorization;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import java.util.Arrays;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.admin.client.resource.RolesResource;
+import org.keycloak.representations.idm.GroupRepresentation;
+import org.keycloak.representations.idm.RoleRepresentation;
+import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
+import org.keycloak.representations.idm.authorization.Logic;
+import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
+import org.keycloak.representations.idm.authorization.UserPolicyRepresentation;
+import org.keycloak.testsuite.admin.ApiUtil;
+import org.keycloak.testsuite.console.page.clients.authorization.policy.GroupPolicy;
+import org.keycloak.testsuite.console.page.clients.authorization.policy.RolePolicy;
+import org.keycloak.testsuite.console.page.clients.authorization.policy.UserPolicy;
+import org.keycloak.testsuite.util.GroupBuilder;
+
+/**
+ * @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
+ */
+public class GroupPolicyManagementTest extends AbstractAuthorizationSettingsTest {
+
+    @Before
+    public void configureTest() {
+        super.configureTest();
+        RealmResource realmResource = testRealmResource();
+        String groupAId = ApiUtil.getCreatedId(realmResource.groups().add(GroupBuilder.create().name("Group A").build()));
+        String groupBId = ApiUtil.getCreatedId(realmResource.groups().group(groupAId).subGroup(GroupBuilder.create().name("Group B").build()));
+        realmResource.groups().group(groupBId).subGroup(GroupBuilder.create().name("Group D").build());
+        realmResource.groups().group(groupBId).subGroup(GroupBuilder.create().name("Group E").build());
+        realmResource.groups().group(groupAId).subGroup(GroupBuilder.create().name("Group C").build());
+        realmResource.groups().add(GroupBuilder.create().name("Group F").build());
+    }
+
+    @Test
+    public void testUpdate() throws InterruptedException {
+        authorizationPage.navigateTo();
+        GroupPolicyRepresentation expected = new GroupPolicyRepresentation();
+
+        expected.setName("Test Group Policy");
+        expected.setDescription("description");
+        expected.setGroupsClaim("groups");
+        expected.addGroupPath("/Group A", true);
+        expected.addGroupPath("/Group A/Group B/Group D");
+        expected.addGroupPath("Group F");
+
+        expected = createPolicy(expected);
+
+        String previousName = expected.getName();
+
+        expected.setName("Changed Test Group Policy");
+        expected.setDescription("Changed description");
+        expected.setLogic(Logic.NEGATIVE);
+
+        authorizationPage.navigateTo();
+        authorizationPage.authorizationTabs().policies().update(previousName, expected);
+        assertAlertSuccess();
+
+        authorizationPage.navigateTo();
+        GroupPolicy actual = authorizationPage.authorizationTabs().policies().name(expected.getName());
+
+        assertPolicy(expected, actual);
+
+        expected.getGroups().clear();
+        expected.addGroupPath("/Group A", false);
+        expected.addGroupPath("/Group A/Group B/Group D");
+
+        authorizationPage.navigateTo();
+        authorizationPage.authorizationTabs().policies().update(expected.getName(), expected);
+        assertAlertSuccess();
+
+        authorizationPage.navigateTo();
+        actual = authorizationPage.authorizationTabs().policies().name(expected.getName());
+
+        assertPolicy(expected, actual);
+
+        expected.getGroups().clear();
+        expected.addGroupPath("/Group E");
+        expected.addGroupPath("/Group A/Group B", true);
+        expected.addGroupPath("/Group A/Group C");
+
+
+        authorizationPage.navigateTo();
+        authorizationPage.authorizationTabs().policies().update(expected.getName(), expected);
+        assertAlertSuccess();
+
+        authorizationPage.navigateTo();
+        actual = authorizationPage.authorizationTabs().policies().name(expected.getName());
+
+        assertPolicy(expected, actual);
+    }
+
+    @Test
+    public void testDelete() throws InterruptedException {
+        authorizationPage.navigateTo();
+        GroupPolicyRepresentation expected = new GroupPolicyRepresentation();
+
+        expected.setName("Test Delete Group Policy");
+        expected.setDescription("description");
+        expected.setGroupsClaim("groups");
+        expected.addGroupPath("/Group A", true);
+        expected.addGroupPath("/Group A/Group B/Group D");
+        expected.addGroupPath("Group F");
+
+        expected = createPolicy(expected);
+        authorizationPage.navigateTo();
+        authorizationPage.authorizationTabs().policies().delete(expected.getName());
+        assertAlertSuccess();
+        authorizationPage.navigateTo();
+        assertNull(authorizationPage.authorizationTabs().policies().policies().findByName(expected.getName()));
+    }
+
+    private GroupPolicyRepresentation createPolicy(GroupPolicyRepresentation expected) {
+        GroupPolicy policy = authorizationPage.authorizationTabs().policies().create(expected);
+        assertAlertSuccess();
+        return assertPolicy(expected, policy);
+    }
+
+    private GroupPolicyRepresentation assertPolicy(GroupPolicyRepresentation expected, GroupPolicy policy) {
+        GroupPolicyRepresentation actual = policy.toRepresentation();
+
+        assertEquals(expected.getName(), actual.getName());
+        assertEquals(expected.getDescription(), actual.getDescription());
+        assertEquals(expected.getLogic(), actual.getLogic());
+
+        assertNotNull(actual.getGroups());
+        assertEquals(expected.getGroups().size(), actual.getGroups().size());
+        assertEquals(0, actual.getGroups().stream().filter(actualDefinition -> !expected.getGroups().stream()
+                .filter(groupDefinition -> actualDefinition.getPath().contains(groupDefinition.getPath()) && actualDefinition.isExtendChildren() == groupDefinition.isExtendChildren())
+                .findFirst().isPresent())
+                .count());
+        return actual;
+    }
+}
diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
index af76c9c..1a0de6e 100644
--- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -1209,6 +1209,13 @@ authz-policy-js-code.tooltip=The JavaScript code providing the conditions for th
 authz-aggregated=Aggregated
 authz-add-aggregated-policy=Add Aggregated Policy
 
+# Authz Group Policy Detail
+authz-add-group-policy=Add Group Policy
+authz-no-groups-assigned=No groups assigned.
+authz-policy-group-claim=Groups Claim
+authz-policy-group-claim.tooltip=A claim to use as the source for user’s group. If the claim is present it must be an array of strings.
+authz-policy-group-groups.tooltip=Specifies the groups allowed by this policy.
+
 # Authz Permission List
 authz-no-permissions-available=No permissions available.
 
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-app.js b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-app.js
index 7c5e7fa..2b92bc5 100644
--- a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-app.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-app.js
@@ -324,7 +324,29 @@ module.config(['$routeProvider', function ($routeProvider) {
             }
         },
         controller: 'ResourceServerPolicyRoleDetailCtrl'
-    }).when('/realms/:realm/clients/:client/authz/resource-server/policy/js/create', {
+    }).when('/realms/:realm/clients/:client/authz/resource-server/policy/group/create', {
+          templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-group-detail.html',
+          resolve: {
+              realm: function (RealmLoader) {
+                  return RealmLoader();
+              },
+              client : function(ClientLoader) {
+                  return ClientLoader();
+              }
+          },
+          controller: 'ResourceServerPolicyGroupDetailCtrl'
+      }).when('/realms/:realm/clients/:client/authz/resource-server/policy/group/:id', {
+          templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-group-detail.html',
+          resolve: {
+              realm: function (RealmLoader) {
+                  return RealmLoader();
+              },
+              client : function(ClientLoader) {
+                  return ClientLoader();
+              }
+          },
+          controller: 'ResourceServerPolicyGroupDetailCtrl'
+      }).when('/realms/:realm/clients/:client/authz/resource-server/policy/js/create', {
         templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-js-detail.html',
         resolve: {
             realm: function (RealmLoader) {
diff --git a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js
index 2cce138..258a980 100644
--- a/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js
+++ b/themes/src/main/resources/theme/base/admin/resources/js/authz/authz-controller.js
@@ -1728,6 +1728,119 @@ module.controller('ResourceServerPolicyRoleDetailCtrl', function($scope, $route,
     }
 });
 
+module.controller('ResourceServerPolicyGroupDetailCtrl', function($scope, $route, realm, client, Client, Groups, Group, PolicyController) {
+    PolicyController.onInit({
+        getPolicyType : function() {
+            return "group";
+        },
+
+        onInit : function() {
+            $scope.tree = [];
+
+            Groups.query({realm: $route.current.params.realm}, function(groups) {
+                $scope.groups = groups;
+                $scope.groupList = [
+                    {"id" : "realm", "name": "Groups",
+                                "subGroups" : groups}
+                ];
+            });
+
+            var isLeaf = function(node) {
+                return node.id != "realm" && (!node.subGroups || node.subGroups.length == 0);
+            }
+
+            $scope.getGroupClass = function(node) {
+                if (node.id == "realm") {
+                    return 'pficon pficon-users';
+                }
+                if (isLeaf(node)) {
+                    return 'normal';
+                }
+                if (node.subGroups.length && node.collapsed) return 'collapsed';
+                if (node.subGroups.length && !node.collapsed) return 'expanded';
+                return 'collapsed';
+
+            }
+
+            $scope.getSelectedClass = function(node) {
+                if (node.selected) {
+                    return 'selected';
+                } else if ($scope.cutNode && $scope.cutNode.id == node.id) {
+                    return 'cut';
+                }
+                return undefined;
+            }
+
+            $scope.selectGroup = function(group) {
+                for (i = 0; i < $scope.selectedGroups.length; i++) {
+                    if ($scope.selectedGroups[i].id == group.id) {
+                        return
+                    }
+                }
+                $scope.selectedGroups.push({id: group.id, path: group.path});
+                $scope.changed = true;
+            }
+
+            $scope.extendChildren = function(group) {
+                $scope.changed = true;
+            }
+
+            $scope.removeFromList = function(group) {
+                var index = $scope.selectedGroups.indexOf(group);
+                if (index != -1) {
+                    $scope.selectedGroups.splice(index, 1);
+                    $scope.changed = true;
+                }
+            }
+        },
+
+        onInitCreate : function(policy) {
+            var selectedGroups = [];
+
+            $scope.selectedGroups = angular.copy(selectedGroups);
+
+            $scope.$watch('selectedGroups', function() {
+                if (!angular.equals($scope.selectedGroups, selectedGroups)) {
+                    $scope.changed = true;
+                } else {
+                    $scope.changed = false;
+                }
+            }, true);
+        },
+
+        onInitUpdate : function(policy) {
+            $scope.selectedGroups = policy.groups;
+
+            angular.forEach($scope.selectedGroups, function(group, index){
+               Group.get({realm: $route.current.params.realm, groupId: group.id}, function (existing) {
+                   group.path = existing.path;
+               });
+            });
+
+            $scope.$watch('selectedGroups', function() {
+                if (!$scope.changed) {
+                    return;
+                }
+                if (!angular.equals($scope.selectedGroups, selectedGroups)) {
+                    $scope.changed = true;
+                } else {
+                    $scope.changed = false;
+                }
+            }, true);
+        },
+
+        onUpdate : function() {
+            $scope.policy.groups = $scope.selectedGroups;
+            delete $scope.policy.config;
+        },
+
+        onCreate : function() {
+            $scope.policy.groups = $scope.selectedGroups;
+            delete $scope.policy.config;
+        }
+    }, realm, client, $scope);
+});
+
 module.controller('ResourceServerPolicyJSDetailCtrl', function($scope, $route, $location, realm, PolicyController, client) {
     PolicyController.onInit({
         getPolicyType : function() {
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-group-detail.html b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-group-detail.html
new file mode 100644
index 0000000..61af0f1
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/authz/policy/provider/resource-server-policy-group-detail.html
@@ -0,0 +1,124 @@
+<!--
+  ~ * Copyright 2017 Red Hat, Inc. and/or its affiliates
+  ~ * and other contributors as indicated by the @author tags.
+  ~ *
+  ~ * Licensed under the Apache License, Version 2.0 (the "License");
+  ~ * you may not use this file except in compliance with the License.
+  ~ * You may obtain a copy of the License at
+  ~ *
+  ~ * http://www.apache.org/licenses/LICENSE-2.0
+  ~ *
+  ~ * Unless required by applicable law or agreed to in writing, software
+  ~ * distributed under the License is distributed on an "AS IS" BASIS,
+  ~ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ * See the License for the specific language governing permissions and
+  ~ * limitations under the License.
+  -->
+
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+
+    <ol class="breadcrumb">
+        <li><a href="#/realms/{{realm.realm}}/clients">{{:: 'clients' | translate}}</a></li>
+        <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}">{{client.clientId}}</a></li>
+        <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server">{{:: 'authz-authorization' | translate}}</a></li>
+        <li><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/policy">{{:: 'authz-policies' | translate}}</a></li>
+        <li data-ng-show="create">{{:: 'authz-add-group-policy' | translate}}</li>
+        <li data-ng-hide="create">{{:: 'groups' | translate}}</li>
+        <li data-ng-hide="create">{{originalPolicy.name}}</li>
+    </ol>
+
+    <h1 data-ng-show="create">{{:: 'authz-add-group-policy' | translate}}</h1>
+    <h1 data-ng-hide="create">{{originalPolicy.name|capitalize}}<i class="pficon pficon-delete clickable" data-ng-show="!create"
+                                                         data-ng-click="remove()"></i></h1>
+
+    <form class="form-horizontal" name="groupPolicyForm" novalidate>
+        <fieldset class="border-top">
+            <div class="form-group">
+                <label class="col-md-2 control-label" for="name">{{:: 'name' | translate}} <span class="required">*</span></label>
+                <div class="col-sm-6">
+                    <input class="form-control" type="text" id="name" name="name" data-ng-model="policy.name" autofocus required data-ng-blur="checkNewNameAvailability()">
+                </div>
+                <kc-tooltip>{{:: 'authz-policy-name.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group">
+                <label class="col-md-2 control-label" for="description">{{:: 'description' | translate}} </label>
+                <div class="col-sm-6">
+                    <input class="form-control" type="text" id="description" name="description" data-ng-model="policy.description">
+                </div>
+                <kc-tooltip>{{:: 'authz-policy-description.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group">
+                <label class="col-md-2 control-label" for="groupsClaim">{{:: 'authz-policy-group-claim' | translate}} <span class="required">*</span></label>
+                <div class="col-sm-6">
+                    <input class="form-control" type="text" id="groupsClaim" name="groupsClaim" data-ng-model="policy.groupsClaim" required>
+                </div>
+                <kc-tooltip>{{:: 'authz-policy-group-claim.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group">
+                <label class="col-md-2 control-label" for="selectedGroups">{{:: 'groups' | translate}} <span class="required">*</span></label>
+                <div class="col-md-6">
+                    <div tree-id="tree"
+                        angular-treeview="true"
+                        tree-model="groupList"
+                        node-id="id"
+                        node-label="name"
+                        node-children="subGroups" >
+                    </div>
+                    <button data-ng-click="selectGroup(tree.currentNode)" id="selectGroup" class="btn btn-primary" data-ng-disabled="tree.currentNode == null">Select</button>
+                    <input class="form-control" type="text" id="selectedGroups" name="selectedGroups" data-ng-model="noop" data-ng-required="selectedGroups.length <= 0" autofocus required data-ng-show="false">
+                </div>
+                <kc-tooltip>{{:: 'authz-policy-user-users.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <div class="form-group" data-ng-if="selectedGroups.length > 0">
+                <label class="col-md-2 control-label"></label>
+                <div class="col-md-5">
+                    <table class="table table-striped table-bordered" id="selected-groups">
+                        <thead>
+                            <tr>
+                                <th>{{:: 'path' | translate}}</th>
+                                <th class="col-sm-3">Extend to Children</th>
+                                <th>{{:: 'actions' | translate}}</th>
+                            </tr>
+                        </thead>
+                        <tbody>
+                            <tr ng-repeat="group in selectedGroups | orderBy:'name' track by $index">
+                                <td>{{group.path}}</td>
+                                <td>
+                                    <input type="checkbox" ng-model="group.extendChildren" id="{{role.id}}" data-ng-click="extendChildren()">
+                                </td>
+                                <td class="kc-action-cell">
+                                    <button class="btn btn-default btn-block btn-sm" ng-click="removeFromList(group);">{{:: 'remove' | translate}}</button>
+                                </td>
+                            </tr>
+                            <tr data-ng-show="!selectedGroups.length">
+                                <td class="text-muted" colspan="3">{{:: 'authz-no-groups-assigned' | translate}}</td>
+                            </tr>
+                        </tbody>
+                    </table>
+                </div>
+            </div>
+            <div class="form-group clearfix">
+                <label class="col-md-2 control-label" for="logic">{{:: 'authz-policy-logic' | translate}}</label>
+
+                <div class="col-sm-1">
+                    <select class="form-control" id="logic"
+                            data-ng-model="policy.logic">
+                        <option value="POSITIVE">{{:: 'authz-policy-logic-positive' | translate}}</option>
+                        <option value="NEGATIVE">{{:: 'authz-policy-logic-negative' | translate}}</option>
+                    </select>
+                </div>
+
+                <kc-tooltip>{{:: 'authz-policy-logic.tooltip' | translate}}</kc-tooltip>
+            </div>
+            <input type="hidden" data-ng-model="policy.type"/>
+        </fieldset>
+        <div class="form-group" data-ng-show="access.manageAuthorization">
+            <div class="col-md-10 col-md-offset-2">
+                <button kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>
+                <button kc-reset data-ng-disabled="!changed">{{:: 'cancel' | translate}}</button>
+            </div>
+        </div>
+    </form>
+</div>
+
+<kc-menu></kc-menu>
\ No newline at end of file