keycloak-aplcache
Changes
authz/client/src/main/java/org/keycloak/authorization/client/representation/AuthorizationRequestMetadata.java 36(+36 -0)
authz/client/src/main/java/org/keycloak/authorization/client/representation/EntitlementRequest.java 58(+54 -4)
authz/client/src/main/java/org/keycloak/authorization/client/representation/PermissionRequest.java 19(+19 -0)
authz/client/src/main/java/org/keycloak/authorization/client/resource/EntitlementResource.java 27(+23 -4)
services/src/main/java/org/keycloak/authorization/admin/representation/PolicyEvaluationResponseBuilder.java 2(+1 -1)
services/src/main/java/org/keycloak/authorization/authorization/AuthorizationTokenService.java 9(+8 -1)
services/src/main/java/org/keycloak/authorization/authorization/representation/AuthorizationRequest.java 9(+5 -4)
services/src/main/java/org/keycloak/authorization/authorization/representation/AuthorizationRequestMetadata.java 36(+36 -0)
Details
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 0000000..0dfd416
--- /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 <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;
+ }
+}
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 daec233..b3efa85 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;
/**
+ * <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;
+ }
+
+ /**
+ * 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;
}
- public void setPermissions(List<PermissionRequest> permissions) {
- this.permissions = permissions;
+ /**
+ * Return the {@link Metadata} associated with this request.
+ *
+ * @return
+ */
+ public AuthorizationRequestMetadata getMetadata() {
+ return metadata;
}
- public void addPermission(PermissionRequest request) {
- getPermissions().add(request);
+ /**
+ * 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 39518fc..38d5471 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<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;
}
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 55c0abd..e81a34f 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.<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);
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 4b59450..81d556e 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 e4a2634..d407613 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> 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()));
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 d4f0f24..2faf12f 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 0000000..92df744
--- /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 <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;
+ }
+}
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 98f38af..013cb88 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<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();
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 444645a..f5a5745 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;
+
/**
+ * <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;
+ }
}
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 a805fbc..2ee705e 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<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) {
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 0000000..b9f4226
--- /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 <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;
+ }
+}