From 3716fa44ac4e56b03d28a81ec691e068357291e5 Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Fri, 27 Oct 2017 12:40:30 -0200 Subject: [PATCH] [KEYCLOAK-5728] - Permission Claims support --- .../idm/authorization/Permission.java | 13 +- .../permission/ResourcePermission.java | 44 ++++ .../authorization/util/Permissions.java | 4 +- .../testsuite/authz/PermissionClaimTest.java | 196 ++++++++++++++++++ 4 files changed, 253 insertions(+), 4 deletions(-) create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/PermissionClaimTest.java diff --git a/core/src/main/java/org/keycloak/representations/idm/authorization/Permission.java b/core/src/main/java/org/keycloak/representations/idm/authorization/Permission.java index 7e865cda50..74df64fde5 100644 --- a/core/src/main/java/org/keycloak/representations/idm/authorization/Permission.java +++ b/core/src/main/java/org/keycloak/representations/idm/authorization/Permission.java @@ -20,6 +20,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.HashSet; +import java.util.Map; import java.util.Set; /** @@ -36,14 +37,18 @@ public class Permission { @JsonInclude(JsonInclude.Include.NON_EMPTY) private Set scopes; + @JsonInclude(JsonInclude.Include.NON_EMPTY) + private final Map> claims; + public Permission() { - this(null, null, null); + this(null, null, null, null); } - public Permission(final String resourceSetId, String resourceSetName, final Set scopes) { + public Permission(final String resourceSetId, String resourceSetName, final Set scopes, Map> claims) { this.resourceSetId = resourceSetId; this.resourceSetName = resourceSetName; this.scopes = scopes; + this.claims = claims; } public String getResourceSetId() { @@ -62,6 +67,10 @@ public class Permission { return this.scopes; } + public Map> getClaims() { + return claims; + } + @Override public String toString() { StringBuilder builder = new StringBuilder(); diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/permission/ResourcePermission.java b/server-spi-private/src/main/java/org/keycloak/authorization/permission/ResourcePermission.java index e2821e11e7..efd3c27ba8 100644 --- a/server-spi-private/src/main/java/org/keycloak/authorization/permission/ResourcePermission.java +++ b/server-spi-private/src/main/java/org/keycloak/authorization/permission/ResourcePermission.java @@ -23,7 +23,11 @@ import org.keycloak.authorization.model.ResourceServer; import org.keycloak.authorization.model.Scope; import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; /** * Represents a permission for a given resource. @@ -35,6 +39,7 @@ public class ResourcePermission { private final Resource resource; private final List scopes; private ResourceServer resourceServer; + private Map> claims; public ResourcePermission(Resource resource, List scopes, ResourceServer resourceServer) { this.resource = resource; @@ -68,4 +73,43 @@ public class ResourcePermission { public ResourceServer getResourceServer() { return this.resourceServer; } + + /** + * Returns all permission claims. + * + * @return + */ + public Map> getClaims() { + if (claims == null) { + return Collections.emptyMap(); + } + return Collections.unmodifiableMap(claims); + } + + /** + *

Adds a permission claim with the given name and a single value. + * + *

If a claim already exists, the value is added to list of values of the existing claim

+ * + * @param name the name of the claim + * @param value the value of the claim + */ + public boolean addClaim(String name, String value) { + if (claims == null) { + claims = new HashMap<>(); + } + return claims.computeIfAbsent(name, key -> new HashSet<>()).add(value); + } + + /** + *

Removes a permission claim. + * + * + * @param name the name of the claim + */ + public void removeClaim(String name) { + if (claims != null) { + claims.remove(name); + } + } } diff --git a/services/src/main/java/org/keycloak/authorization/util/Permissions.java b/services/src/main/java/org/keycloak/authorization/util/Permissions.java index a420cf9bb5..1064b444a7 100644 --- a/services/src/main/java/org/keycloak/authorization/util/Permissions.java +++ b/services/src/main/java/org/keycloak/authorization/util/Permissions.java @@ -241,7 +241,7 @@ public final class Permissions { Permission evalPermission = permissions.get(allowedResource.getId()); if (evalPermission == null) { - evalPermission = new Permission(resourceId, resourceName, scopes); + evalPermission = new Permission(resourceId, resourceName, scopes, permission.getClaims()); permissions.put(resourceId, evalPermission); } @@ -261,7 +261,7 @@ public final class Permissions { } } } else { - Permission scopePermission = new Permission(null, null, scopes); + Permission scopePermission = new Permission(null, null, scopes, permission.getClaims()); permissions.put(scopePermission.toString(), scopePermission); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/PermissionClaimTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/PermissionClaimTest.java new file mode 100644 index 0000000000..cde7fc2d63 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/PermissionClaimTest.java @@ -0,0 +1,196 @@ +/* + * 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.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; + +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.authorization.client.util.HttpResponseException; +import org.keycloak.representations.AccessToken; +import org.keycloak.representations.AccessToken.Authorization; +import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.RoleRepresentation; +import org.keycloak.representations.idm.authorization.JSPolicyRepresentation; +import org.keycloak.representations.idm.authorization.Permission; +import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation; +import org.keycloak.representations.idm.authorization.ResourceRepresentation; +import org.keycloak.testsuite.util.ClientBuilder; +import org.keycloak.testsuite.util.OAuthClient; +import org.keycloak.testsuite.util.RealmBuilder; +import org.keycloak.testsuite.util.RoleBuilder; +import org.keycloak.testsuite.util.RolesBuilder; +import org.keycloak.testsuite.util.UserBuilder; +import org.keycloak.util.JsonSerialization; + +/** + * @author Pedro Igor + */ +public class PermissionClaimTest extends AbstractAuthzTest { + + private JSPolicyRepresentation claimAPolicy; + private JSPolicyRepresentation claimBPolicy; + + @Override + public void addTestRealms(List testRealms) { + testRealms.add(RealmBuilder.create().name("authz-test") + .roles(RolesBuilder.create().realmRole(RoleBuilder.create().name("uma_authorization").build())) + .user(UserBuilder.create().username("marta").password("password").addRoles("uma_authorization")) + .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()) + .client(ClientBuilder.create().clientId("test-client") + .secret("secret") + .authorizationServicesEnabled(true) + .redirectUris("http://localhost/test-client") + .directAccessGrants()) + .build()); + } + + @Before + public void configureAuthorization() throws Exception { + ClientResource client = getClient(getRealm()); + AuthorizationResource authorization = client.authorization(); + + claimAPolicy = new JSPolicyRepresentation(); + + claimAPolicy.setName("Claim A Policy"); + claimAPolicy.setCode("$evaluation.getPermission().addClaim('claim-a', 'claim-a');$evaluation.getPermission().addClaim('claim-a', 'claim-a1');$evaluation.grant();"); + + authorization.policies().js().create(claimAPolicy).close(); + + claimBPolicy = new JSPolicyRepresentation(); + + claimBPolicy.setName("Policy Claim B"); + claimBPolicy.setCode("$evaluation.getPermission().addClaim('claim-b', 'claim-b');$evaluation.grant();"); + + authorization.policies().js().create(claimBPolicy).close(); + } + + @Test + public void testPermissionWithClaims() throws Exception { + ClientResource client = getClient(getRealm()); + AuthorizationResource authorization = client.authorization(); + ResourceRepresentation resource = new ResourceRepresentation("Resource A"); + + authorization.resources().create(resource).close(); + + ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation(); + + permission.setName(resource.getName() + " Permission"); + permission.addResource(resource.getName()); + permission.addPolicy(claimAPolicy.getName()); + + authorization.permissions().resource().create(permission).close(); + + PermissionRequest request = new PermissionRequest(); + + request.setResourceSetName(resource.getName()); + + String accessToken = new OAuthClient().realm("authz-test").clientId("test-client").doGrantAccessTokenRequest("secret", "marta", "password").getAccessToken(); + AuthzClient authzClient = getAuthzClient(); + String ticket = authzClient.protection().permission().forResource(request).getTicket(); + AuthorizationResponse response = authzClient.authorization(accessToken).authorize(new AuthorizationRequest(ticket)); + + assertNotNull(response.getRpt()); + AccessToken rpt = toAccessToken(response.getRpt()); + Authorization authorizationClaim = rpt.getAuthorization(); + List permissions = authorizationClaim.getPermissions(); + + assertEquals(1, permissions.size()); + + assertTrue(permissions.get(0).getClaims().get("claim-a").containsAll(Arrays.asList("claim-a", "claim-a1"))); + } + + @Test + public void testPermissionWithClaimsDifferentPolicies() throws Exception { + ClientResource client = getClient(getRealm()); + AuthorizationResource authorization = client.authorization(); + + ResourceRepresentation resource = new ResourceRepresentation("Resource B"); + + authorization.resources().create(resource).close(); + + ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation(); + + permission.setName(resource.getName() + " Permission"); + permission.addResource(resource.getName()); + permission.addPolicy(claimAPolicy.getName(), claimBPolicy.getName()); + + authorization.permissions().resource().create(permission).close(); + + PermissionRequest request = new PermissionRequest(); + + request.setResourceSetName(resource.getName()); + + String accessToken = new OAuthClient().realm("authz-test").clientId("test-client").doGrantAccessTokenRequest("secret", "marta", "password").getAccessToken(); + AuthzClient authzClient = getAuthzClient(); + String ticket = authzClient.protection().permission().forResource(request).getTicket(); + AuthorizationResponse response = authzClient.authorization(accessToken).authorize(new AuthorizationRequest(ticket)); + + assertNotNull(response.getRpt()); + AccessToken rpt = toAccessToken(response.getRpt()); + Authorization authorizationClaim = rpt.getAuthorization(); + List permissions = authorizationClaim.getPermissions(); + + assertEquals(1, permissions.size()); + + Map> claims = permissions.get(0).getClaims(); + + assertTrue(claims.containsKey("claim-a")); + assertTrue(claims.containsKey("claim-b")); + } + + private RealmResource getRealm() throws Exception { + return adminClient.realm("authz-test"); + } + + 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); + } + } +}