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..ca45017410
--- /dev/null
+++ b/authz/client/src/main/java/org/keycloak/authorization/client/representation/AuthorizationRequestMetadata.java
@@ -0,0 +1,48 @@
+/*
+ * 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 {
+
+ public static final String INCLUDE_RESOURCE_NAME = "include_resource_name";
+
+ @JsonProperty(INCLUDE_RESOURCE_NAME)
+ private boolean includeResourceName = true;
+
+ private int limit;
+
+ public boolean isIncludeResourceName() {
+ return includeResourceName;
+ }
+
+ public void setIncludeResourceName(boolean includeResourceName) {
+ this.includeResourceName = includeResourceName;
+ }
+
+ public void setLimit(int limit) {
+ this.limit = limit;
+ }
+
+ public int getLimit() {
+ return limit;
+ }
+}
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..8c12abf7e4 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;
@@ -23,9 +25,8 @@ public class EntitlementResource {
public EntitlementResponse getAll(String resourceServerId) {
try {
return this.http.get("/authz/entitlement/" + resourceServerId)
- .authorizationBearer(this.eat)
- .response()
- .json(EntitlementResponse.class).execute();
+ .authorizationBearer(eat)
+ .response().json(EntitlementResponse.class).execute();
} catch (HttpResponseException e) {
if (403 == e.getStatusCode()) {
throw new AuthorizationDeniedException(e);
@@ -39,7 +40,7 @@ public class EntitlementResource {
public EntitlementResponse get(String resourceServerId, EntitlementRequest request) {
try {
return this.http.post("/authz/entitlement/" + resourceServerId)
- .authorizationBearer(this.eat)
+ .authorizationBearer(eat)
.json(JsonSerialization.writeValueAsBytes(request))
.response().json(EntitlementResponse.class).execute();
} catch (HttpResponseException e) {
diff --git a/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DecisionResultCollector.java b/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DecisionResultCollector.java
index 02312ba86a..2ffc049ee4 100644
--- a/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DecisionResultCollector.java
+++ b/server-spi-private/src/main/java/org/keycloak/authorization/policy/evaluation/DecisionResultCollector.java
@@ -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 {
- private Map results = new HashMap();
+ private Map results = new LinkedHashMap<>();
@Override
public void onDecision(DefaultEvaluation evaluation) {
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..faa90ce2df
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authorization/authorization/representation/AuthorizationRequestMetadata.java
@@ -0,0 +1,51 @@
+/*
+ * 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 java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.sun.org.apache.xpath.internal.operations.Bool;
+
+/**
+ * @author Pedro Igor
+ */
+public class AuthorizationRequestMetadata {
+
+ public static final String INCLUDE_RESOURCE_NAME = "include_resource_name";
+
+ @JsonProperty(INCLUDE_RESOURCE_NAME)
+ private boolean includeResourceName = true;
+
+ private int limit;
+
+ public boolean isIncludeResourceName() {
+ return includeResourceName;
+ }
+
+ public void setIncludeResourceName(boolean includeResourceName) {
+ this.includeResourceName = includeResourceName;
+ }
+
+ public int getLimit() {
+ return limit;
+ }
+
+ public void setLimit(int limit) {
+ this.limit = limit;
+ }
+}
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..54097bbe36 100644
--- a/services/src/main/java/org/keycloak/authorization/entitlement/EntitlementService.java
+++ b/services/src/main/java/org/keycloak/authorization/entitlement/EntitlementService.java
@@ -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;
@@ -36,6 +37,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 +47,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;
@@ -54,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;
@@ -121,7 +125,7 @@ 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);
+ return evaluate(null, Permissions.all(resourceServer, identity, authorization), identity, resourceServer);
}
@Path("{resource_server_id}")
@@ -154,13 +158,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();
@@ -192,9 +196,15 @@ public class EntitlementService {
private List createPermissions(EntitlementRequest entitlementRequest, ResourceServer resourceServer, AuthorizationProvider authorization) {
StoreFactory storeFactory = authorization.getStoreFactory();
- Map> permissionsToEvaluate = new HashMap<>();
+ Map> 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) {
@@ -208,14 +218,17 @@ public class EntitlementService {
}
Set requestedScopes = requestedResource.getScopes().stream().map(ScopeRepresentation::new).collect(Collectors.toSet());
- Set collect = requestedScopes.stream().map(ScopeRepresentation::getName).collect(Collectors.toSet());
+ Set 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 resources = new ArrayList();
+ List resources = new ArrayList<>();
resources.addAll(resourceStore.findByScope(requestedScopes.stream().map(scopeRepresentation -> {
Scope scope = scopeStore.findByName(scopeRepresentation.getName(), resourceServer.getId());
@@ -228,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);
}
@@ -258,7 +275,11 @@ public class EntitlementService {
List 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) {
@@ -267,6 +288,9 @@ public class EntitlementService {
if (scopes == null) {
scopes = new HashSet<>();
permissionsToEvaluate.put(resourcePermission.getId(), scopes);
+ if (limit != null) {
+ limit--;
+ }
}
Set scopePermission = permission.getScopes();
@@ -275,7 +299,7 @@ public class EntitlementService {
scopes.addAll(scopePermission);
}
}
- });
+ }
}
}
}
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..325c056eb7 100644
--- a/services/src/main/java/org/keycloak/authorization/util/Permissions.java
+++ b/services/src/main/java/org/keycloak/authorization/util/Permissions.java
@@ -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;
@@ -29,6 +30,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,8 +136,8 @@ public final class Permissions {
return permissions;
}
- public static List permits(List evaluation, AuthorizationProvider authorizationProvider, String resourceServerId) {
- Map permissions = new HashMap<>();
+ public static List permits(List evaluation, AuthorizationRequestMetadata metadata, AuthorizationProvider authorizationProvider, ResourceServer resourceServer) {
+ Map permissions = new LinkedHashMap<>();
for (Result result : evaluation) {
Set deniedScopes = new HashSet<>();
@@ -188,14 +190,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 +214,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 +226,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..0f2ac61da4
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/authz/EntitlementAPITest.java
@@ -0,0 +1,285 @@
+/*
+ * 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.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();
+
+ JSPolicyRepresentation policy = new JSPolicyRepresentation();
+
+ policy.setName("Default Policy");
+ policy.setCode("$evaluation.grant();");
+
+ authorization.policies().js().create(policy).close();
+
+ for (int i = 1; i <= 20; i++) {
+ ResourceRepresentation resource = new ResourceRepresentation("Resource " + i);
+
+ authorization.resources().create(resource).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
+ public void testRptRequestWithoutResourceName() {
+ AuthorizationRequestMetadata metadata = new AuthorizationRequestMetadata();
+
+ metadata.setIncludeResourceName(false);
+
+ assertResponse(metadata, () -> {
+ EntitlementRequest request = new EntitlementRequest();
+
+ request.setMetadata(metadata);
+ request.addPermission(new PermissionRequest("Resource 1"));
+
+ 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"));
+
+ EntitlementRequest request = new EntitlementRequest();
+
+ request.setMetadata(metadata);
+ request.addPermission(new PermissionRequest("Resource 13"));
+
+ 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));
+ }
+
+ @Test
+ public void testPermissionLimit() {
+ EntitlementRequest request = new EntitlementRequest();
+
+ for (int i = 1; i <= 10; i++) {
+ request.addPermission(new PermissionRequest("Resource " + i));
+ }
+
+ 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 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 responseSupplier) {
+ AccessToken.Authorization authorization = toAccessToken(responseSupplier.get()).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 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");
+ }
+
+ 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;
+ }
+}