Merge pull request #4224 from pedroigor/KEYCLOAK-3168
[KEYCLOAK-3168] - Group-Based Access Control
This commit is contained in:
commit
8c82201add
22 changed files with 2061 additions and 4 deletions
|
@ -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() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)));
|
||||||
|
}
|
||||||
|
}
|
|
@ -42,3 +42,4 @@ org.keycloak.authorization.policy.provider.scope.ScopePolicyProviderFactory
|
||||||
org.keycloak.authorization.policy.provider.time.TimePolicyProviderFactory
|
org.keycloak.authorization.policy.provider.time.TimePolicyProviderFactory
|
||||||
org.keycloak.authorization.policy.provider.user.UserPolicyProviderFactory
|
org.keycloak.authorization.policy.provider.user.UserPolicyProviderFactory
|
||||||
org.keycloak.authorization.policy.provider.client.ClientPolicyProviderFactory
|
org.keycloak.authorization.policy.provider.client.ClientPolicyProviderFactory
|
||||||
|
org.keycloak.authorization.policy.provider.group.GroupPolicyProviderFactory
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
|
@ -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();
|
||||||
|
|
||||||
|
}
|
|
@ -89,4 +89,7 @@ public interface PoliciesResource {
|
||||||
|
|
||||||
@Path("client")
|
@Path("client")
|
||||||
ClientPoliciesResource client();
|
ClientPoliciesResource client();
|
||||||
|
|
||||||
|
@Path("group")
|
||||||
|
GroupPoliciesResource group();
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,6 +107,10 @@ public interface Attributes {
|
||||||
return values.length;
|
return values.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isEmpty() {
|
||||||
|
return values.length == 0;
|
||||||
|
}
|
||||||
|
|
||||||
public String asString(int idx) {
|
public String asString(int idx) {
|
||||||
if (idx >= values.length) {
|
if (idx >= values.length) {
|
||||||
throw new IllegalArgumentException("Invalid index [" + idx + "]. Values are [" + values + "].");
|
throw new IllegalArgumentException("Invalid index [" + idx + "]. Values are [" + values + "].");
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -627,12 +627,13 @@ public class ExportImportUtil {
|
||||||
assertPredicate(scopes, scopePredicates);
|
assertPredicate(scopes, scopePredicates);
|
||||||
|
|
||||||
List<PolicyRepresentation> policies = authzResource.policies().policies();
|
List<PolicyRepresentation> policies = authzResource.policies().policies();
|
||||||
Assert.assertEquals(13, policies.size());
|
Assert.assertEquals(14, policies.size());
|
||||||
List<Predicate<PolicyRepresentation>> policyPredicates = new ArrayList<>();
|
List<Predicate<PolicyRepresentation>> policyPredicates = new ArrayList<>();
|
||||||
policyPredicates.add(policyRepresentation -> "Any Admin Policy".equals(policyRepresentation.getName()));
|
policyPredicates.add(policyRepresentation -> "Any Admin Policy".equals(policyRepresentation.getName()));
|
||||||
policyPredicates.add(policyRepresentation -> "Any User 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 and Realm Role Policy".equals(representation.getName()));
|
||||||
policyPredicates.add(representation -> "Client Test 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 -> "Only Premium User Policy".equals(policyRepresentation.getName()));
|
||||||
policyPredicates.add(policyRepresentation -> "wburke policy".equals(policyRepresentation.getName()));
|
policyPredicates.add(policyRepresentation -> "wburke policy".equals(policyRepresentation.getName()));
|
||||||
policyPredicates.add(policyRepresentation -> "All Users Policy".equals(policyRepresentation.getName()));
|
policyPredicates.add(policyRepresentation -> "All Users Policy".equals(policyRepresentation.getName()));
|
||||||
|
|
|
@ -18,7 +18,9 @@
|
||||||
package org.keycloak.testsuite.util;
|
package org.keycloak.testsuite.util;
|
||||||
|
|
||||||
import org.keycloak.representations.idm.ClientRepresentation;
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
|
@ -175,7 +177,15 @@ public class ClientBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
public ClientBuilder authorizationServicesEnabled(boolean enable) {
|
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;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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": [
|
"users": [
|
||||||
{
|
{
|
||||||
"username": "wburke",
|
"username": "wburke",
|
||||||
|
@ -298,6 +342,14 @@
|
||||||
"clients": "[\"broker\",\"admin-cli\"]"
|
"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",
|
"name": "Only Premium User Policy",
|
||||||
"description": "Defines that only premium users can do something",
|
"description": "Defines that only premium users can do something",
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,6 +22,7 @@ import org.jboss.arquillian.graphene.page.Page;
|
||||||
import org.keycloak.representations.idm.authorization.AbstractPolicyRepresentation;
|
import org.keycloak.representations.idm.authorization.AbstractPolicyRepresentation;
|
||||||
import org.keycloak.representations.idm.authorization.AggregatePolicyRepresentation;
|
import org.keycloak.representations.idm.authorization.AggregatePolicyRepresentation;
|
||||||
import org.keycloak.representations.idm.authorization.ClientPolicyRepresentation;
|
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.JSPolicyRepresentation;
|
||||||
import org.keycloak.representations.idm.authorization.PolicyRepresentation;
|
import org.keycloak.representations.idm.authorization.PolicyRepresentation;
|
||||||
import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
|
import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
|
||||||
|
@ -66,6 +67,9 @@ public class Policies extends Form {
|
||||||
@Page
|
@Page
|
||||||
private ClientPolicy clientPolicy;
|
private ClientPolicy clientPolicy;
|
||||||
|
|
||||||
|
@Page
|
||||||
|
private GroupPolicy groupPolicy;
|
||||||
|
|
||||||
public PoliciesTable policies() {
|
public PoliciesTable policies() {
|
||||||
return table;
|
return table;
|
||||||
}
|
}
|
||||||
|
@ -103,6 +107,10 @@ public class Policies extends Form {
|
||||||
clientPolicy.form().populate((ClientPolicyRepresentation) expected);
|
clientPolicy.form().populate((ClientPolicyRepresentation) expected);
|
||||||
clientPolicy.form().save();
|
clientPolicy.form().save();
|
||||||
return (P) clientPolicy;
|
return (P) clientPolicy;
|
||||||
|
} else if ("group".equals(type)) {
|
||||||
|
groupPolicy.form().populate((GroupPolicyRepresentation) expected);
|
||||||
|
groupPolicy.form().save();
|
||||||
|
return (P) groupPolicy;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@ -130,6 +138,8 @@ public class Policies extends Form {
|
||||||
rulePolicy.form().populate((RulePolicyRepresentation) representation);
|
rulePolicy.form().populate((RulePolicyRepresentation) representation);
|
||||||
} else if ("client".equals(type)) {
|
} else if ("client".equals(type)) {
|
||||||
clientPolicy.form().populate((ClientPolicyRepresentation) representation);
|
clientPolicy.form().populate((ClientPolicyRepresentation) representation);
|
||||||
|
} else if ("group".equals(type)) {
|
||||||
|
groupPolicy.form().populate((GroupPolicyRepresentation) representation);
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
@ -158,6 +168,8 @@ public class Policies extends Form {
|
||||||
return (P) rulePolicy;
|
return (P) rulePolicy;
|
||||||
} else if ("client".equals(type)) {
|
} else if ("client".equals(type)) {
|
||||||
return (P) clientPolicy;
|
return (P) clientPolicy;
|
||||||
|
} else if ("group".equals(type)) {
|
||||||
|
return (P) groupPolicy;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -187,6 +199,8 @@ public class Policies extends Form {
|
||||||
rulePolicy.form().delete();
|
rulePolicy.form().delete();
|
||||||
} else if ("client".equals(type)) {
|
} else if ("client".equals(type)) {
|
||||||
clientPolicy.form().delete();
|
clientPolicy.form().delete();
|
||||||
|
} else if ("group".equals(type)) {
|
||||||
|
groupPolicy.form().delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1209,6 +1209,13 @@ authz-policy-js-code.tooltip=The JavaScript code providing the conditions for th
|
||||||
authz-aggregated=Aggregated
|
authz-aggregated=Aggregated
|
||||||
authz-add-aggregated-policy=Add Aggregated Policy
|
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 Permission List
|
||||||
authz-no-permissions-available=No permissions available.
|
authz-no-permissions-available=No permissions available.
|
||||||
|
|
||||||
|
|
|
@ -324,7 +324,29 @@ module.config(['$routeProvider', function ($routeProvider) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
controller: 'ResourceServerPolicyRoleDetailCtrl'
|
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',
|
templateUrl: resourceUrl + '/partials/authz/policy/provider/resource-server-policy-js-detail.html',
|
||||||
resolve: {
|
resolve: {
|
||||||
realm: function (RealmLoader) {
|
realm: function (RealmLoader) {
|
||||||
|
|
|
@ -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) {
|
module.controller('ResourceServerPolicyJSDetailCtrl', function($scope, $route, $location, realm, PolicyController, client) {
|
||||||
PolicyController.onInit({
|
PolicyController.onInit({
|
||||||
getPolicyType : function() {
|
getPolicyType : function() {
|
||||||
|
|
|
@ -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>
|
Loading…
Reference in a new issue