[KEYCLOAK-4992] - Allow clients to exclude resource_set_name from RPT

This commit is contained in:
Pedro Igor 2017-05-31 19:33:34 -03:00
parent b71bb96540
commit dcd1a68d95
12 changed files with 455 additions and 31 deletions

View file

@ -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 <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class AuthorizationRequestMetadata {
@JsonProperty("include_resource_name")
private boolean includeResourceName;
public boolean isIncludeResourceName() {
return includeResourceName;
}
public void setIncludeResourceName(boolean includeResourceName) {
this.includeResourceName = includeResourceName;
}
}

View file

@ -4,31 +4,81 @@ import java.util.ArrayList;
import java.util.List;
/**
* <p>An {@code {@link EntitlementRequest} represents a request sent to the server containing the permissions being requested.
*
* <p>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 <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class EntitlementRequest {
private String rpt;
private AuthorizationRequestMetadata metadata;
private List<PermissionRequest> permissions = new ArrayList<>();
/**
* Returns the permissions being requested.
*
* @return the permissions being requested (not {@code null})
*/
public List<PermissionRequest> 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<PermissionRequest> 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;
}
}

View file

@ -34,6 +34,25 @@ public class PermissionRequest {
private Set<String> scopes;
public PermissionRequest() {
}
public PermissionRequest(String resourceSetId, String resourceSetName, Set<String> scopes) {
this.resourceSetId = resourceSetId;
this.resourceSetName = resourceSetName;
this.scopes = scopes;
}
public PermissionRequest(String resourceSetName) {
this.resourceSetName = resourceSetName;
}
public PermissionRequest(String resourceSetName, Set<String> scopes) {
this.resourceSetName = resourceSetName;
this.scopes = scopes;
}
public String getResourceSetId() {
return this.resourceSetId;
}

View file

@ -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.<EntitlementResponse>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<EntitlementResponse> method = this.http.<EntitlementResponse>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);

View file

@ -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);

View file

@ -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> result = authorization.evaluators().from(createPermissions(ticket, authorizationRequest, authorization), evaluationContext).evaluate();
List<Permission> entitlements = Permissions.permits(result, authorization, ticket.getResourceServerId());
List<Permission> entitlements = Permissions.permits(result, authorizationRequest.getMetadata(), authorization, resourceServer);
if (!entitlements.isEmpty()) {
AuthorizationResponse response = new AuthorizationResponse(createRequestingPartyToken(entitlements, identity.getAccessToken()));

View file

@ -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;
}
}

View file

@ -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 <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class AuthorizationRequestMetadata {
@JsonProperty("include_resource_name")
private boolean includeResourceName;
public boolean isIncludeResourceName() {
return includeResourceName;
}
public void setIncludeResourceName(boolean includeResourceName) {
this.includeResourceName = includeResourceName;
}
}

View file

@ -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<ResourcePermission> permissions, KeycloakIdentity identity, ResourceServer resourceServer) {
private Response evaluate(AuthorizationRequestMetadata metadata, List<ResourcePermission> permissions, KeycloakIdentity identity, ResourceServer resourceServer) {
try {
List<Result> result = authorization.evaluators().from(permissions, new KeycloakEvaluationContext(this.authorization.getKeycloakSession())).evaluate();
List<Permission> entitlements = Permissions.permits(result, authorization, resourceServer.getId());
List<Permission> 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();

View file

@ -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;
/**
* <p>An {@code {@link EntitlementRequest} represents a request sent to the server containing the permissions being requested.
*
* <p>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 <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class EntitlementRequest {
private String rpt;
private AuthorizationRequestMetadata metadata;
private List<PermissionRequest> permissions = new ArrayList<>();
/**
* Returns the permissions being requested.
*
* @return the permissions being requested (not {@code null})
*/
public List<PermissionRequest> getPermissions() {
return permissions;
}
/**
* Set the permissions being requested
*
* @param permissions the permissions being requests (not {@code null})
*/
public void setPermissions(List<PermissionRequest> 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;
}
}

View file

@ -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<Permission> permits(List<Result> evaluation, AuthorizationProvider authorizationProvider, String resourceServerId) {
public static List<Permission> permits(List<Result> evaluation, AuthorizationRequestMetadata metadata, AuthorizationProvider authorizationProvider, ResourceServer resourceServer) {
Map<String, Permission> 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<String, Permission> permissions, ResourcePermission permission, String resourceServer) {
private static void grantPermission(AuthorizationProvider authorizationProvider, Map<String, Permission> permissions, ResourcePermission permission, ResourceServer resourceServer, AuthorizationRequestMetadata metadata) {
List<Resource> resources = new ArrayList<>();
Resource resource = permission.getResource();
Set<String> 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) {

View file

@ -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 <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class EntitlementAPITest extends AbstractKeycloakTest {
private AuthzClient authzClient;
@Override
public void addTestRealms(List<RealmRepresentation> 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<EntitlementResponse> 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<Permission> 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;
}
}