From 8e64bc3e4dc3d5b6ab449ffff42e425bc76f253f Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Sat, 8 Apr 2017 11:56:01 -0300 Subject: [PATCH] Tests for new permission management rest api --- .../authorization/PolicyRepresentation.java | 70 +---- .../resource/AuthorizationResource.java | 4 + .../client/resource/PermissionsResource.java | 28 ++ .../client/resource/PoliciesResource.java | 7 + .../resource/ResourcePermissionResource.java | 69 +++++ .../resource/ResourcePermissionsResource.java | 42 +++ .../resource/ResourceScopesResource.java | 7 + .../client/resource/ResourcesResource.java | 5 + .../models/utils/RepresentationToModel.java | 2 +- .../admin/PermissionService.java | 8 +- .../admin/PermissionTypeService.java | 94 ------- .../authorization/admin/PolicyService.java | 54 +++- ...ce.java => PolicyTypeResourceService.java} | 8 +- .../admin/PolicyTypeService.java | 39 +++ .../authorization/admin/ScopeService.java | 17 +- .../ResourcePermissionManagementTest.java | 263 ++++++++++++++++++ .../authz/ConflictingScopePermissionTest.java | 25 +- 17 files changed, 537 insertions(+), 205 deletions(-) create mode 100644 integration/admin-client/src/main/java/org/keycloak/admin/client/resource/PermissionsResource.java create mode 100644 integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourcePermissionResource.java create mode 100644 integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourcePermissionsResource.java delete mode 100644 services/src/main/java/org/keycloak/authorization/admin/PermissionTypeService.java rename services/src/main/java/org/keycloak/authorization/admin/{PermissionResourceService.java => PolicyTypeResourceService.java} (86%) create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourcePermissionManagementTest.java diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/PolicyRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/authorization/PolicyRepresentation.java index dc049919d4e..e36d7af445e 100644 --- a/core/src/main/java/org/keycloak/representations/idm/authorization/PolicyRepresentation.java +++ b/core/src/main/java/org/keycloak/representations/idm/authorization/PolicyRepresentation.java @@ -18,53 +18,14 @@ package org.keycloak.representations.idm.authorization; import java.util.HashMap; import java.util.Map; -import java.util.Objects; /** * @author Pedro Igor */ -public class PolicyRepresentation { +public class PolicyRepresentation extends AbstractPolicyRepresentation { - private String id; - private String name; - private String description; - private String type; - private Logic logic = Logic.POSITIVE; - private DecisionStrategy decisionStrategy = DecisionStrategy.UNANIMOUS; private Map config = new HashMap(); - public String getId() { - return this.id; - } - - public void setId(String id) { - this.id = id; - } - - public String getType() { - return this.type; - } - - public void setType(String type) { - this.type = type; - } - - public DecisionStrategy getDecisionStrategy() { - return this.decisionStrategy; - } - - public void setDecisionStrategy(DecisionStrategy decisionStrategy) { - this.decisionStrategy = decisionStrategy; - } - - public Logic getLogic() { - return logic; - } - - public void setLogic(Logic logic) { - this.logic = logic; - } - public Map getConfig() { return this.config; } @@ -72,33 +33,4 @@ public class PolicyRepresentation { public void setConfig(Map config) { this.config = config; } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getDescription() { - return this.description; - } - - public void setDescription(String description) { - this.description = description; - } - - @Override - public boolean equals(final Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - final PolicyRepresentation policy = (PolicyRepresentation) o; - return Objects.equals(getId(), policy.getId()); - } - - @Override - public int hashCode() { - return Objects.hash(getId()); - } } \ No newline at end of file diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/AuthorizationResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/AuthorizationResource.java index 07276eca027..de8d958ab7a 100644 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/AuthorizationResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/AuthorizationResource.java @@ -23,6 +23,7 @@ import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; +import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; @@ -58,4 +59,7 @@ public interface AuthorizationResource { @Path("/policy") PoliciesResource policies(); + + @Path("/permission") + PermissionsResource permissions(); } diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/PermissionsResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/PermissionsResource.java new file mode 100644 index 00000000000..6c160807132 --- /dev/null +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/PermissionsResource.java @@ -0,0 +1,28 @@ +/* + * Copyright 2016 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.Path; + +/** + * @author Pedro Igor + */ +public interface PermissionsResource { + + @Path("resource") + ResourcePermissionsResource resource(); +} diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/PoliciesResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/PoliciesResource.java index e5120c621c4..2d31ab11033 100644 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/PoliciesResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/PoliciesResource.java @@ -28,6 +28,7 @@ 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 java.util.List; @@ -45,6 +46,12 @@ public interface PoliciesResource { @Path("{id}") PolicyResource policy(@PathParam("id") String id); + @Path("/search") + @GET + @Produces(MediaType.APPLICATION_JSON) + @NoCache + PolicyRepresentation findByName(@QueryParam("name") String name); + @GET @Produces(MediaType.APPLICATION_JSON) @NoCache diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourcePermissionResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourcePermissionResource.java new file mode 100644 index 00000000000..ec49ec8d6ef --- /dev/null +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourcePermissionResource.java @@ -0,0 +1,69 @@ +/* + * Copyright 2016 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.PolicyRepresentation; +import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation; +import org.keycloak.representations.idm.authorization.ResourceRepresentation; + +/** + * @author Pedro Igor + */ +public interface ResourcePermissionResource { + + @GET + @Produces(MediaType.APPLICATION_JSON) + @NoCache + ResourcePermissionRepresentation toRepresentation(); + + @PUT + @Consumes(MediaType.APPLICATION_JSON) + void update(ResourcePermissionRepresentation representation); + + @DELETE + void remove(); + + @Path("/associatedPolicies") + @GET + @Produces(MediaType.APPLICATION_JSON) + @NoCache + List associatedPolicies(); + + @Path("/dependentPolicies") + @GET + @Produces(MediaType.APPLICATION_JSON) + @NoCache + List dependentPolicies(); + + @Path("/resources") + @GET + @Produces("application/json") + @NoCache + List resources(); + +} diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourcePermissionsResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourcePermissionsResource.java new file mode 100644 index 00000000000..d4dd0ed376c --- /dev/null +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourcePermissionsResource.java @@ -0,0 +1,42 @@ +/* + * Copyright 2016 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.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation; + +/** + * @author Pedro Igor + */ +public interface ResourcePermissionsResource { + + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + Response create(ResourcePermissionRepresentation representation); + + @Path("{id}") + ResourcePermissionResource findById(@PathParam("id") String id); + +} diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourceScopesResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourceScopesResource.java index 3a481140ccf..ddcaf53055c 100644 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourceScopesResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourceScopesResource.java @@ -25,6 +25,7 @@ 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 java.util.List; @@ -46,4 +47,10 @@ public interface ResourceScopesResource { @NoCache @Produces(MediaType.APPLICATION_JSON) List scopes(); + + @Path("/search") + @GET + @Produces(MediaType.APPLICATION_JSON) + @NoCache + ScopeRepresentation findByName(@QueryParam("name") String name); } diff --git a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourcesResource.java b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourcesResource.java index 07438d07f4e..e7daaa18e8b 100644 --- a/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourcesResource.java +++ b/integration/admin-client/src/main/java/org/keycloak/admin/client/resource/ResourcesResource.java @@ -54,6 +54,11 @@ public interface ResourcesResource { @QueryParam("first") Integer firstResult, @QueryParam("max") Integer maxResult); + @GET + @NoCache + @Produces(MediaType.APPLICATION_JSON) + List findByName(@QueryParam("name") String name); + @GET @NoCache @Produces(MediaType.APPLICATION_JSON) diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index 1d27f3d9639..766e2755783 100755 --- a/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/server-spi-private/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -2113,7 +2113,7 @@ public class RepresentationToModel { type = "rules"; } if (authorization.getProvider(type) == null) { - throw new RuntimeException("Unknown polucy type [" + type + "]. Could not find a provider for this type."); + throw new RuntimeException("Unknown policy type [" + type + "]. Could not find a provider for this type."); } } diff --git a/services/src/main/java/org/keycloak/authorization/admin/PermissionService.java b/services/src/main/java/org/keycloak/authorization/admin/PermissionService.java index 32a607f9769..4ada87d316b 100644 --- a/services/src/main/java/org/keycloak/authorization/admin/PermissionService.java +++ b/services/src/main/java/org/keycloak/authorization/admin/PermissionService.java @@ -28,18 +28,14 @@ import org.keycloak.services.resources.admin.RealmAuth; * @author Pedro Igor */ public class PermissionService extends PolicyService { + public PermissionService(ResourceServer resourceServer, AuthorizationProvider authorization, RealmAuth auth) { super(resourceServer, authorization, auth); } - @Override - protected Object doCreatePolicyTypeResource(String type) { - return new PermissionTypeService(type, resourceServer, authorization, auth); - } - @Override protected PolicyResourceService doCreatePolicyResource(Policy policy) { - return new PermissionResourceService(policy, resourceServer, authorization, auth); + return new PolicyTypeResourceService(policy, resourceServer, authorization, auth); } @Override diff --git a/services/src/main/java/org/keycloak/authorization/admin/PermissionTypeService.java b/services/src/main/java/org/keycloak/authorization/admin/PermissionTypeService.java deleted file mode 100644 index d2b598a3356..00000000000 --- a/services/src/main/java/org/keycloak/authorization/admin/PermissionTypeService.java +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright 2016 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.admin; - -import static org.keycloak.models.utils.RepresentationToModel.toModel; - -import java.io.IOException; - -import javax.ws.rs.Path; - -import org.jboss.resteasy.spi.ResteasyProviderFactory; -import org.keycloak.authorization.AuthorizationProvider; -import org.keycloak.authorization.model.Policy; -import org.keycloak.authorization.model.ResourceServer; -import org.keycloak.authorization.policy.provider.PolicyProviderAdminService; -import org.keycloak.representations.idm.authorization.AbstractPolicyRepresentation; -import org.keycloak.services.resources.admin.RealmAuth; -import org.keycloak.util.JsonSerialization; - -/** - * @author Pedro Igor - */ -public class PermissionTypeService extends PolicyService { - - private final String type; - - PermissionTypeService(String type, ResourceServer resourceServer, AuthorizationProvider authorization, RealmAuth auth) { - super(resourceServer, authorization, auth); - this.type = type; - } - - @Path("/provider") - public Object getPolicyAdminResourceProvider() { - PolicyProviderAdminService resource = getPolicyProviderAdminResource(type); - - ResteasyProviderFactory.getInstance().injectProperties(resource); - - return resource; - } - - @Override - protected Object doCreatePolicyResource(Policy policy) { - return new PermissionResourceService(policy, resourceServer,authorization, auth); - } - - @Override - protected Object doCreate(String payload) { - PolicyProviderAdminService provider = getPolicyProviderAdminResource(type); - AbstractPolicyRepresentation representation = toRepresentation(type, payload, provider); - - Policy policy = toModel(representation, this.resourceServer, authorization); - - if (provider != null) { - try { - provider.onCreate(policy, representation); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - return representation; - } - - private AbstractPolicyRepresentation toRepresentation(String type, String payload, PolicyProviderAdminService provider) { - Class representationType = provider.getRepresentationType(); - - if (representationType == null) { - throw new RuntimeException("Policy provider for type [" + type + "] returned a null representation type."); - } - - AbstractPolicyRepresentation representation; - - try { - representation = JsonSerialization.readValue(payload, representationType); - } catch (IOException e) { - throw new RuntimeException("Failed to deserialize JSON using policy provider for type [" + type + "].", e); - } - return representation; - } -} diff --git a/services/src/main/java/org/keycloak/authorization/admin/PolicyService.java b/services/src/main/java/org/keycloak/authorization/admin/PolicyService.java index 66a3d1abf70..6a9a32f0ab0 100644 --- a/services/src/main/java/org/keycloak/authorization/admin/PolicyService.java +++ b/services/src/main/java/org/keycloak/authorization/admin/PolicyService.java @@ -50,8 +50,10 @@ import org.keycloak.authorization.store.PolicyStore; import org.keycloak.authorization.store.StoreFactory; import org.keycloak.models.Constants; import org.keycloak.models.utils.ModelToRepresentation; +import org.keycloak.representations.idm.authorization.AbstractPolicyRepresentation; import org.keycloak.representations.idm.authorization.PolicyProviderRepresentation; import org.keycloak.representations.idm.authorization.PolicyRepresentation; +import org.keycloak.services.ErrorResponse; import org.keycloak.services.resources.admin.RealmAuth; import org.keycloak.util.JsonSerialization; @@ -72,10 +74,10 @@ public class PolicyService { @Path("{type}") public Object getResource(@PathParam("type") String type) { - PolicyProviderFactory providerFactory = authorization.getProviderFactory(type); + PolicyProviderFactory providerFactory = getPolicyProviderFactory(type); if (providerFactory != null) { - return doCreatePolicyTypeResource(type); + return new PolicyTypeService(type, resourceServer, authorization, auth); } Policy policy = authorization.getStoreFactory().getPolicyStore().findById(type, resourceServer.getId()); @@ -83,10 +85,6 @@ public class PolicyService { return doCreatePolicyResource(policy); } - protected Object doCreatePolicyTypeResource(String type) { - return new PolicyTypeService(type, resourceServer, authorization, auth); - } - protected Object doCreatePolicyResource(Policy policy) { return new PolicyResourceService(policy, resourceServer, authorization, auth); } @@ -95,13 +93,39 @@ public class PolicyService { @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) @NoCache - @Deprecated public Response create(String payload) { this.auth.requireManage(); - return Response.status(Status.CREATED).entity(doCreate(payload)).build(); + + AbstractPolicyRepresentation representation = doCreateRepresentation(payload); + + Policy existing = authorization.getStoreFactory().getPolicyStore().findByName(representation.getName(), resourceServer.getId()); + + if (existing != null) { + return ErrorResponse.exists("Policy with name [" + representation.getName() + "] already exists"); + } + + Policy policy = doCreate(representation); + PolicyProviderAdminService provider = getPolicyProviderAdminResource(representation.getType()); + + if (provider != null) { + try { + provider.onCreate(policy, representation); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + + representation.setId(policy.getId()); + + return Response.status(Status.CREATED).entity(representation).build(); } - protected Object doCreate(String payload) { + protected Policy doCreate(AbstractPolicyRepresentation representation) { + return create(PolicyRepresentation.class.cast(representation)); + } + + protected AbstractPolicyRepresentation doCreateRepresentation(String payload) { PolicyRepresentation representation; try { @@ -110,12 +134,10 @@ public class PolicyService { throw new RuntimeException("Failed to deserialize representation", cause); } - create(representation); - return representation; } - public void create(PolicyRepresentation representation) { + public Policy create(PolicyRepresentation representation) { Policy policy = toModel(representation, this.resourceServer, authorization); PolicyProviderAdminService resource = getPolicyProviderAdminResource(policy.getType()); @@ -127,7 +149,7 @@ public class PolicyService { } } - representation.setId(policy.getId()); + return policy; } protected Object toRepresentation(Policy model) { @@ -268,7 +290,7 @@ public class PolicyService { } protected PolicyProviderAdminService getPolicyProviderAdminResource(String policyType) { - PolicyProviderFactory providerFactory = authorization.getProviderFactory(policyType); + PolicyProviderFactory providerFactory = getPolicyProviderFactory(policyType); if (providerFactory != null) { return providerFactory.getAdminResource(resourceServer, authorization); @@ -277,6 +299,10 @@ public class PolicyService { return null; } + private PolicyProviderFactory getPolicyProviderFactory(String policyType) { + return authorization.getProviderFactory(policyType); + } + private void findAssociatedPolicies(Policy policy, List policies) { policy.getAssociatedPolicies().forEach(associated -> { policies.add(associated); diff --git a/services/src/main/java/org/keycloak/authorization/admin/PermissionResourceService.java b/services/src/main/java/org/keycloak/authorization/admin/PolicyTypeResourceService.java similarity index 86% rename from services/src/main/java/org/keycloak/authorization/admin/PermissionResourceService.java rename to services/src/main/java/org/keycloak/authorization/admin/PolicyTypeResourceService.java index 6d4583d1310..dc1040019b1 100644 --- a/services/src/main/java/org/keycloak/authorization/admin/PermissionResourceService.java +++ b/services/src/main/java/org/keycloak/authorization/admin/PolicyTypeResourceService.java @@ -31,9 +31,9 @@ import org.keycloak.util.JsonSerialization; /** * @author Pedro Igor */ -public class PermissionResourceService extends PolicyResourceService { +public class PolicyTypeResourceService extends PolicyResourceService { - public PermissionResourceService(Policy policy, ResourceServer resourceServer, AuthorizationProvider authorization, RealmAuth auth) { + public PolicyTypeResourceService(Policy policy, ResourceServer resourceServer, AuthorizationProvider authorization, RealmAuth auth) { super(policy, resourceServer, authorization, auth); } @@ -82,10 +82,6 @@ public class PermissionResourceService extends PolicyResourceService { representation.setType(policy.getType()); representation.setDecisionStrategy(policy.getDecisionStrategy()); representation.setLogic(policy.getLogic()); - representation.addResource(policy.getResources().stream().map(resource -> resource.getId()).findFirst().orElse(null)); - representation.addPolicies(policy.getAssociatedPolicies().stream().map(associated -> associated.getId()).toArray(value -> new String[value])); - representation.addScopes(policy.getScopes().stream().map(associated -> associated.getId()).toArray(value -> new String[value])); - return representation; } } diff --git a/services/src/main/java/org/keycloak/authorization/admin/PolicyTypeService.java b/services/src/main/java/org/keycloak/authorization/admin/PolicyTypeService.java index 7d3f8b46de1..865e8db9b43 100644 --- a/services/src/main/java/org/keycloak/authorization/admin/PolicyTypeService.java +++ b/services/src/main/java/org/keycloak/authorization/admin/PolicyTypeService.java @@ -16,13 +16,20 @@ */ package org.keycloak.authorization.admin; +import static org.keycloak.models.utils.RepresentationToModel.toModel; + +import java.io.IOException; + import javax.ws.rs.Path; import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.model.Policy; import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.policy.provider.PolicyProviderAdminService; +import org.keycloak.representations.idm.authorization.AbstractPolicyRepresentation; import org.keycloak.services.resources.admin.RealmAuth; +import org.keycloak.util.JsonSerialization; /** * @author Pedro Igor @@ -44,4 +51,36 @@ public class PolicyTypeService extends PolicyService { return resource; } + + @Override + protected Object doCreatePolicyResource(Policy policy) { + return new PolicyTypeResourceService(policy, resourceServer,authorization, auth); + } + + @Override + protected AbstractPolicyRepresentation doCreateRepresentation(String payload) { + PolicyProviderAdminService provider = getPolicyProviderAdminResource(type); + Class representationType = provider.getRepresentationType(); + + if (representationType == null) { + throw new RuntimeException("Policy provider for type [" + type + "] returned a null representation type."); + } + + AbstractPolicyRepresentation representation; + + try { + representation = JsonSerialization.readValue(payload, representationType); + } catch (IOException e) { + throw new RuntimeException("Failed to deserialize JSON using policy provider for type [" + type + "].", e); + } + + representation.setType(type); + + return representation; + } + + @Override + protected Policy doCreate(AbstractPolicyRepresentation representation) { + return toModel(representation, resourceServer, authorization); + } } diff --git a/services/src/main/java/org/keycloak/authorization/admin/ScopeService.java b/services/src/main/java/org/keycloak/authorization/admin/ScopeService.java index 7724830a979..1ec9a139e0e 100644 --- a/services/src/main/java/org/keycloak/authorization/admin/ScopeService.java +++ b/services/src/main/java/org/keycloak/authorization/admin/ScopeService.java @@ -41,6 +41,7 @@ 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 javax.ws.rs.core.Response.Status; @@ -69,8 +70,8 @@ public class ScopeService { } @POST - @Consumes("application/json") - @Produces("application/json") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) public Response create(ScopeRepresentation scope) { this.auth.requireManage(); Scope model = toModel(scope, this.resourceServer, authorization); @@ -82,8 +83,8 @@ public class ScopeService { @Path("{id}") @PUT - @Consumes("application/json") - @Produces("application/json") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) public Response update(@PathParam("id") String id, ScopeRepresentation scope) { this.auth.requireManage(); scope.setId(id); @@ -134,7 +135,7 @@ public class ScopeService { @Path("{id}") @GET - @Produces("application/json") + @Produces(MediaType.APPLICATION_JSON) public Response findById(@PathParam("id") String id) { this.auth.requireView(); Scope model = this.authorization.getStoreFactory().getScopeStore().findById(id, resourceServer.getId()); @@ -148,7 +149,7 @@ public class ScopeService { @Path("{id}/resources") @GET - @Produces("application/json") + @Produces(MediaType.APPLICATION_JSON) public Response getResources(@PathParam("id") String id) { this.auth.requireView(); StoreFactory storeFactory = this.authorization.getStoreFactory(); @@ -170,7 +171,7 @@ public class ScopeService { @Path("{id}/permissions") @GET - @Produces("application/json") + @Produces(MediaType.APPLICATION_JSON) public Response getPermissions(@PathParam("id") String id) { this.auth.requireView(); StoreFactory storeFactory = this.authorization.getStoreFactory(); @@ -195,7 +196,7 @@ public class ScopeService { @Path("/search") @GET - @Produces("application/json") + @Produces(MediaType.APPLICATION_JSON) @NoCache public Response find(@QueryParam("name") String name) { this.auth.requireView(); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourcePermissionManagementTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourcePermissionManagementTest.java new file mode 100644 index 00000000000..b1572da723c --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/authorization/ResourcePermissionManagementTest.java @@ -0,0 +1,263 @@ +/* + * Copyright 2016 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.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.IOException; +import java.util.ArrayList; +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 java.util.function.IntFunction; +import java.util.stream.Collectors; + +import javax.ws.rs.NotFoundException; +import javax.ws.rs.core.Response; + +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.admin.client.resource.ResourcePermissionResource; +import org.keycloak.admin.client.resource.ResourcePermissionsResource; +import org.keycloak.authorization.client.AuthzClient; +import org.keycloak.authorization.client.Configuration; +import org.keycloak.authorization.client.representation.ResourceRepresentation; +import org.keycloak.authorization.client.representation.ScopeRepresentation; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.authorization.DecisionStrategy; +import org.keycloak.representations.idm.authorization.Logic; +import org.keycloak.representations.idm.authorization.PolicyRepresentation; +import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation; +import org.keycloak.testsuite.AbstractKeycloakTest; +import org.keycloak.testsuite.util.AdminClientUtil; +import org.keycloak.testsuite.util.ClientBuilder; +import org.keycloak.testsuite.util.RealmBuilder; +import org.keycloak.testsuite.util.UserBuilder; +import org.keycloak.util.JsonSerialization; + +/** + * @author Pedro Igor + */ +public class ResourcePermissionManagementTest extends AbstractKeycloakTest { + + @Override + public void addTestRealms(List testRealms) { + testRealms.add(RealmBuilder.create().name("authz-test") + .user(UserBuilder.create().username("marta").password("password")) + .user(UserBuilder.create().username("kolo").password("password")) + .client(ClientBuilder.create().clientId("resource-server-test") + .secret("secret") + .authorizationServicesEnabled(true) + .redirectUris("http://localhost/resource-server-test") + .defaultRoles("uma_protection") + .directAccessGrants()) + .build()); + } + + @Before + public void configureAuthorization() throws Exception { + createResourcesAndScopes(); + RealmResource realm = getRealm(); + createPolicies(realm, getClient(realm)); + } + + @Test + public void testCreateResourcePermission() { + AuthorizationResource authorization = getClient(getRealm()).authorization(); + ResourcePermissionRepresentation representation = new ResourcePermissionRepresentation(); + + representation.setName("Resource A Permission"); + representation.setDescription("description"); + representation.setDecisionStrategy(DecisionStrategy.CONSENSUS); + representation.setLogic(Logic.NEGATIVE); + representation.addResource(getResourceId("Resource A", authorization)); + representation.addPolicies(getPolicyIds(Arrays.asList("Only Marta Policy", "Only Kolo Policy"), authorization).stream().toArray((IntFunction) value -> new String[value])); + + assertCreated(authorization, representation); + } + + @Test + public void testCreateResourceType() { + AuthorizationResource authorization = getClient(getRealm()).authorization(); + ResourcePermissionRepresentation representation = new ResourcePermissionRepresentation(); + + representation.setName("Resource A Type Permission"); + representation.setDescription("description"); + representation.setDecisionStrategy(DecisionStrategy.CONSENSUS); + representation.setLogic(Logic.NEGATIVE); + representation.setResourceType("test-resource"); + representation.addPolicies(getPolicyIds(Arrays.asList("Only Marta Policy"), authorization).stream().toArray((IntFunction) value -> new String[value])); + + assertCreated(authorization, representation); + } + + @Test + public void testDelete() { + AuthorizationResource authorization = getClient(getRealm()).authorization(); + ResourcePermissionRepresentation representation = new ResourcePermissionRepresentation(); + + representation.setName("Test Delete Permission"); + representation.setResourceType("test-resource"); + representation.addPolicies(getPolicyIds(Arrays.asList("Only Marta Policy"), authorization).stream().toArray((IntFunction) value -> new String[value])); + + ResourcePermissionsResource permissions = authorization.permissions().resource(); + Response response = permissions.create(representation); + ResourcePermissionRepresentation created = response.readEntity(ResourcePermissionRepresentation.class); + + permissions.findById(created.getId()).remove(); + + ResourcePermissionResource removed = permissions.findById(created.getId()); + + try { + removed.toRepresentation(); + fail("Permission not removed"); + } catch (NotFoundException ignore) { + + } + } + + @Test + public void failCreateWithSameName() { + AuthorizationResource authorization = getClient(getRealm()).authorization(); + ResourcePermissionRepresentation permission1 = new ResourcePermissionRepresentation(); + + permission1.setName("Conflicting Name Permission"); + permission1.setResourceType("test-resource"); + permission1.addPolicies(getPolicyIds(Arrays.asList("Only Marta Policy"), authorization).stream().toArray((IntFunction) value -> new String[value])); + + ResourcePermissionsResource permissions = authorization.permissions().resource(); + + permissions.create(permission1); + + ResourcePermissionRepresentation permission2 = new ResourcePermissionRepresentation(); + + permission2.setName(permission1.getName()); + + Response response = permissions.create(permission2); + + assertEquals(Response.Status.CONFLICT.getStatusCode(), response.getStatus()); + } + + private void assertCreated(AuthorizationResource authorization, ResourcePermissionRepresentation representation) { + ResourcePermissionsResource permissions = authorization.permissions().resource(); + Response response = permissions.create(representation); + ResourcePermissionRepresentation created = response.readEntity(ResourcePermissionRepresentation.class); + + assertNotNull(created); + assertNotNull(created.getId()); + + ResourcePermissionResource permission = permissions.findById(created.getId()); + ResourcePermissionRepresentation found = permission.toRepresentation(); + + assertNotNull(found); + assertEquals(created.getId(), found.getId()); + assertEquals(created.getName(), found.getName()); + assertEquals(created.getDescription(), found.getDescription()); + assertEquals(created.getDecisionStrategy(), found.getDecisionStrategy()); + assertEquals(created.getLogic(), found.getLogic()); + assertEquals(created.getResourceType(), found.getResourceType()); + assertNull(found.getResources()); + assertNull(found.getPolicies()); + + assertEquals(representation.getPolicies().size(), permission.associatedPolicies().stream().map(representation1 -> representation1.getId()).filter(policyId -> representation.getPolicies().contains(policyId)).count()); + + if (representation.getResources() != null) { + assertEquals(representation.getResources().size(), permission.resources().stream().map(representation1 -> representation1.getId()).filter(resourceId -> representation.getResources().contains(resourceId)).count()); + } else { + assertTrue(permission.resources().isEmpty()); + } + } + + private void createResourcesAndScopes() throws IOException { + AuthzClient authzClient = getAuthzClient(); + Set scopes = new HashSet<>(); + + scopes.add(new ScopeRepresentation("read")); + scopes.add(new ScopeRepresentation("write")); + scopes.add(new ScopeRepresentation("execute")); + + List resources = new ArrayList<>(); + + resources.add(new ResourceRepresentation("Resource A", scopes)); + resources.add(new ResourceRepresentation("Resource B", scopes)); + resources.add(new ResourceRepresentation("Resource C", scopes)); + + resources.forEach(resource -> authzClient.protection().resource().create(resource)); + } + + private void createPolicies(RealmResource realm, ClientResource client) throws IOException { + createUserPolicy("Only Marta Policy", realm, client, "marta"); + createUserPolicy("Only Kolo Policy", realm, client, "kolo"); + } + + private void createUserPolicy(String name, RealmResource realm, ClientResource client, String username) throws IOException { + String userId = realm.users().search(username).stream().map(representation -> representation.getId()).findFirst().orElseThrow(() -> new RuntimeException("Expected user [userId]")); + + PolicyRepresentation representation = new PolicyRepresentation(); + + representation.setName(name); + representation.setType("user"); + + Map config = new HashMap<>(); + + config.put("users", JsonSerialization.writeValueAsString(new String[] {userId})); + + representation.setConfig(config); + + client.authorization().policies().create(representation); + } + + 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 RealmResource getRealm() { + try { + return AdminClientUtil.createAdminClient().realm("authz-test"); + } catch (Exception cause) { + throw new RuntimeException("Failed to create admin client", cause); + } + } + + private String getResourceId(String resourceName, AuthorizationResource authorization) { + return authorization.resources().findByName(resourceName).stream().map(representation -> representation.getId()).findFirst().orElseThrow(() -> new RuntimeException("Expected user [userId]")); + } + + private List getPolicyIds(List policies, AuthorizationResource authorization) { + return policies.stream().map(policyName -> authorization.policies().findByName(policyName).getId()).collect(Collectors.toList()); + } + + 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); + } + } +} diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/ConflictingScopePermissionTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/ConflictingScopePermissionTest.java index da835d558b3..1b0b50bb1db 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/ConflictingScopePermissionTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/ConflictingScopePermissionTest.java @@ -33,6 +33,7 @@ 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; @@ -192,8 +193,9 @@ public class ConflictingScopePermissionTest extends AbstractKeycloakTest { } private void createResourcePermission(String name, String resourceName, List policies, ClientResource client) throws IOException { - String resourceId = client.authorization().resources().find(resourceName, null, null, null, null, -1, -1).stream().map(representation -> representation.getId()).findFirst().orElseThrow(() -> new RuntimeException("Expected user [userId]")); - List policyIds = client.authorization().policies().policies().stream().filter(representation -> policies.contains(representation.getName())).map(representation -> representation.getId()).collect(Collectors.toList()); + AuthorizationResource authorization = client.authorization(); + String resourceId = getResourceId(resourceName, authorization); + List policyIds = getPolicyIds(policies, authorization); PolicyRepresentation representation = new PolicyRepresentation(); @@ -207,18 +209,23 @@ public class ConflictingScopePermissionTest extends AbstractKeycloakTest { representation.setConfig(config); - client.authorization().policies().create(representation); + authorization.policies().create(representation); + } + + private String getResourceId(String resourceName, AuthorizationResource authorization) { + return authorization.resources().findByName(resourceName).stream().map(representation -> representation.getId()).findFirst().orElseThrow(() -> new RuntimeException("Expected user [userId]")); } private void createScopePermission(String name, String resourceName, List scopes, List policies, ClientResource client) throws IOException { + AuthorizationResource authorization = client.authorization(); String resourceId = null; if (resourceName != null) { - resourceId = client.authorization().resources().find(resourceName, null, null, null, null, -1, -1).stream().map(representation -> representation.getId()).findFirst().orElseThrow(() -> new RuntimeException("Expected user [userId]")); + resourceId = getResourceId(resourceName, authorization); } - List scopeIds = client.authorization().scopes().scopes().stream().filter(representation -> policies.contains(representation.getName())).map(representation -> representation.getId()).collect(Collectors.toList()); - List policyIds = client.authorization().policies().policies().stream().filter(representation -> policies.contains(representation.getName())).map(representation -> representation.getId()).collect(Collectors.toList()); + List scopeIds = scopes.stream().map(scopeName -> authorization.scopes().findByName(scopeName).getId()).collect(Collectors.toList()); + List policyIds = getPolicyIds(policies, authorization); PolicyRepresentation representation = new PolicyRepresentation(); @@ -236,7 +243,11 @@ public class ConflictingScopePermissionTest extends AbstractKeycloakTest { representation.setConfig(config); - client.authorization().policies().create(representation); + authorization.policies().create(representation); + } + + private List getPolicyIds(List policies, AuthorizationResource authorization) { + return policies.stream().map(policyName -> authorization.policies().findByName(policyName).getId()).collect(Collectors.toList()); } private AuthzClient getAuthzClient() {