[KEYCLOAK-4991] - Allow clients to limit the number of permission in a RPT when using entitlements

This commit is contained in:
Pedro Igor 2017-06-02 19:06:40 -03:00
parent 813af5d757
commit d0f505455d
7 changed files with 182 additions and 98 deletions

View file

@ -26,7 +26,9 @@ public class AuthorizationRequestMetadata {
public static final String INCLUDE_RESOURCE_NAME = "include_resource_name";
@JsonProperty(INCLUDE_RESOURCE_NAME)
private boolean includeResourceName;
private boolean includeResourceName = true;
private int limit;
public boolean isIncludeResourceName() {
return includeResourceName;
@ -35,4 +37,12 @@ public class AuthorizationRequestMetadata {
public void setIncludeResourceName(boolean includeResourceName) {
this.includeResourceName = includeResourceName;
}
public void setLimit(int limit) {
this.limit = limit;
}
public int getLimit() {
return limit;
}
}

View file

@ -24,31 +24,9 @@ public class EntitlementResource {
public EntitlementResponse getAll(String resourceServerId) {
try {
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) {
StringBuilder params = new StringBuilder();
params.append(AuthorizationRequestMetadata.INCLUDE_RESOURCE_NAME).append("=").append(metadata.isIncludeResourceName());
method.param("metadata", params.toString());
}
return method.response().json(EntitlementResponse.class).execute();
return this.http.<EntitlementResponse>get("/authz/entitlement/" + resourceServerId)
.authorizationBearer(eat)
.response().json(EntitlementResponse.class).execute();
} catch (HttpResponseException e) {
if (403 == e.getStatusCode()) {
throw new AuthorizationDeniedException(e);
@ -62,7 +40,7 @@ public class EntitlementResource {
public EntitlementResponse get(String resourceServerId, EntitlementRequest request) {
try {
return this.http.<EntitlementResponse>post("/authz/entitlement/" + resourceServerId)
.authorizationBearer(this.eat)
.authorizationBearer(eat)
.json(JsonSerialization.writeValueAsBytes(request))
.response().json(EntitlementResponse.class).execute();
} catch (HttpResponseException e) {

View file

@ -19,6 +19,7 @@
package org.keycloak.authorization.policy.evaluation;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@ -33,7 +34,7 @@ import org.keycloak.representations.idm.authorization.DecisionStrategy;
*/
public abstract class DecisionResultCollector implements Decision<DefaultEvaluation> {
private Map<ResourcePermission, Result> results = new HashMap();
private Map<ResourcePermission, Result> results = new LinkedHashMap<>();
@Override
public void onDecision(DefaultEvaluation evaluation) {

View file

@ -29,17 +29,9 @@ public class AuthorizationRequestMetadata {
public static final String INCLUDE_RESOURCE_NAME = "include_resource_name";
@JsonProperty(INCLUDE_RESOURCE_NAME)
private boolean includeResourceName;
private boolean includeResourceName = true;
public AuthorizationRequestMetadata() {
this(null);
}
public AuthorizationRequestMetadata(Map<String, String> claims) {
if (claims != null) {
includeResourceName = Boolean.valueOf(claims.getOrDefault(INCLUDE_RESOURCE_NAME, "true")).booleanValue();
}
}
private int limit;
public boolean isIncludeResourceName() {
return includeResourceName;
@ -48,4 +40,12 @@ public class AuthorizationRequestMetadata {
public void setIncludeResourceName(boolean includeResourceName) {
this.includeResourceName = includeResourceName;
}
public int getLimit() {
return limit;
}
public void setLimit(int limit) {
this.limit = limit;
}
}

View file

@ -21,6 +21,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@ -56,6 +57,7 @@ import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.model.Scope;
import org.keycloak.authorization.permission.ResourcePermission;
import org.keycloak.authorization.policy.evaluation.Result;
import org.keycloak.authorization.protection.permission.representation.PermissionRequest;
import org.keycloak.authorization.store.ResourceStore;
import org.keycloak.authorization.store.ScopeStore;
import org.keycloak.authorization.store.StoreFactory;
@ -102,7 +104,7 @@ public class EntitlementService {
@GET()
@Produces("application/json")
@Consumes("application/json")
public Response getAll(@PathParam("resource_server_id") String resourceServerId, @QueryParam("metadata") String metadataParam) {
public Response getAll(@PathParam("resource_server_id") String resourceServerId) {
KeycloakIdentity identity = new KeycloakIdentity(this.authorization.getKeycloakSession());
if (resourceServerId == null) {
@ -123,7 +125,7 @@ public class EntitlementService {
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "Client does not support permissions", Status.FORBIDDEN);
}
return evaluate(getMetadata(metadataParam), Permissions.all(resourceServer, identity, authorization), identity, resourceServer);
return evaluate(null, Permissions.all(resourceServer, identity, authorization), identity, resourceServer);
}
@Path("{resource_server_id}")
@ -194,9 +196,15 @@ public class EntitlementService {
private List<ResourcePermission> createPermissions(EntitlementRequest entitlementRequest, ResourceServer resourceServer, AuthorizationProvider authorization) {
StoreFactory storeFactory = authorization.getStoreFactory();
Map<String, Set<String>> permissionsToEvaluate = new HashMap<>();
Map<String, Set<String>> permissionsToEvaluate = new LinkedHashMap<>();
AuthorizationRequestMetadata metadata = entitlementRequest.getMetadata();
Integer limit = metadata != null && metadata.getLimit() > 0 ? metadata.getLimit() : null;
for (PermissionRequest requestedResource : entitlementRequest.getPermissions()) {
if (limit != null && limit <= 0) {
break;
}
entitlementRequest.getPermissions().forEach(requestedResource -> {
Resource resource;
if (requestedResource.getResourceSetId() != null) {
@ -210,14 +218,17 @@ public class EntitlementService {
}
Set<ScopeRepresentation> requestedScopes = requestedResource.getScopes().stream().map(ScopeRepresentation::new).collect(Collectors.toSet());
Set<String> collect = requestedScopes.stream().map(ScopeRepresentation::getName).collect(Collectors.toSet());
Set<String> scopeNames = requestedScopes.stream().map(ScopeRepresentation::getName).collect(Collectors.toSet());
if (resource != null) {
permissionsToEvaluate.put(resource.getId(), collect);
permissionsToEvaluate.put(resource.getId(), scopeNames);
if (limit != null) {
limit--;
}
} else {
ResourceStore resourceStore = authorization.getStoreFactory().getResourceStore();
ScopeStore scopeStore = authorization.getStoreFactory().getScopeStore();
List<Resource> resources = new ArrayList<Resource>();
List<Resource> resources = new ArrayList<>();
resources.addAll(resourceStore.findByScope(requestedScopes.stream().map(scopeRepresentation -> {
Scope scope = scopeStore.findByName(scopeRepresentation.getName(), resourceServer.getId());
@ -230,17 +241,21 @@ public class EntitlementService {
}).filter(s -> s != null).collect(Collectors.toList()), resourceServer.getId()));
for (Resource resource1 : resources) {
permissionsToEvaluate.put(resource1.getId(), collect);
permissionsToEvaluate.put(resource1.getId(), scopeNames);
if (limit != null) {
limit--;
}
}
permissionsToEvaluate.put("$KC_SCOPE_PERMISSION", collect);
permissionsToEvaluate.put("$KC_SCOPE_PERMISSION", scopeNames);
}
});
}
String rpt = entitlementRequest.getRpt();
if (rpt != null && !"".equals(rpt)) {
KeycloakContext context = authorization.getKeycloakSession().getContext();
if (!Tokens.verifySignature(session, context.getRealm(), rpt)) {
throw new ErrorResponseException("invalid_rpt", "RPT signature is invalid", Status.FORBIDDEN);
}
@ -260,7 +275,11 @@ public class EntitlementService {
List<Permission> permissions = authorizationData.getPermissions();
if (permissions != null) {
permissions.forEach(permission -> {
for (Permission permission : permissions) {
if (limit != null && limit <= 0) {
break;
}
Resource resourcePermission = storeFactory.getResourceStore().findById(permission.getResourceSetId(), resourceServer.getId());
if (resourcePermission != null) {
@ -269,6 +288,9 @@ public class EntitlementService {
if (scopes == null) {
scopes = new HashSet<>();
permissionsToEvaluate.put(resourcePermission.getId(), scopes);
if (limit != null) {
limit--;
}
}
Set<String> scopePermission = permission.getScopes();
@ -277,7 +299,7 @@ public class EntitlementService {
scopes.addAll(scopePermission);
}
}
});
}
}
}
}
@ -297,27 +319,4 @@ public class EntitlementService {
}
}).collect(Collectors.toList());
}
private AuthorizationRequestMetadata getMetadata(@QueryParam("metadata") String metadataParam) {
AuthorizationRequestMetadata metadata;
if (metadataParam != null) {
Map<String, String> claims = new HashMap<>();
for (String claim : metadataParam.split(",")) {
String[] values = claim.split("=");
if (values.length < 2) {
throw new ErrorResponseException("invalid_metadata", "Invalid metadata", Status.BAD_REQUEST);
}
claims.put(values[0], values[1]);
}
metadata = new AuthorizationRequestMetadata(claims);
} else {
metadata = null;
}
return metadata;
}
}

View file

@ -21,6 +21,7 @@ package org.keycloak.authorization.util;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@ -136,7 +137,7 @@ public final class Permissions {
}
public static List<Permission> permits(List<Result> evaluation, AuthorizationRequestMetadata metadata, AuthorizationProvider authorizationProvider, ResourceServer resourceServer) {
Map<String, Permission> permissions = new HashMap<>();
Map<String, Permission> permissions = new LinkedHashMap<>();
for (Result result : evaluation) {
Set<Scope> deniedScopes = new HashSet<>();

View file

@ -16,6 +16,7 @@
*/
package org.keycloak.testsuite.authz;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
@ -80,27 +81,27 @@ public class EntitlementAPITest extends AbstractKeycloakTest {
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();
authorization.policies().js().create(policy).close();
ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation();
for (int i = 1; i <= 20; i++) {
ResourceRepresentation resource = new ResourceRepresentation("Resource " + i);
permission.setName(resource.getName() + " Permission");
permission.addResource(resource.getName());
permission.addPolicy(policy.getName());
authorization.resources().create(resource).close();
response = authorization.permissions().resource().create(permission);
response.close();
ResourcePermissionRepresentation permission = new ResourcePermissionRepresentation();
permission.setName(resource.getName() + " Permission");
permission.addResource(resource.getName());
permission.addPolicy(policy.getName());
authorization.permissions().resource().create(permission).close();
}
}
@Test
@ -109,12 +110,11 @@ public class EntitlementAPITest extends AbstractKeycloakTest {
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"));
request.addPermission(new PermissionRequest("Resource 1"));
return getAuthzClient().entitlement(authzClient.obtainAccessToken("marta", "password").getToken()).get("resource-server-test", request);
});
@ -126,13 +126,12 @@ public class EntitlementAPITest extends AbstractKeycloakTest {
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"));
request.addPermission(new PermissionRequest("Resource 13"));
assertResponse(metadata, () -> getAuthzClient().entitlement(authzClient.obtainAccessToken("marta", "password").getToken()).get("resource-server-test", request));
@ -141,17 +140,102 @@ public class EntitlementAPITest extends AbstractKeycloakTest {
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;
@Test
public void testPermissionLimit() {
EntitlementRequest request = new EntitlementRequest();
try {
accessToken = new JWSInput(response.getRpt()).readJsonContent(AccessToken.class);
} catch (JWSInputException cause) {
throw new RuntimeException("Failed to deserialize RPT", cause);
for (int i = 1; i <= 10; i++) {
request.addPermission(new PermissionRequest("Resource " + i));
}
AccessToken.Authorization authorization = accessToken.getAuthorization();
AuthorizationRequestMetadata metadata = new AuthorizationRequestMetadata();
metadata.setLimit(10);
request.setMetadata(metadata);
EntitlementResponse response = getAuthzClient().entitlement(authzClient.obtainAccessToken("marta", "password").getToken()).get("resource-server-test", request);
AccessToken rpt = toAccessToken(response);
List<Permission> permissions = rpt.getAuthorization().getPermissions();
assertEquals(10, permissions.size());
for (int i = 0; i < 10; i++) {
assertEquals("Resource " + (i + 1), permissions.get(i).getResourceSetName());
}
request = new EntitlementRequest();
for (int i = 11; i <= 15; i++) {
request.addPermission(new PermissionRequest("Resource " + i));
}
request.setMetadata(metadata);
request.setRpt(response.getRpt());
response = getAuthzClient().entitlement(authzClient.obtainAccessToken("marta", "password").getToken()).get("resource-server-test", request);
rpt = toAccessToken(response);
permissions = rpt.getAuthorization().getPermissions();
assertEquals(10, permissions.size());
for (int i = 0; i < 10; i++) {
if (i < 5) {
assertEquals("Resource " + (i + 11), permissions.get(i).getResourceSetName());
} else {
assertEquals("Resource " + (i - 4), permissions.get(i).getResourceSetName());
}
}
request = new EntitlementRequest();
for (int i = 16; i <= 18; i++) {
request.addPermission(new PermissionRequest("Resource " + i));
}
request.setMetadata(metadata);
request.setRpt(response.getRpt());
response = getAuthzClient().entitlement(authzClient.obtainAccessToken("marta", "password").getToken()).get("resource-server-test", request);
rpt = toAccessToken(response);
permissions = rpt.getAuthorization().getPermissions();
assertEquals(10, permissions.size());
assertEquals("Resource 16", permissions.get(0).getResourceSetName());
assertEquals("Resource 17", permissions.get(1).getResourceSetName());
assertEquals("Resource 18", permissions.get(2).getResourceSetName());
assertEquals("Resource 11", permissions.get(3).getResourceSetName());
assertEquals("Resource 12", permissions.get(4).getResourceSetName());
assertEquals("Resource 13", permissions.get(5).getResourceSetName());
assertEquals("Resource 14", permissions.get(6).getResourceSetName());
assertEquals("Resource 15", permissions.get(7).getResourceSetName());
assertEquals("Resource 1", permissions.get(8).getResourceSetName());
assertEquals("Resource 2", permissions.get(9).getResourceSetName());
request = new EntitlementRequest();
metadata.setLimit(5);
request.setMetadata(metadata);
request.setRpt(response.getRpt());
response = getAuthzClient().entitlement(authzClient.obtainAccessToken("marta", "password").getToken()).get("resource-server-test", request);
rpt = toAccessToken(response);
permissions = rpt.getAuthorization().getPermissions();
assertEquals(5, permissions.size());
assertEquals("Resource 16", permissions.get(0).getResourceSetName());
assertEquals("Resource 17", permissions.get(1).getResourceSetName());
assertEquals("Resource 18", permissions.get(2).getResourceSetName());
assertEquals("Resource 11", permissions.get(3).getResourceSetName());
assertEquals("Resource 12", permissions.get(4).getResourceSetName());
}
private void assertResponse(AuthorizationRequestMetadata metadata, Supplier<EntitlementResponse> responseSupplier) {
AccessToken.Authorization authorization = toAccessToken(responseSupplier.get()).getAuthorization();
List<Permission> permissions = authorization.getPermissions();
@ -167,6 +251,17 @@ public class EntitlementAPITest extends AbstractKeycloakTest {
}
}
private AccessToken toAccessToken(EntitlementResponse response) {
AccessToken accessToken;
try {
accessToken = new JWSInput(response.getRpt()).readJsonContent(AccessToken.class);
} catch (JWSInputException cause) {
throw new RuntimeException("Failed to deserialize RPT", cause);
}
return accessToken;
}
private RealmResource getRealm() throws Exception {
return adminClient.realm("authz-test");
}