keycloak-aplcache
Changes
authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/group/GroupPolicyProvider.java 80(+80 -0)
authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/group/GroupPolicyProviderFactory.java 214(+214 -0)
authz/policy/common/src/main/resources/META-INF/services/org.keycloak.authorization.policy.provider.PolicyProviderFactory 3(+2 -1)
core/src/main/java/org/keycloak/representations/idm/authorization/GroupPolicyRepresentation.java 141(+141 -0)
integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupPoliciesResource.java 51(+51 -0)
integration/admin-client/src/main/java/org/keycloak/admin/client/resource/GroupPolicyResource.java 70(+70 -0)
integration/admin-client/src/main/java/org/keycloak/admin/client/resource/PoliciesResource.java 3(+3 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/GroupPolicyManagementTest.java 210(+210 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/GroupNamePolicyTest.java 302(+302 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/GroupPathPolicyTest.java 284(+284 -0)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java 3(+2 -1)
testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/util/ClientBuilder.java 12(+11 -1)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/GroupPolicy.java 42(+42 -0)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/GroupPolicyForm.java 153(+153 -0)
testsuite/integration-arquillian/tests/other/console/src/main/java/org/keycloak/testsuite/console/page/clients/authorization/policy/Policies.java 14(+14 -0)
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..f558449
--- /dev/null
+++ b/authz/policy/common/src/main/java/org/keycloak/authorization/policy/provider/group/GroupPolicyProviderFactory.java
@@ -0,0 +1,214 @@
+/*
+ * 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.ClientModel;
+import org.keycloak.models.GroupModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.utils.ModelToRepresentation;
+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(getGroupsDefinition(policy.getConfig()));
+ } 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"), getGroupsDefinition(representation.getConfig()), authorization);
+ } catch (IOException cause) {
+ throw new RuntimeException("Failed to deserialize groups", cause);
+ }
+ }
+
+ @Override
+ public void onExport(Policy policy, PolicyRepresentation representation, AuthorizationProvider authorizationProvider) {
+ Map<String, String> config = new HashMap<>();
+ GroupPolicyRepresentation groupPolicy = toRepresentation(policy, new GroupPolicyRepresentation());
+ Set<GroupPolicyRepresentation.GroupDefinition> groups = groupPolicy.getGroups();
+
+ for (GroupPolicyRepresentation.GroupDefinition definition: groups) {
+ GroupModel group = authorizationProvider.getRealm().getGroupById(definition.getId());
+ definition.setId(null);
+ definition.setPath(ModelToRepresentation.buildGroupPath(group));
+ }
+
+ try {
+ config.put("groupsClaim", groupPolicy.getGroupsClaim());
+ config.put("groups", JsonSerialization.writeValueAsString(groups));
+ } catch (IOException cause) {
+ throw new RuntimeException("Failed to export group policy [" + policy.getName() + "]", cause);
+ }
+
+ representation.setConfig(config);
+ }
+
+ @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 (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);
+ }
+
+ private HashSet<GroupPolicyRepresentation.GroupDefinition> getGroupsDefinition(Map<String, String> config) throws IOException {
+ return new HashSet<>(Arrays.asList(JsonSerialization.readValue(config.get("groups"), GroupPolicyRepresentation.GroupDefinition[].class)));
+ }
+}
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/exportimport/ExportImportUtil.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java
index b29abc1..677430d 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java
@@ -627,12 +627,13 @@ public class ExportImportUtil {
assertPredicate(scopes, scopePredicates);
List<PolicyRepresentation> policies = authzResource.policies().policies();
- Assert.assertEquals(13, policies.size());
+ Assert.assertEquals(14, policies.size());
List<Predicate<PolicyRepresentation>> policyPredicates = new ArrayList<>();
policyPredicates.add(policyRepresentation -> "Any Admin Policy".equals(policyRepresentation.getName()));
policyPredicates.add(policyRepresentation -> "Any User Policy".equals(policyRepresentation.getName()));
policyPredicates.add(representation -> "Client and Realm Role Policy".equals(representation.getName()));
policyPredicates.add(representation -> "Client Test Policy".equals(representation.getName()));
+ policyPredicates.add(representation -> "Group Policy Test".equals(representation.getName()));
policyPredicates.add(policyRepresentation -> "Only Premium User Policy".equals(policyRepresentation.getName()));
policyPredicates.add(policyRepresentation -> "wburke policy".equals(policyRepresentation.getName()));
policyPredicates.add(policyRepresentation -> "All Users Policy".equals(policyRepresentation.getName()));
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/base/src/test/resources/model/testrealm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/model/testrealm.json
index 5f84e38..fb1a7e0 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/model/testrealm.json
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/model/testrealm.json
@@ -71,6 +71,50 @@
}
}
],
+ "groups": [
+ {
+ "name": "Group A",
+ "path": "/Group A",
+ "attributes": {
+ "topAttribute": [
+ "true"
+ ]
+ },
+ "subGroups": [
+ {
+ "name": "Group B",
+ "path": "/Group A/Group B",
+ "attributes": {
+ "level2Attribute": [
+ "true"
+ ]
+ },
+ "subGroups": []
+ }
+ ]
+ },
+ {
+ "name": "Group C",
+ "path": "/Group C",
+ "attributes": {
+ "topAttribute": [
+ "true"
+ ]
+ },
+ "subGroups": [
+ {
+ "name": "Group D",
+ "path": "/Group C/Group D",
+ "attributes": {
+ "level2Attribute": [
+ "true"
+ ]
+ },
+ "subGroups": []
+ }
+ ]
+ }
+ ],
"users": [
{
"username": "wburke",
@@ -299,6 +343,14 @@
}
},
{
+ "name": "Group Policy Test",
+ "type": "group",
+ "config": {
+ "groupsClaim": "groups",
+ "groups": "[{\"path\":\"/Group A\",\"extendChildren\":true},{\"path\":\"/Group A/Group B\",\"extendChildren\":false},{\"path\":\"/Group C/Group D\",\"extendChildren\":true}]"
+ }
+ },
+ {
"name": "Only Premium User Policy",
"description": "Defines that only premium users can do something",
"type": "role",
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 6ccdb94..89d0a48 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
@@ -1734,6 +1734,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