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 0000000000..2ae1bdc4b3 --- /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 Pedro Igor + */ +public class GroupPolicyProvider implements PolicyProvider { + + private final Function representationFunction; + + public GroupPolicyProvider(Function 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 0000000000..f55844978d --- /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 Pedro Igor + */ +public class GroupPolicyProviderFactory implements PolicyProviderFactory { + + 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 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 config = new HashMap<>(); + GroupPolicyRepresentation groupPolicy = toRepresentation(policy, new GroupPolicyRepresentation()); + Set 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 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 config = new HashMap<>(policy.getConfig()); + + config.put("groupsClaim", groupsClaim); + + List 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 getGroupsDefinition(Map 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 e4588f87a6..e6fa1cc9e4 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 0000000000..c063f8f87b --- /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 Pedro Igor + */ +public class GroupPolicyRepresentation extends AbstractPolicyRepresentation { + + private String groupsClaim; + private Set groups; + + @Override + public String getType() { + return "group"; + } + + public String getGroupsClaim() { + return groupsClaim; + } + + public void setGroupsClaim(String groupsClaim) { + this.groupsClaim = groupsClaim; + } + + public Set getGroups() { + return groups; + } + + public void setGroups(Set 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 0000000000..1cc51b0040 --- /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 Pedro Igor + */ +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 0000000000..6171868b65 --- /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 Pedro Igor + */ +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 associatedPolicies(); + + @Path("/dependentPolicies") + @GET + @Produces(MediaType.APPLICATION_JSON) + @NoCache + List dependentPolicies(); + + @Path("/resources") + @GET + @Produces("application/json") + @NoCache + List 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 a0af5d4c62..9ced12c290 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 c83d9f8146..0719bab259 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 0000000000..57d86a79ee --- /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 Pedro Igor + */ +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() { + @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 0000000000..cc4b9118f9 --- /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 Pedro Igor + */ +public class GroupNamePolicyTest extends AbstractKeycloakTest { + + @Override + public void addTestRealms(List testRealms) { + ProtocolMapperRepresentation groupProtocolMapper = new ProtocolMapperRepresentation(); + + groupProtocolMapper.setName("groups"); + groupProtocolMapper.setProtocolMapper(GroupMembershipMapper.PROVIDER_ID); + groupProtocolMapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + groupProtocolMapper.setConsentRequired(false); + Map 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() { + @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() { + @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 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 0000000000..19f74b42fc --- /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 Pedro Igor + */ +public class GroupPathPolicyTest extends AbstractKeycloakTest { + + @Override + public void addTestRealms(List testRealms) { + ProtocolMapperRepresentation groupProtocolMapper = new ProtocolMapperRepresentation(); + + groupProtocolMapper.setName("groups"); + groupProtocolMapper.setProtocolMapper(GroupMembershipMapper.PROVIDER_ID); + groupProtocolMapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + groupProtocolMapper.setConsentRequired(false); + Map 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() { + @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() { + @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 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 b29abc143f..677430d23a 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 policies = authzResource.policies().policies(); - Assert.assertEquals(13, policies.size()); + Assert.assertEquals(14, policies.size()); List> 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 b4f313008c..842e4062a7 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 5f84e38b51..fb1a7e0002 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", @@ -298,6 +342,14 @@ "clients": "[\"broker\",\"admin-cli\"]" } }, + { + "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", 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 0000000000..2fd68f4fcf --- /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 Pedro Igor + */ +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 0000000000..389a214d57 --- /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 Pedro Igor + */ +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 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 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 af2a5402bd..7be563e114 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 0000000000..e8b05bf45b --- /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 Pedro Igor + */ +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 af76c9c9c9..1a0de6e9bb 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 7c5e7fa176..2b92bc5b11 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 6ccdb943a4..89d0a481bc 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 0000000000..61af0f157d --- /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 @@ + + + + + + {{:: 'clients' | translate}} + {{client.clientId}} + {{:: 'authz-authorization' | translate}} + {{:: 'authz-policies' | translate}} + {{:: 'authz-add-group-policy' | translate}} + {{:: 'groups' | translate}} + {{originalPolicy.name}} + + + {{:: 'authz-add-group-policy' | translate}} + {{originalPolicy.name|capitalize}} + + + + + {{:: 'name' | translate}} * + + + + {{:: 'authz-policy-name.tooltip' | translate}} + + + {{:: 'description' | translate}} + + + + {{:: 'authz-policy-description.tooltip' | translate}} + + + {{:: 'authz-policy-group-claim' | translate}} * + + + + {{:: 'authz-policy-group-claim.tooltip' | translate}} + + + {{:: 'groups' | translate}} * + + + + Select + + + {{:: 'authz-policy-user-users.tooltip' | translate}} + + + + + + + + {{:: 'path' | translate}} + Extend to Children + {{:: 'actions' | translate}} + + + + + {{group.path}} + + + + + {{:: 'remove' | translate}} + + + + {{:: 'authz-no-groups-assigned' | translate}} + + + + + + + {{:: 'authz-policy-logic' | translate}} + + + + {{:: 'authz-policy-logic-positive' | translate}} + {{:: 'authz-policy-logic-negative' | translate}} + + + + {{:: 'authz-policy-logic.tooltip' | translate}} + + + + + + {{:: 'save' | translate}} + {{:: 'cancel' | translate}} + + + + + + \ No newline at end of file