From dcd1a68d9561098178862a943b4b10cbd4c82f59 Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Wed, 31 May 2017 19:33:34 -0300 Subject: [PATCH] [KEYCLOAK-4992] - Allow clients to exclude resource_set_name from RPT --- .../AuthorizationRequestMetadata.java | 36 ++++ .../representation/EntitlementRequest.java | 66 +++++- .../representation/PermissionRequest.java | 19 ++ .../client/resource/EntitlementResource.java | 27 ++- .../PolicyEvaluationResponseBuilder.java | 2 +- .../AuthorizationTokenService.java | 9 +- .../representation/AuthorizationRequest.java | 9 +- .../AuthorizationRequestMetadata.java | 36 ++++ .../entitlement/EntitlementService.java | 21 +- .../representation/EntitlementRequest.java | 58 +++++- .../authorization/util/Permissions.java | 13 +- .../testsuite/authz/EntitlementAPITest.java | 190 ++++++++++++++++++ 12 files changed, 455 insertions(+), 31 deletions(-) create mode 100644 authz/client/src/main/java/org/keycloak/authorization/client/representation/AuthorizationRequestMetadata.java create mode 100644 services/src/main/java/org/keycloak/authorization/authorization/representation/AuthorizationRequestMetadata.java create mode 100644 testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/EntitlementAPITest.java diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/representation/AuthorizationRequestMetadata.java b/authz/client/src/main/java/org/keycloak/authorization/client/representation/AuthorizationRequestMetadata.java new file mode 100644 index 0000000000..0dfd4161f6 --- /dev/null +++ b/authz/client/src/main/java/org/keycloak/authorization/client/representation/AuthorizationRequestMetadata.java @@ -0,0 +1,36 @@ +/* + * 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.client.representation; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * @author Pedro Igor + */ +public class AuthorizationRequestMetadata { + + @JsonProperty("include_resource_name") + private boolean includeResourceName; + + public boolean isIncludeResourceName() { + return includeResourceName; + } + + public void setIncludeResourceName(boolean includeResourceName) { + this.includeResourceName = includeResourceName; + } +} diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/representation/EntitlementRequest.java b/authz/client/src/main/java/org/keycloak/authorization/client/representation/EntitlementRequest.java index daec23372d..b3efa85c96 100644 --- a/authz/client/src/main/java/org/keycloak/authorization/client/representation/EntitlementRequest.java +++ b/authz/client/src/main/java/org/keycloak/authorization/client/representation/EntitlementRequest.java @@ -4,31 +4,81 @@ import java.util.ArrayList; import java.util.List; /** + *

An {@code {@link EntitlementRequest} represents a request sent to the server containing the permissions being requested. + * + *

Along with an entitlement request additional {@link AuthorizationRequestMetadata} information can be passed in order to define what clients expect from + * the server when evaluating the requested permissions and when returning with a response. + * * @author Pedro Igor */ public class EntitlementRequest { private String rpt; + private AuthorizationRequestMetadata metadata; private List permissions = new ArrayList<>(); + /** + * Returns the permissions being requested. + * + * @return the permissions being requested (not {@code null}) + */ public List getPermissions() { return permissions; } - public String getRpt() { - return rpt; - } - - public void setRpt(String rpt) { - this.rpt = rpt; - } - + /** + * Set the permissions being requested + * + * @param permissions the permissions being requests (not {@code null}) + */ public void setPermissions(List permissions) { this.permissions = permissions; } + /** + * Adds the given {@link PermissionRequest} to the list of requested permissions. + * + * @param request the permission to request (not {@code null}) + */ public void addPermission(PermissionRequest request) { getPermissions().add(request); } + + /** + * Returns a {@code String} representing a previously issued RPT which permissions will be included the response in addition to the new ones being requested. + * + * @return a previously issued RPT (may be {@code null}) + */ + public String getRpt() { + return rpt; + } + + /** + * A {@code String} representing a previously issued RPT which permissions will be included the response in addition to the new ones being requested. + * + * @param rpt a previously issued RPT. If {@code null}, only the requested permissions are evaluated + */ + public void setRpt(String rpt) { + this.rpt = rpt; + } + + /** + * Return the {@link Metadata} associated with this request. + * + * @return + */ + public AuthorizationRequestMetadata getMetadata() { + return metadata; + } + + /** + * The {@link Metadata} associated with this request. The metadata defines specific information that should be considered + * by the server when evaluating and returning permissions. + * + * @param metadata the {@link Metadata} associated with this request (may be {@code null}) + */ + public void setMetadata(AuthorizationRequestMetadata metadata) { + this.metadata = metadata; + } } diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/representation/PermissionRequest.java b/authz/client/src/main/java/org/keycloak/authorization/client/representation/PermissionRequest.java index 39518fc73c..38d54710db 100644 --- a/authz/client/src/main/java/org/keycloak/authorization/client/representation/PermissionRequest.java +++ b/authz/client/src/main/java/org/keycloak/authorization/client/representation/PermissionRequest.java @@ -34,6 +34,25 @@ public class PermissionRequest { private Set scopes; + public PermissionRequest() { + + } + + public PermissionRequest(String resourceSetId, String resourceSetName, Set scopes) { + this.resourceSetId = resourceSetId; + this.resourceSetName = resourceSetName; + this.scopes = scopes; + } + + public PermissionRequest(String resourceSetName) { + this.resourceSetName = resourceSetName; + } + + public PermissionRequest(String resourceSetName, Set scopes) { + this.resourceSetName = resourceSetName; + this.scopes = scopes; + } + public String getResourceSetId() { return this.resourceSetId; } diff --git a/authz/client/src/main/java/org/keycloak/authorization/client/resource/EntitlementResource.java b/authz/client/src/main/java/org/keycloak/authorization/client/resource/EntitlementResource.java index 55c0abdb5d..e81a34f92a 100644 --- a/authz/client/src/main/java/org/keycloak/authorization/client/resource/EntitlementResource.java +++ b/authz/client/src/main/java/org/keycloak/authorization/client/resource/EntitlementResource.java @@ -1,9 +1,11 @@ package org.keycloak.authorization.client.resource; import org.keycloak.authorization.client.AuthorizationDeniedException; +import org.keycloak.authorization.client.representation.AuthorizationRequestMetadata; import org.keycloak.authorization.client.representation.EntitlementRequest; import org.keycloak.authorization.client.representation.EntitlementResponse; import org.keycloak.authorization.client.util.Http; +import org.keycloak.authorization.client.util.HttpMethod; import org.keycloak.authorization.client.util.HttpResponseException; import org.keycloak.util.JsonSerialization; @@ -22,10 +24,27 @@ public class EntitlementResource { public EntitlementResponse getAll(String resourceServerId) { try { - return this.http.get("/authz/entitlement/" + resourceServerId) - .authorizationBearer(this.eat) - .response() - .json(EntitlementResponse.class).execute(); + return getAll(resourceServerId, null); + } catch (HttpResponseException e) { + if (403 == e.getStatusCode()) { + throw new AuthorizationDeniedException(e); + } + throw new RuntimeException("Failed to obtain entitlements.", e); + } catch (Exception e) { + throw new RuntimeException("Failed to obtain entitlements.", e); + } + } + + public EntitlementResponse getAll(String resourceServerId, AuthorizationRequestMetadata metadata) { + try { + HttpMethod method = this.http.get("/authz/entitlement/" + resourceServerId) + .authorizationBearer(this.eat); + + if (metadata != null) { + method.param("include_resource_name", String.valueOf(metadata.isIncludeResourceName())); + } + + return method.response().json(EntitlementResponse.class).execute(); } catch (HttpResponseException e) { if (403 == e.getStatusCode()) { throw new AuthorizationDeniedException(e); diff --git a/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponseBuilder.java b/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponseBuilder.java index 4b59450f5a..81d556ed1d 100644 --- a/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponseBuilder.java +++ b/services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponseBuilder.java @@ -52,7 +52,7 @@ public class PolicyEvaluationResponseBuilder { AccessToken accessToken = identity.getAccessToken(); AccessToken.Authorization authorizationData = new AccessToken.Authorization(); - authorizationData.setPermissions(Permissions.permits(results, authorization, resourceServer.getId())); + authorizationData.setPermissions(Permissions.permits(results, null, authorization, resourceServer)); accessToken.setAuthorization(authorizationData); response.setRpt(accessToken); diff --git a/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java b/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java index e4a263476f..d407613d6d 100644 --- a/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java +++ b/services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java @@ -108,8 +108,15 @@ public class AuthorizationTokenService { try { PermissionTicket ticket = verifyPermissionTicket(authorizationRequest); + ResourceServer resourceServer = authorization.getStoreFactory().getResourceServerStore().findById(ticket.getResourceServerId()); + + if (resourceServer == null) { + throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Client does not support permissions", Status.FORBIDDEN); + } + List result = authorization.evaluators().from(createPermissions(ticket, authorizationRequest, authorization), evaluationContext).evaluate(); - List entitlements = Permissions.permits(result, authorization, ticket.getResourceServerId()); + + List entitlements = Permissions.permits(result, authorizationRequest.getMetadata(), authorization, resourceServer); if (!entitlements.isEmpty()) { AuthorizationResponse response = new AuthorizationResponse(createRequestingPartyToken(entitlements, identity.getAccessToken())); diff --git a/services/src/main/java/org/keycloak/authorization/authorization/representation/AuthorizationRequest.java b/services/src/main/java/org/keycloak/authorization/authorization/representation/AuthorizationRequest.java index d4f0f24ad9..2faf12fe41 100644 --- a/services/src/main/java/org/keycloak/authorization/authorization/representation/AuthorizationRequest.java +++ b/services/src/main/java/org/keycloak/authorization/authorization/representation/AuthorizationRequest.java @@ -23,6 +23,7 @@ package org.keycloak.authorization.authorization.representation; */ public class AuthorizationRequest { + private AuthorizationRequestMetadata metadata; private String ticket; private String rpt; @@ -31,10 +32,6 @@ public class AuthorizationRequest { this.rpt = rpt; } - public AuthorizationRequest(String ticket) { - this(ticket, null); - } - public AuthorizationRequest() { this(null, null); } @@ -46,4 +43,8 @@ public class AuthorizationRequest { public String getRpt() { return this.rpt; } + + public AuthorizationRequestMetadata getMetadata() { + return metadata; + } } diff --git a/services/src/main/java/org/keycloak/authorization/authorization/representation/AuthorizationRequestMetadata.java b/services/src/main/java/org/keycloak/authorization/authorization/representation/AuthorizationRequestMetadata.java new file mode 100644 index 0000000000..92df7441fd --- /dev/null +++ b/services/src/main/java/org/keycloak/authorization/authorization/representation/AuthorizationRequestMetadata.java @@ -0,0 +1,36 @@ +/* + * 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.authorization.representation; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * @author Pedro Igor + */ +public class AuthorizationRequestMetadata { + + @JsonProperty("include_resource_name") + private boolean includeResourceName; + + public boolean isIncludeResourceName() { + return includeResourceName; + } + + public void setIncludeResourceName(boolean includeResourceName) { + this.includeResourceName = includeResourceName; + } +} diff --git a/services/src/main/java/org/keycloak/authorization/entitlement/EntitlementService.java b/services/src/main/java/org/keycloak/authorization/entitlement/EntitlementService.java index 98f38af82d..013cb881c9 100644 --- a/services/src/main/java/org/keycloak/authorization/entitlement/EntitlementService.java +++ b/services/src/main/java/org/keycloak/authorization/entitlement/EntitlementService.java @@ -36,6 +36,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.Context; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; @@ -45,6 +46,7 @@ import org.jboss.resteasy.spi.HttpRequest; import org.keycloak.OAuth2Constants; import org.keycloak.OAuthErrorException; import org.keycloak.authorization.AuthorizationProvider; +import org.keycloak.authorization.authorization.representation.AuthorizationRequestMetadata; import org.keycloak.authorization.common.KeycloakEvaluationContext; import org.keycloak.authorization.common.KeycloakIdentity; import org.keycloak.authorization.entitlement.representation.EntitlementRequest; @@ -100,7 +102,7 @@ public class EntitlementService { @GET() @Produces("application/json") @Consumes("application/json") - public Response getAll(@PathParam("resource_server_id") String resourceServerId) { + public Response getAll(@PathParam("resource_server_id") String resourceServerId, @QueryParam("include_resource_name") Boolean includeResourceName) { KeycloakIdentity identity = new KeycloakIdentity(this.authorization.getKeycloakSession()); if (resourceServerId == null) { @@ -121,7 +123,16 @@ public class EntitlementService { throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Client does not support permissions", Status.FORBIDDEN); } - return evaluate(Permissions.all(resourceServer, identity, authorization), identity, resourceServer); + AuthorizationRequestMetadata metadata; + + if (includeResourceName != null) { + metadata = new AuthorizationRequestMetadata(); + metadata.setIncludeResourceName(includeResourceName); + } else { + metadata = null; + } + + return evaluate(metadata, Permissions.all(resourceServer, identity, authorization), identity, resourceServer); } @Path("{resource_server_id}") @@ -154,13 +165,13 @@ public class EntitlementService { throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Client does not support permissions", Status.FORBIDDEN); } - return evaluate(createPermissions(entitlementRequest, resourceServer, authorization), identity, resourceServer); + return evaluate(entitlementRequest.getMetadata(), createPermissions(entitlementRequest, resourceServer, authorization), identity, resourceServer); } - private Response evaluate(List permissions, KeycloakIdentity identity, ResourceServer resourceServer) { + private Response evaluate(AuthorizationRequestMetadata metadata, List permissions, KeycloakIdentity identity, ResourceServer resourceServer) { try { List result = authorization.evaluators().from(permissions, new KeycloakEvaluationContext(this.authorization.getKeycloakSession())).evaluate(); - List entitlements = Permissions.permits(result, authorization, resourceServer.getId()); + List entitlements = Permissions.permits(result, metadata, authorization, resourceServer); if (!entitlements.isEmpty()) { return Cors.add(request, Response.ok().entity(new EntitlementResponse(createRequestingPartyToken(entitlements, identity.getAccessToken())))).allowedOrigins(identity.getAccessToken()).allowedMethods("GET").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build(); diff --git a/services/src/main/java/org/keycloak/authorization/entitlement/representation/EntitlementRequest.java b/services/src/main/java/org/keycloak/authorization/entitlement/representation/EntitlementRequest.java index 444645a448..f5a5745035 100644 --- a/services/src/main/java/org/keycloak/authorization/entitlement/representation/EntitlementRequest.java +++ b/services/src/main/java/org/keycloak/authorization/entitlement/representation/EntitlementRequest.java @@ -1,24 +1,78 @@ package org.keycloak.authorization.entitlement.representation; -import org.keycloak.authorization.protection.permission.representation.PermissionRequest; - import java.util.ArrayList; import java.util.List; +import org.keycloak.authorization.authorization.representation.AuthorizationRequestMetadata; +import org.keycloak.authorization.protection.permission.representation.PermissionRequest; + /** + *

An {@code {@link EntitlementRequest} represents a request sent to the server containing the permissions being requested. + * + *

Along with an entitlement request additional {@link AuthorizationRequestMetadata} information can be passed in order to define what clients expect from + * the server when evaluating the requested permissions and when returning with a response. + * * @author Pedro Igor */ public class EntitlementRequest { private String rpt; + private AuthorizationRequestMetadata metadata; private List permissions = new ArrayList<>(); + /** + * Returns the permissions being requested. + * + * @return the permissions being requested (not {@code null}) + */ public List getPermissions() { return permissions; } + /** + * Set the permissions being requested + * + * @param permissions the permissions being requests (not {@code null}) + */ + public void setPermissions(List permissions) { + this.permissions = permissions; + } + + /** + * Returns a {@code String} representing a previously issued RPT which permissions will be included the response in addition to the new ones being requested. + * + * @return a previously issued RPT (may be {@code null}) + */ public String getRpt() { return rpt; } + + /** + * A {@code String} representing a previously issued RPT which permissions will be included the response in addition to the new ones being requested. + * + * @param rpt a previously issued RPT. If {@code null}, only the requested permissions are evaluated + */ + public void setRpt(String rpt) { + this.rpt = rpt; + } + + /** + * Return the {@link Metadata} associated with this request. + * + * @return + */ + public AuthorizationRequestMetadata getMetadata() { + return metadata; + } + + /** + * The {@link Metadata} associated with this request. The metadata defines specific information that should be considered + * by the server when evaluating and returning permissions. + * + * @param metadata the {@link Metadata} associated with this request (may be {@code null}) + */ + public void setMetadata(AuthorizationRequestMetadata metadata) { + this.metadata = metadata; + } } 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 a805fbc7c9..2ee705e149 100644 --- a/services/src/main/java/org/keycloak/authorization/util/Permissions.java +++ b/services/src/main/java/org/keycloak/authorization/util/Permissions.java @@ -29,6 +29,7 @@ import java.util.stream.Collectors; import org.keycloak.authorization.AuthorizationProvider; import org.keycloak.authorization.Decision.Effect; +import org.keycloak.authorization.authorization.representation.AuthorizationRequestMetadata; import org.keycloak.authorization.identity.Identity; import org.keycloak.authorization.model.Policy; import org.keycloak.authorization.model.Resource; @@ -134,7 +135,7 @@ public final class Permissions { return permissions; } - public static List permits(List evaluation, AuthorizationProvider authorizationProvider, String resourceServerId) { + public static List permits(List evaluation, AuthorizationRequestMetadata metadata, AuthorizationProvider authorizationProvider, ResourceServer resourceServer) { Map permissions = new HashMap<>(); for (Result result : evaluation) { @@ -188,14 +189,14 @@ public final class Permissions { if (deniedCount == 0) { result.setStatus(Effect.PERMIT); - grantPermission(authorizationProvider, permissions, permission, resourceServerId); + grantPermission(authorizationProvider, permissions, permission, resourceServer, metadata); } else { // if a full deny or resource denied or the requested scopes were denied if (deniedCount == results.size() || resourceDenied || (!deniedScopes.isEmpty() && grantedScopes.isEmpty())) { result.setStatus(Effect.DENY); } else { result.setStatus(Effect.PERMIT); - grantPermission(authorizationProvider, permissions, permission, resourceServerId); + grantPermission(authorizationProvider, permissions, permission, resourceServer, metadata); } } } @@ -212,7 +213,7 @@ public final class Permissions { return "scope".equals(policy.getType()); } - private static void grantPermission(AuthorizationProvider authorizationProvider, Map permissions, ResourcePermission permission, String resourceServer) { + private static void grantPermission(AuthorizationProvider authorizationProvider, Map permissions, ResourcePermission permission, ResourceServer resourceServer, AuthorizationRequestMetadata metadata) { List resources = new ArrayList<>(); Resource resource = permission.getResource(); Set scopes = permission.getScopes().stream().map(Scope::getName).collect(Collectors.toSet()); @@ -224,14 +225,14 @@ public final class Permissions { if (!permissionScopes.isEmpty()) { ResourceStore resourceStore = authorizationProvider.getStoreFactory().getResourceStore(); - resources.addAll(resourceStore.findByScope(permissionScopes.stream().map(Scope::getId).collect(Collectors.toList()), resourceServer)); + resources.addAll(resourceStore.findByScope(permissionScopes.stream().map(Scope::getId).collect(Collectors.toList()), resourceServer.getId())); } } if (!resources.isEmpty()) { for (Resource allowedResource : resources) { String resourceId = allowedResource.getId(); - String resourceName = allowedResource.getName(); + String resourceName = metadata == null || metadata.isIncludeResourceName() ? allowedResource.getName() : null; Permission evalPermission = permissions.get(allowedResource.getId()); if (evalPermission == null) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/EntitlementAPITest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/EntitlementAPITest.java new file mode 100644 index 0000000000..b9f422611d --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/EntitlementAPITest.java @@ -0,0 +1,190 @@ +/* + * 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.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import java.io.IOException; +import java.util.List; +import java.util.function.Supplier; + +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.authorization.client.AuthzClient; +import org.keycloak.authorization.client.Configuration; +import org.keycloak.authorization.client.representation.AuthorizationRequestMetadata; +import org.keycloak.authorization.client.representation.EntitlementRequest; +import org.keycloak.authorization.client.representation.EntitlementResponse; +import org.keycloak.authorization.client.representation.PermissionRequest; +import org.keycloak.jose.jws.JWSInput; +import org.keycloak.jose.jws.JWSInputException; +import org.keycloak.representations.AccessToken; +import org.keycloak.representations.idm.RealmRepresentation; +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.AbstractKeycloakTest; +import org.keycloak.testsuite.util.ClientBuilder; +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 EntitlementAPITest extends AbstractKeycloakTest { + + private AuthzClient authzClient; + + @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()) + .build()); + } + + @Before + public void configureAuthorization() throws Exception { + ClientResource client = getClient(getRealm()); + AuthorizationResource authorization = client.authorization(); + ResourceRepresentation resource = new ResourceRepresentation("Resource A"); + + Response response = authorization.resources().create(resource); + response.close(); + + JSPolicyRepresentation policy = new JSPolicyRepresentation(); + + policy.setName("Default Policy"); + policy.setCode("$evaluation.grant();"); + + response = authorization.policies().js().create(policy); + response.close(); + + ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation(); + + permission.setName(resource.getName() + " Permission"); + permission.addResource(resource.getName()); + permission.addPolicy(policy.getName()); + + response = authorization.permissions().resource().create(permission); + response.close(); + } + + @Test + public void testRptRequestWithoutResourceName() { + AuthorizationRequestMetadata metadata = new AuthorizationRequestMetadata(); + + metadata.setIncludeResourceName(false); + + assertResponse(metadata, () -> getAuthzClient().entitlement(authzClient.obtainAccessToken("marta", "password").getToken()).getAll("resource-server-test", metadata)); + assertResponse(metadata, () -> { + EntitlementRequest request = new EntitlementRequest(); + + request.setMetadata(metadata); + request.addPermission(new PermissionRequest("Resource A")); + + return getAuthzClient().entitlement(authzClient.obtainAccessToken("marta", "password").getToken()).get("resource-server-test", request); + }); + } + + @Test + public void testRptRequestWithResourceName() { + AuthorizationRequestMetadata metadata = new AuthorizationRequestMetadata(); + + metadata.setIncludeResourceName(true); + + assertResponse(metadata, () -> getAuthzClient().entitlement(authzClient.obtainAccessToken("marta", "password").getToken()).getAll("resource-server-test", metadata)); + assertResponse(metadata, () -> getAuthzClient().entitlement(authzClient.obtainAccessToken("marta", "password").getToken()).getAll("resource-server-test")); + + EntitlementRequest request = new EntitlementRequest(); + + request.setMetadata(metadata); + request.addPermission(new PermissionRequest("Resource A")); + + assertResponse(metadata, () -> getAuthzClient().entitlement(authzClient.obtainAccessToken("marta", "password").getToken()).get("resource-server-test", request)); + + request.setMetadata(null); + + assertResponse(metadata, () -> getAuthzClient().entitlement(authzClient.obtainAccessToken("marta", "password").getToken()).get("resource-server-test", request)); + } + + private void assertResponse(AuthorizationRequestMetadata metadata, Supplier responseSupplier) { + EntitlementResponse response = responseSupplier.get(); + AccessToken accessToken; + + try { + accessToken = new JWSInput(response.getRpt()).readJsonContent(AccessToken.class); + } catch (JWSInputException cause) { + throw new RuntimeException("Failed to deserialize RPT", cause); + } + + AccessToken.Authorization authorization = accessToken.getAuthorization(); + + List permissions = authorization.getPermissions(); + + assertNotNull(permissions); + assertFalse(permissions.isEmpty()); + + for (Permission permission : permissions) { + if (metadata.isIncludeResourceName()) { + assertNotNull(permission.getResourceSetName()); + } else { + assertNull(permission.getResourceSetName()); + } + } + } + + 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() { + if (authzClient == null) { + try { + authzClient = 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); + } + } + + return authzClient; + } +}