AuthZen Evaluations API

Closes #47825

Signed-off-by: Ryan Emerson <remerson@ibm.com>
This commit is contained in:
Ryan Emerson 2026-04-07 15:37:43 +01:00 committed by Pedro Igor
parent a9d523b0cd
commit 5811348cbc
10 changed files with 1003 additions and 91 deletions

View file

@ -16,18 +16,21 @@
*/
package org.keycloak.authorization.authzen;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonValue;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
public final class AuthZen {
public static final String AUTHZEN_ACCESS_PATH = "access/v1";
public static final String EVALUATION_PATH = AUTHZEN_ACCESS_PATH + "/evaluation";
public static final String EVALUATIONS_PATH = AUTHZEN_ACCESS_PATH + "/evaluations";
private AuthZen() {
}
@ -80,6 +83,43 @@ public final class AuthZen {
@JsonProperty(required = true) Action action,
Map<String, Object> context) {}
@JsonIgnoreProperties(ignoreUnknown = true)
public record EvaluationResponse(boolean decision) {}
public record EvaluationResponse(boolean decision, Map<String, Object> context) {
public EvaluationResponse(boolean decision) {
this(decision, null);
}
}
@JsonInclude(JsonInclude.Include.NON_NULL)
public record EvaluationItem(
Subject subject,
Resource resource,
Action action,
Map<String, Object> context) {}
public enum EvaluationsSemantic {
@JsonProperty("execute_all")
EXECUTE_ALL,
@JsonProperty("deny_on_first_deny")
DENY_ON_FIRST_DENY,
@JsonProperty("permit_on_first_permit")
PERMIT_ON_FIRST_PERMIT;
}
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public record Options(EvaluationsSemantic evaluationsSemantic) {}
@JsonInclude(JsonInclude.Include.NON_NULL)
public record EvaluationsRequest(
Subject subject,
Resource resource,
Action action,
Map<String, Object> context,
Options options,
List<EvaluationItem> evaluations) {}
@JsonInclude(JsonInclude.Include.NON_NULL)
public record EvaluationsResponse(List<EvaluationResponse> evaluations) {}
}

View file

@ -16,6 +16,7 @@
*/
package org.keycloak.authorization.authzen;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
@ -56,7 +57,7 @@ import org.keycloak.util.JsonSerialization;
public class AuthZenResource {
private static final Response DECISION_FALSE = Response.ok(new AuthZen.EvaluationResponse(false)).build();
private static final AuthZen.EvaluationResponse DECISION_FALSE = new AuthZen.EvaluationResponse(false);
private static final Pattern UUID_PATTERN = Pattern.compile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$");
private static final String NAMESPACE_ID = "id:";
@ -78,6 +79,64 @@ public class AuthZenResource {
if (token == null) {
throw new NotAuthorizedException("Bearer");
}
return Response.ok(evaluateSingle(request, token)).build();
}
@POST
@Path(AuthZen.EVALUATIONS_PATH)
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response evaluations(AuthZen.EvaluationsRequest request) {
AccessToken token = Tokens.getAccessToken(session);
if (token == null) {
throw new NotAuthorizedException("Bearer");
}
if (request.evaluations() == null || request.evaluations().isEmpty()) {
// AuthZen 1.0 Section 7.1
// "If an evaluations array is NOT present or is empty, the Access Evaluations Request behaves in a
// backwards-compatible manner with the (single) Access Evaluation API Request (Section 6.1)."
AuthZen.EvaluationRequest single = new AuthZen.EvaluationRequest(
request.subject(), request.resource(), request.action(), request.context());
return Response.ok(evaluateSingle(single, token)).build();
}
AuthZen.EvaluationsSemantic semantic = AuthZen.EvaluationsSemantic.EXECUTE_ALL;
if (request.options() != null && request.options().evaluationsSemantic() != null) {
semantic = request.options().evaluationsSemantic();
}
List<AuthZen.EvaluationResponse> results = new ArrayList<>(request.evaluations().size());
for (AuthZen.EvaluationItem item : request.evaluations()) {
AuthZen.EvaluationRequest merged = mergeDefaults(request, item);
AuthZen.EvaluationResponse itemResponse = evaluateSingle(merged, token);
if (semantic == AuthZen.EvaluationsSemantic.DENY_ON_FIRST_DENY && !itemResponse.decision()) {
results.add(new AuthZen.EvaluationResponse(false, Map.of("reason", "deny_on_first_deny")));
break;
}
results.add(itemResponse);
if (semantic == AuthZen.EvaluationsSemantic.PERMIT_ON_FIRST_PERMIT && itemResponse.decision()) {
break;
}
}
return Response.ok(new AuthZen.EvaluationsResponse(results)).build();
}
private static AuthZen.EvaluationRequest mergeDefaults(AuthZen.EvaluationsRequest defaults, AuthZen.EvaluationItem item) {
AuthZen.Subject subject = item.subject() != null ? item.subject() : defaults.subject();
AuthZen.Resource resource = item.resource() != null ? item.resource() : defaults.resource();
AuthZen.Action action = item.action() != null ? item.action() : defaults.action();
Map<String, Object> context = item.context() != null ? item.context() : defaults.context();
return new AuthZen.EvaluationRequest(subject, resource, action, context);
}
private AuthZen.EvaluationResponse evaluateSingle(AuthZen.EvaluationRequest request, AccessToken token) {
if (request.subject() == null || request.resource() == null || request.action() == null) {
return new AuthZen.EvaluationResponse(false);
}
RealmModel realm = session.getContext().getRealm();
AuthorizationProvider authorization = session.getProvider(AuthorizationProvider.class);
@ -143,8 +202,7 @@ public class AuthZenResource {
.from(List.of(permission), context)
.evaluate(resourceServer, null);
boolean decision = !granted.isEmpty();
return Response.ok(new AuthZen.EvaluationResponse(decision)).build();
return new AuthZen.EvaluationResponse(!granted.isEmpty());
}
private Identity resolveSubjectIdentity(RealmModel realm, AuthZen.EvaluationRequest request) {

View file

@ -39,7 +39,8 @@ public class AuthZenWellKnownProvider implements WellKnownProvider {
return Map.of(
"policy_decision_point", realmUri,
"access_evaluation_endpoint", accessEvaluationEndpoint(realmUri)
"access_evaluation_endpoint", accessEvaluationEndpoint(realmUri),
"access_evaluations_endpoint", accessEvaluationsEndpoint(realmUri)
);
}
@ -47,6 +48,10 @@ public class AuthZenWellKnownProvider implements WellKnownProvider {
return String.format("%s/%s/%s", realmUri, AuthZenRealmResourceProviderFactory.PROVIDER_ID, AuthZen.EVALUATION_PATH);
}
public static String accessEvaluationsEndpoint(String realmUri) {
return String.format("%s/%s/%s", realmUri, AuthZenRealmResourceProviderFactory.PROVIDER_ID, AuthZen.EVALUATIONS_PATH);
}
@Override
public void close() {
}

View file

@ -17,7 +17,9 @@
package org.keycloak.testframework.authzen.client;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import jakarta.ws.rs.core.HttpHeaders;
@ -68,7 +70,9 @@ public class AuthZenClient {
}
try (SimpleHttpResponse response = req(postReq)) {
int status = response.getStatus();
AuthZen.EvaluationResponse body = response.asJson(AuthZen.EvaluationResponse.class);
AuthZen.EvaluationResponse body = status == 200
? response.asJson(AuthZen.EvaluationResponse.class)
: null;
Map<String, String> responseHeaders = new HashMap<>();
for (Header h : response.getAllHeaders()) {
responseHeaders.putIfAbsent(h.getName(), h.getValue());
@ -77,6 +81,38 @@ public class AuthZenClient {
}
}
public EvaluationsResult evaluations(AuthZen.EvaluationsRequest request) throws IOException {
return evaluations((Object) request, null);
}
public EvaluationsResult evaluations(AuthZen.EvaluationsRequest request, Map<String, String> headers) throws IOException {
return evaluations((Object) request, headers);
}
public EvaluationsResult evaluations(JsonNode request) throws IOException {
return evaluations((Object) request, null);
}
private EvaluationsResult evaluations(Object req, Map<String, String> headers) throws IOException {
String url = realmUrl + "/authzen/access/v1/evaluations";
SimpleHttpRequest postReq = simpleHttp.doPost(url).json(req);
if (headers != null) {
headers.forEach(postReq::header);
}
try (SimpleHttpResponse response = req(postReq)) {
int status = response.getStatus();
AuthZen.EvaluationsResponse body = status == 200
? response.asJson(AuthZen.EvaluationsResponse.class)
: null;
Map<String, String> responseHeaders = new HashMap<>();
for (Header h : response.getAllHeaders()) {
responseHeaders.putIfAbsent(h.getName(), h.getValue());
}
return new EvaluationsResult(status, body, responseHeaders);
}
}
public WellKnownResponse fetchWellKnownConfiguration() throws IOException {
String url = realmUrl + "/.well-known/authzen-configuration";
try (SimpleHttpResponse rsp = req(simpleHttp.doGet(url))) {
@ -124,10 +160,29 @@ public class AuthZenClient {
}
}
public record EvaluationsResult(int statusCode, AuthZen.EvaluationsResponse response, Map<String, String> headers) {
public List<AuthZen.EvaluationResponse> evaluations() {
return response.evaluations();
}
public String header(String name) {
return headers != null ? headers.get(name) : null;
}
}
public static EvaluationRequestBuilder evaluationRequest() {
return new EvaluationRequestBuilder();
}
public static EvaluationsRequestBuilder evaluationsRequest() {
return new EvaluationsRequestBuilder();
}
public static EvaluationItemBuilder evaluationItem() {
return new EvaluationItemBuilder();
}
public static final class EvaluationRequestBuilder {
private AuthZen.SubjectType subjectType;
private String subjectId;
@ -189,4 +244,114 @@ public class AuthZenClient {
return new AuthZen.EvaluationRequest(subject, resource, action, contextProperties);
}
}
public static final class EvaluationItemBuilder {
private AuthZen.SubjectType subjectType;
private String subjectId;
private boolean subjectSet;
private String resourceType;
private String resourceId;
private Map<String, Object> resourceProperties;
private boolean resourceSet;
private AuthZen.Action action;
private Map<String, Object> contextProperties;
public EvaluationItemBuilder subject(AuthZen.SubjectType type, String id) {
this.subjectType = type;
this.subjectId = id;
this.subjectSet = true;
return this;
}
public EvaluationItemBuilder resource(String type, String id) {
this.resourceType = type;
this.resourceId = id;
this.resourceSet = true;
return this;
}
public EvaluationItemBuilder resourceProperty(String key, Object value) {
if (resourceProperties == null) {
resourceProperties = new HashMap<>();
}
resourceProperties.put(key, value);
return this;
}
public EvaluationItemBuilder action(String name) {
this.action = new AuthZen.Action(name);
return this;
}
public EvaluationItemBuilder contextProperty(String key, Object value) {
if (contextProperties == null) {
contextProperties = new HashMap<>();
}
contextProperties.put(key, value);
return this;
}
public AuthZen.EvaluationItem build() {
AuthZen.Subject subject = subjectSet ? new AuthZen.Subject(subjectType, subjectId, null) : null;
AuthZen.Resource resource = resourceSet ? new AuthZen.Resource(resourceType, resourceId, resourceProperties) : null;
return new AuthZen.EvaluationItem(subject, resource, action, contextProperties);
}
}
public static final class EvaluationsRequestBuilder {
private AuthZen.SubjectType subjectType;
private String subjectId;
private boolean subjectSet;
private String resourceType;
private String resourceId;
private boolean resourceSet;
private AuthZen.Action action;
private Map<String, Object> contextProperties;
private AuthZen.EvaluationsSemantic evaluationsSemantic;
private final List<AuthZen.EvaluationItem> evaluations = new ArrayList<>();
public EvaluationsRequestBuilder subject(AuthZen.SubjectType type, String id) {
this.subjectType = type;
this.subjectId = id;
this.subjectSet = true;
return this;
}
public EvaluationsRequestBuilder resource(String type, String id) {
this.resourceType = type;
this.resourceId = id;
this.resourceSet = true;
return this;
}
public EvaluationsRequestBuilder action(String name) {
this.action = new AuthZen.Action(name);
return this;
}
public EvaluationsRequestBuilder contextProperty(String key, Object value) {
if (contextProperties == null) {
contextProperties = new HashMap<>();
}
contextProperties.put(key, value);
return this;
}
public EvaluationsRequestBuilder evaluationsSemantic(AuthZen.EvaluationsSemantic semantic) {
this.evaluationsSemantic = semantic;
return this;
}
public EvaluationsRequestBuilder addEvaluation(AuthZen.EvaluationItem evaluation) {
evaluations.add(evaluation);
return this;
}
public AuthZen.EvaluationsRequest build() {
AuthZen.Subject subject = subjectSet ? new AuthZen.Subject(subjectType, subjectId, null) : null;
AuthZen.Resource resource = resourceSet ? new AuthZen.Resource(resourceType, resourceId, null) : null;
AuthZen.Options options = evaluationsSemantic != null ? new AuthZen.Options(evaluationsSemantic) : null;
return new AuthZen.EvaluationsRequest(subject, resource, action, contextProperties, options, evaluations);
}
}
}

View file

@ -23,15 +23,16 @@ import java.util.Map;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import org.keycloak.authorization.authzen.AuthZen;
import org.keycloak.testframework.annotations.InjectRealm;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.authzen.client.AuthZenClient;
import org.keycloak.testframework.authzen.client.AuthZenClient.EvaluationResult;
import org.keycloak.testframework.authzen.client.AuthZenClient.EvaluationsResult;
import org.keycloak.testframework.authzen.client.annotations.InjectAuthZenClient;
import org.keycloak.testframework.oauth.OAuthClient;
import org.keycloak.testframework.oauth.annotations.InjectOAuthClient;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
import org.keycloak.util.JsonSerialization;
import com.fasterxml.jackson.databind.JsonNode;
@ -42,10 +43,10 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* Runs the AuthZen interop evaluation test suite defined in the OpenID AuthZen interop project.
* Test scenarios are loaded from the decisions-authorization-api-1_0-01.json resource file.
* Each entry in the JSON defines an AuthZen evaluation request and its expected decision.
* Test scenarios are loaded from the decisions-authorization-api-1_0-02.json resource file.
* The JSON file defines AuthZen evaluation and evaluations requests, as well as the expected decision.
* <p>
* All required Realm configuration including users, roles, client, and authorization settings is loaded from interop-realm.json.
* All required Realm configuration including users, roles, client, and authorization settings are loaded from interop-realm.json.
*/
@KeycloakIntegrationTest(config = AuthZenServerConfig.class)
public class AuthZenEvaluationInteropTest {
@ -73,44 +74,83 @@ public class AuthZenEvaluationInteropTest {
@TestFactory
Stream<DynamicTest> interopEvaluationTests() throws IOException {
List<InteropDecision> decisions = loadDecisions();
JsonNode root = loadDecisions();
AuthZenClient.Authenticated client = authenticatedClient();
AccessTokenResponse tokenResponse = oauth
.client(PDP_CLIENT_ID, PDP_CLIENT_SECRET)
.doClientCredentialsGrantAccessTokenRequest();
AuthZenClient.Authenticated client = authZenClient.withAccessToken(tokenResponse.getAccessToken());
return decisions.stream().map(decision -> {
String testName = buildTestName(decision);
return DynamicTest.dynamicTest(testName, () -> {
EvaluationResult result = client.evaluate(decision.request());
assertEquals(200, result.statusCode(), "Expected 200 OK for: " + testName);
assertEquals(decision.expected(), result.decision(), "Expected decision=" + decision.expected() + " for: " + testName);
});
});
return StreamSupport.stream(root.path("evaluation").spliterator(), false)
.map(node -> {
JsonNode request = node.get("request");
boolean expected = node.get("expected").asBoolean();
String testName = buildEvaluationTestName(request, expected);
return DynamicTest.dynamicTest(testName, () -> {
EvaluationResult result = client.evaluate(request);
assertEquals(200, result.statusCode(), "Expected 200 OK for: " + testName);
assertEquals(expected, result.decision(), "Expected decision=" + expected + " for: " + testName);
});
});
}
private static String buildTestName(InteropDecision decision) {
JsonNode req = decision.request();
String subjectId = req.path("subject").path("id").asText();
@TestFactory
Stream<DynamicTest> interopEvaluationsTests() throws IOException {
JsonNode root = loadDecisions();
JsonNode evaluationsNode = root.path("evaluations");
if (evaluationsNode.isMissingNode() || !evaluationsNode.isArray() || evaluationsNode.isEmpty()) {
return Stream.empty();
}
AuthZenClient.Authenticated client = authenticatedClient();
return StreamSupport.stream(evaluationsNode.spliterator(), false)
.map(node -> {
JsonNode request = node.get("request");
JsonNode expectedArray = node.get("expected");
String testName = buildEvaluationsTestName(request);
return DynamicTest.dynamicTest(testName, () -> {
EvaluationsResult result = client.evaluations(request);
assertEquals(200, result.statusCode(), "Expected 200 OK for: " + testName);
List<AuthZen.EvaluationResponse> actualEvaluations = result.evaluations();
assertEquals(expectedArray.size(), actualEvaluations.size(), "Evaluations count mismatch for: " + testName);
for (int i = 0; i < expectedArray.size(); i++) {
boolean expectedDecision = expectedArray.get(i).get("decision").asBoolean();
assertEquals(expectedDecision, actualEvaluations.get(i).decision(),
"Decision mismatch at index " + i + " for: " + testName);
}
});
});
}
private AuthZenClient.Authenticated authenticatedClient() {
return authZenClient.withAccessToken(
oauth.client(PDP_CLIENT_ID, PDP_CLIENT_SECRET)
.doClientCredentialsGrantAccessTokenRequest()
.getAccessToken()
);
}
private static String buildEvaluationTestName(JsonNode json, boolean expected) {
String subjectId = json.path("subject").path("id").asText();
String userName = USER_NAMES.getOrDefault(subjectId, subjectId);
String action = req.path("action").path("name").asText();
String resourceType = req.path("resource").path("type").asText();
String resourceId = req.path("resource").path("id").asText();
String expected = decision.expected() ? "ALLOW" : "DENY";
return String.format("%s | %s %s:%s => %s", userName, action, resourceType, resourceId, expected);
String action = json.path("action").path("name").asText();
String resourceType = json.path("resource").path("type").asText();
String resourceId = json.path("resource").path("id").asText();
String expectedStr = expected ? "ALLOW" : "DENY";
return String.format("%s | %s %s:%s => %s", userName, action, resourceType, resourceId, expectedStr);
}
private static List<InteropDecision> loadDecisions() throws IOException {
private static String buildEvaluationsTestName(JsonNode json) {
String subjectId = json.path("subject").path("id").asText();
String userName = USER_NAMES.getOrDefault(subjectId, subjectId);
String action = json.path("action").path("name").asText();
int count = json.path("evaluations").size();
return String.format("batch | %s | %s x%d", userName, action, count);
}
private static JsonNode loadDecisions() throws IOException {
try (InputStream is = AuthZenEvaluationInteropTest.class.getResourceAsStream(
"decisions-authorization-api-1_0-01.json")) {
JsonNode root = JsonSerialization.mapper.readTree(is);
return StreamSupport.stream(root.path("evaluation").spliterator(), false)
.map(node -> new InteropDecision(node.get("request"), node.get("expected").asBoolean()))
.toList();
"decisions-authorization-api-1_0-02.json")) {
return JsonSerialization.mapper.readTree(is);
}
}
public record InteropDecision(JsonNode request, boolean expected) {
}
}

View file

@ -629,7 +629,6 @@ public class AuthZenEvaluationTest {
);
assertEquals(401, result.statusCode());
assertFalse(result.decision());
assertEquals(requestId, result.header(X_REQUEST_ID));
}

View file

@ -0,0 +1,506 @@
/*
* Copyright 2025 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.tests.authzen;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
import jakarta.ws.rs.core.Response;
import org.keycloak.admin.client.resource.AuthorizationResource;
import org.keycloak.authorization.authzen.AuthZen;
import org.keycloak.http.simple.SimpleHttp;
import org.keycloak.http.simple.SimpleHttpResponse;
import org.keycloak.representations.idm.authorization.PolicyRepresentation;
import org.keycloak.representations.idm.authorization.ResourcePermissionRepresentation;
import org.keycloak.representations.idm.authorization.ResourceRepresentation;
import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
import org.keycloak.representations.idm.authorization.ScopePermissionRepresentation;
import org.keycloak.representations.idm.authorization.ScopeRepresentation;
import org.keycloak.testframework.annotations.InjectClient;
import org.keycloak.testframework.annotations.InjectKeycloakUrls;
import org.keycloak.testframework.annotations.InjectRealm;
import org.keycloak.testframework.annotations.InjectSimpleHttp;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.annotations.TestSetup;
import org.keycloak.testframework.authzen.client.AuthZenClient;
import org.keycloak.testframework.authzen.client.AuthZenClient.EvaluationsResult;
import org.keycloak.testframework.authzen.client.annotations.InjectAuthZenClient;
import org.keycloak.testframework.oauth.OAuthClient;
import org.keycloak.testframework.oauth.annotations.InjectOAuthClient;
import org.keycloak.testframework.realm.ClientBuilder;
import org.keycloak.testframework.realm.ClientConfig;
import org.keycloak.testframework.realm.ManagedClient;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.testframework.realm.RealmBuilder;
import org.keycloak.testframework.realm.RealmConfig;
import org.keycloak.testframework.realm.UserBuilder;
import org.keycloak.testframework.server.KeycloakUrls;
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
import com.fasterxml.jackson.databind.JsonNode;
import org.apache.http.entity.StringEntity;
import org.junit.jupiter.api.Test;
import static org.keycloak.authorization.authzen.AuthZen.SubjectType.USER;
import static org.keycloak.authorization.authzen.AuthZenRequestIdFilter.X_REQUEST_ID;
import static org.keycloak.authorization.authzen.AuthZenWellKnownProvider.accessEvaluationsEndpoint;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Tests for the AuthZen Evaluations (batch) endpoint.
*/
@KeycloakIntegrationTest(config = AuthZenServerConfig.class)
public class AuthZenEvaluationsTest {
private static final String ADMIN_USER = "admin-user";
private static final String REGULAR_USER = "regular-user";
@InjectRealm(config = TestRealmConfig.class)
ManagedRealm realm;
@InjectClient(ref = "authzen-client", config = AuthzClientConfig.class)
ManagedClient client;
@InjectOAuthClient
OAuthClient oauth;
@InjectAuthZenClient
AuthZenClient authZenClient;
@InjectSimpleHttp
SimpleHttp simpleHttp;
@InjectKeycloakUrls
KeycloakUrls keycloakUrls;
@TestSetup
public void setup() {
AuthorizationResource authz = client.admin().authorization();
String adminRoleId = realm.admin().roles().get("admin").toRepresentation().getId();
createScope(authz, "read");
createScope(authz, "write");
String adminPolicyId = createRolePolicy(authz, "Require Admin Role", adminRoleId);
String alwaysGrantId = createAlwaysGrantPolicy(authz);
createResource(authz, "/admin", "endpoint", "read");
createResourcePermission(authz, "Admin Resource Permission", "/admin", adminPolicyId);
createResource(authz, "/users", "endpoint", "read");
createResourcePermission(authz, "Users Resource Permission", "/users", alwaysGrantId);
createResource(authz, "/scope-limited", "endpoint", "read", "write");
createScopePermission(authz, "Scope Limited Read Permission", "/scope-limited", "read", "Always Grant");
}
@Test
public void testEvaluationsBatchAllGranted() throws IOException {
EvaluationsResult result = authzenClient("admin-user", "password")
.evaluations(AuthZenClient.evaluationsRequest()
.subject(USER, ADMIN_USER)
.action("read")
.addEvaluation(AuthZenClient.evaluationItem()
.resource("endpoint", "/admin")
.build())
.addEvaluation(AuthZenClient.evaluationItem()
.resource("endpoint", "/users")
.build())
.build());
assertEquals(200, result.statusCode());
assertEquals(2, result.evaluations().size());
assertTrue(result.evaluations().get(0).decision());
assertTrue(result.evaluations().get(1).decision());
}
@Test
public void testEvaluationsBatchMixedDecisions() throws IOException {
EvaluationsResult result = authzenClient("regular-user", "password")
.evaluations(AuthZenClient.evaluationsRequest()
.subject(USER, REGULAR_USER)
.action("read")
.addEvaluation(AuthZenClient.evaluationItem()
.resource("endpoint", "/admin")
.build())
.addEvaluation(AuthZenClient.evaluationItem()
.resource("endpoint", "/users")
.build())
.build());
assertEquals(200, result.statusCode());
assertEquals(2, result.evaluations().size());
assertFalse(result.evaluations().get(0).decision());
assertTrue(result.evaluations().get(1).decision());
}
@Test
public void testEvaluationsItemOverridesDefaults() throws IOException {
EvaluationsResult result = authzenClient("admin-user", "password")
.evaluations(AuthZenClient.evaluationsRequest()
.subject(USER, ADMIN_USER)
.action("read")
.resource("endpoint", "/admin")
.addEvaluation(AuthZenClient.evaluationItem()
.build())
.addEvaluation(AuthZenClient.evaluationItem()
.action("write")
.resource("endpoint", "/scope-limited")
.build())
.build());
assertEquals(200, result.statusCode());
assertEquals(2, result.evaluations().size());
assertTrue(result.evaluations().get(0).decision());
assertFalse(result.evaluations().get(1).decision());
}
@Test
public void testEvaluationsUnauthenticatedReturnsUnauthorized() throws IOException {
EvaluationsResult result = authZenClient.evaluations(AuthZenClient.evaluationsRequest()
.subject(USER, ADMIN_USER)
.action("read")
.addEvaluation(AuthZenClient.evaluationItem()
.resource("endpoint", "/admin")
.build())
.build());
assertEquals(401, result.statusCode());
}
@Test
public void testEvaluationsEmptyArrayFallsBackToSingleEvaluation() throws IOException {
String json = """
{"subject":{"type":"user","id":"%s"},\
"resource":{"type":"endpoint","id":"/admin"},\
"action":{"name":"read"},\
"evaluations":[]}""".formatted(ADMIN_USER);
JsonNode body = postEvaluations("admin-user", "password", json);
assertTrue(body.get("decision").asBoolean());
assertNull(body.get("evaluations"));
}
@Test
public void testEvaluationsAbsentFallsBackToSingleEvaluation() throws IOException {
String json = """
{"subject":{"type":"user","id":"%s"},\
"resource":{"type":"endpoint","id":"/admin"},\
"action":{"name":"read"}}""".formatted(ADMIN_USER);
JsonNode body = postEvaluations(ADMIN_USER, "password", json);
assertTrue(body.get("decision").asBoolean());
assertNull(body.get("evaluations"));
}
private JsonNode postEvaluations(String username, String password, String json) throws IOException {
String url = accessEvaluationsEndpoint(keycloakUrls.getBase() + "/realms/" + realm.getName());
AccessTokenResponse tokenResponse = oauth
.client(client.getClientId(), client.getSecret())
.doPasswordGrantRequest(username, password);
try (SimpleHttpResponse response = simpleHttp.doPost(url)
.auth(tokenResponse.getAccessToken())
.header("Content-Type", "application/json")
.entity(new StringEntity(json))
.asResponse()) {
assertEquals(200, response.getStatus());
return response.asJson();
}
}
@Test
public void testExecuteAllSemantic() throws IOException {
EvaluationsResult result = authzenClient("regular-user", "password")
.evaluations(AuthZenClient.evaluationsRequest()
.subject(USER, REGULAR_USER)
.action("read")
.evaluationsSemantic(AuthZen.EvaluationsSemantic.EXECUTE_ALL)
.addEvaluation(AuthZenClient.evaluationItem()
.resource("endpoint", "/admin")
.build())
.addEvaluation(AuthZenClient.evaluationItem()
.resource("endpoint", "/users")
.build())
.build());
assertEquals(200, result.statusCode());
assertEquals(2, result.evaluations().size());
assertFalse(result.evaluations().get(0).decision());
assertTrue(result.evaluations().get(1).decision());
}
@Test
public void testDenyOnFirstDenyStopsOnDenial() throws IOException {
EvaluationsResult result = authzenClient("regular-user", "password")
.evaluations(AuthZenClient.evaluationsRequest()
.subject(USER, REGULAR_USER)
.action("read")
.evaluationsSemantic(AuthZen.EvaluationsSemantic.DENY_ON_FIRST_DENY)
.addEvaluation(AuthZenClient.evaluationItem()
.resource("endpoint", "/admin")
.build())
.addEvaluation(AuthZenClient.evaluationItem()
.resource("endpoint", "/users")
.build())
.build());
assertEquals(200, result.statusCode());
assertEquals(1, result.evaluations().size());
assertFalse(result.evaluations().get(0).decision());
}
@Test
public void testDenyOnFirstDenyReturnsAllWhenAllPermitted() throws IOException {
EvaluationsResult result = authzenClient("admin-user", "password")
.evaluations(AuthZenClient.evaluationsRequest()
.subject(USER, ADMIN_USER)
.action("read")
.evaluationsSemantic(AuthZen.EvaluationsSemantic.DENY_ON_FIRST_DENY)
.addEvaluation(AuthZenClient.evaluationItem()
.resource("endpoint", "/admin")
.build())
.addEvaluation(AuthZenClient.evaluationItem()
.resource("endpoint", "/users")
.build())
.build());
assertEquals(200, result.statusCode());
assertEquals(2, result.evaluations().size());
assertTrue(result.evaluations().get(0).decision());
assertTrue(result.evaluations().get(1).decision());
}
@Test
public void testPermitOnFirstPermitStopsOnPermit() throws IOException {
EvaluationsResult result = authzenClient("regular-user", "password")
.evaluations(AuthZenClient.evaluationsRequest()
.subject(USER, REGULAR_USER)
.action("read")
.evaluationsSemantic(AuthZen.EvaluationsSemantic.PERMIT_ON_FIRST_PERMIT)
.addEvaluation(AuthZenClient.evaluationItem()
.resource("endpoint", "/admin")
.build())
.addEvaluation(AuthZenClient.evaluationItem()
.resource("endpoint", "/users")
.build())
.build());
assertEquals(200, result.statusCode());
assertEquals(2, result.evaluations().size());
assertFalse(result.evaluations().get(0).decision());
assertTrue(result.evaluations().get(1).decision());
}
@Test
public void testPermitOnFirstPermitStopsImmediatelyOnFirstPermit() throws IOException {
EvaluationsResult result = authzenClient("admin-user", "password")
.evaluations(AuthZenClient.evaluationsRequest()
.subject(USER, ADMIN_USER)
.action("read")
.evaluationsSemantic(AuthZen.EvaluationsSemantic.PERMIT_ON_FIRST_PERMIT)
.addEvaluation(AuthZenClient.evaluationItem()
.resource("endpoint", "/admin")
.build())
.addEvaluation(AuthZenClient.evaluationItem()
.resource("endpoint", "/users")
.build())
.build());
assertEquals(200, result.statusCode());
assertEquals(1, result.evaluations().size());
assertTrue(result.evaluations().get(0).decision());
}
@Test
public void testXRequestIdEchoedInResponse() throws IOException {
String requestId = "test-request-id-12345";
EvaluationsResult result = authzenClient("admin-user", "password")
.evaluations(AuthZenClient.evaluationsRequest()
.subject(USER, ADMIN_USER)
.action("read")
.addEvaluation(AuthZenClient.evaluationItem()
.resource("endpoint", "/admin")
.build())
.build(),
Map.of(X_REQUEST_ID, requestId));
assertEquals(200, result.statusCode());
assertTrue(result.evaluations().get(0).decision());
assertEquals(requestId, result.header(X_REQUEST_ID));
}
@Test
public void testXRequestIdEchoedOnUnauthorizedResponse() throws IOException {
String requestId = "unauth-request-id";
EvaluationsResult result = new AuthZenClient(simpleHttp,
keycloakUrls.getBase() + "/realms/" + realm.getName())
.evaluations(AuthZenClient.evaluationsRequest()
.subject(USER, ADMIN_USER)
.action("read")
.addEvaluation(AuthZenClient.evaluationItem()
.resource("endpoint", "/admin")
.build())
.build(),
Map.of(X_REQUEST_ID, requestId));
assertEquals(401, result.statusCode());
assertEquals(requestId, result.header(X_REQUEST_ID));
}
@Test
public void testXRequestIdEchoedOnBadRequestResponse() throws IOException {
String requestId = "bad-request-id";
String url = accessEvaluationsEndpoint(keycloakUrls.getBase() + "/realms/" + realm.getName());
AccessTokenResponse tokenResponse = oauth
.client(client.getClientId(), client.getSecret())
.doPasswordGrantRequest("admin-user", "password");
try (SimpleHttpResponse response = simpleHttp.doPost(url)
.auth(tokenResponse.getAccessToken())
.header("Content-Type", "application/json")
.header(X_REQUEST_ID, requestId)
.entity(new StringEntity("{invalid json"))
.asResponse()) {
assertEquals(400, response.getStatus());
assertEquals(requestId, response.getFirstHeader(X_REQUEST_ID));
}
}
@Test
public void testDefaultSemanticIsExecuteAll() throws IOException {
EvaluationsResult result = authzenClient("regular-user", "password")
.evaluations(AuthZenClient.evaluationsRequest()
.subject(USER, REGULAR_USER)
.action("read")
.addEvaluation(AuthZenClient.evaluationItem()
.resource("endpoint", "/admin")
.build())
.addEvaluation(AuthZenClient.evaluationItem()
.resource("endpoint", "/users")
.build())
.build());
assertEquals(200, result.statusCode());
assertEquals(2, result.evaluations().size());
}
private AuthZenClient.Authenticated authzenClient(String username, String password) {
AccessTokenResponse tokenResponse = oauth
.client(client.getClientId(), client.getSecret())
.doPasswordGrantRequest(username, password);
return authZenClient.withAccessToken(tokenResponse.getAccessToken());
}
private static void createScope(AuthorizationResource authz, String name) {
try (Response response = authz.scopes().create(new ScopeRepresentation(name))) {
assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus());
}
}
private static void createResource(AuthorizationResource authz, String name, String type, String... scopes) {
ResourceRepresentation resource = new ResourceRepresentation();
resource.setName(name);
resource.setType(type);
resource.addScope(scopes);
try (Response response = authz.resources().create(resource)) {
assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus());
}
}
private static String createRolePolicy(AuthorizationResource authz, String name, String roleId) {
RolePolicyRepresentation policy = new RolePolicyRepresentation();
policy.setName(name);
policy.addRole(roleId);
try (Response response = authz.policies().role().create(policy)) {
assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus());
}
return authz.policies().role().findByName(name).getId();
}
private static String createAlwaysGrantPolicy(AuthorizationResource authz) {
PolicyRepresentation policy = new PolicyRepresentation();
policy.setName("Always Grant");
policy.setType("always-grant");
try (Response response = authz.policies().create(policy)) {
assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus());
}
return authz.policies().findByName("Always Grant").getId();
}
private static void createResourcePermission(AuthorizationResource authz, String name,
String resourceName, String policyId) {
ResourcePermissionRepresentation permission = ResourcePermissionRepresentation.create()
.name(name)
.resources(Set.of(authz.resources().findByName(resourceName).get(0).getId()))
.policies(Set.of(policyId))
.build();
try (Response response = authz.permissions().resource().create(permission)) {
assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus());
}
}
private static void createScopePermission(AuthorizationResource authz, String name,
String resourceName, String scopeName, String policyName) {
ScopePermissionRepresentation permission = new ScopePermissionRepresentation();
permission.setName(name);
permission.setResources(Set.of(authz.resources().findByName(resourceName).get(0).getId()));
permission.setScopes(Set.of(authz.scopes().findByName(scopeName).getId()));
permission.addPolicy(policyName);
try (Response response = authz.permissions().scope().create(permission)) {
assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus());
}
}
public static class TestRealmConfig implements RealmConfig {
@Override
public RealmBuilder configure(RealmBuilder realm) {
return realm.realmRoles("admin")
.users(
UserBuilder.create("admin-user")
.name("Admin", "User")
.email("admin@localhost")
.password("password")
.realmRoles("admin"),
UserBuilder.create("regular-user")
.name("Regular", "User")
.email("regular@localhost")
.password("password")
);
}
}
public static class AuthzClientConfig implements ClientConfig {
@Override
public ClientBuilder configure(ClientBuilder client) {
return client
.secret("secret")
.directAccessGrantsEnabled(true)
.authorizationServicesEnabled(true);
}
}
}

View file

@ -70,6 +70,7 @@ public class AuthZenWellKnownTest {
assertNotNull(response);
assertEquals(expectedRealmUrl, response.policyDecisionPoint());
assertEquals(expectedRealmUrl + "/authzen/access/v1/evaluation", response.accessEvaluationEndpoint());
assertEquals(expectedRealmUrl + "/authzen/access/v1/evaluations", response.accessEvaluationsEndpoint());
}
private String realmUrl() {

View file

@ -157,7 +157,7 @@
]
},
{
"name": "240d0db-8ff0-41ec-98b2-34a096273b95",
"name": "7240d0db-8ff0-41ec-98b2-34a096273b95",
"type": "todo",
"scopes": [
{ "name": "can_read_todos" },
@ -254,7 +254,7 @@
"logic": "POSITIVE",
"decisionStrategy": "UNANIMOUS",
"config": {
"resources": "[\"todo-1\",\"7240d0db-8ff0-41ec-98b2-34a096273b92\",\"7240d0db-8ff0-41ec-98b2-34a096273b91\",\"7240d0db-8ff0-41ec-98b2-34a096273b93\",\"7240d0db-8ff0-41ec-98b2-34a096273b94\",\"240d0db-8ff0-41ec-98b2-34a096273b95\"]",
"resources": "[\"todo-1\",\"7240d0db-8ff0-41ec-98b2-34a096273b92\",\"7240d0db-8ff0-41ec-98b2-34a096273b91\",\"7240d0db-8ff0-41ec-98b2-34a096273b93\",\"7240d0db-8ff0-41ec-98b2-34a096273b94\",\"7240d0db-8ff0-41ec-98b2-34a096273b95\"]",
"scopes": "[\"can_read_todos\"]",
"applyPolicies": "[\"any-role-policy\"]"
}
@ -265,7 +265,7 @@
"logic": "POSITIVE",
"decisionStrategy": "UNANIMOUS",
"config": {
"resources": "[\"todo-1\",\"7240d0db-8ff0-41ec-98b2-34a096273b92\",\"7240d0db-8ff0-41ec-98b2-34a096273b91\",\"7240d0db-8ff0-41ec-98b2-34a096273b93\",\"7240d0db-8ff0-41ec-98b2-34a096273b94\",\"240d0db-8ff0-41ec-98b2-34a096273b95\"]",
"resources": "[\"todo-1\",\"7240d0db-8ff0-41ec-98b2-34a096273b92\",\"7240d0db-8ff0-41ec-98b2-34a096273b91\",\"7240d0db-8ff0-41ec-98b2-34a096273b93\",\"7240d0db-8ff0-41ec-98b2-34a096273b94\",\"7240d0db-8ff0-41ec-98b2-34a096273b95\"]",
"scopes": "[\"can_create_todo\"]",
"applyPolicies": "[\"admin-or-editor-policy\"]"
}
@ -276,7 +276,7 @@
"logic": "POSITIVE",
"decisionStrategy": "UNANIMOUS",
"config": {
"resources": "[\"todo-1\",\"7240d0db-8ff0-41ec-98b2-34a096273b92\",\"7240d0db-8ff0-41ec-98b2-34a096273b91\",\"7240d0db-8ff0-41ec-98b2-34a096273b93\",\"7240d0db-8ff0-41ec-98b2-34a096273b94\",\"240d0db-8ff0-41ec-98b2-34a096273b95\"]",
"resources": "[\"todo-1\",\"7240d0db-8ff0-41ec-98b2-34a096273b92\",\"7240d0db-8ff0-41ec-98b2-34a096273b91\",\"7240d0db-8ff0-41ec-98b2-34a096273b93\",\"7240d0db-8ff0-41ec-98b2-34a096273b94\",\"7240d0db-8ff0-41ec-98b2-34a096273b95\"]",
"scopes": "[\"can_update_todo\"]",
"applyPolicies": "[\"admin-or-editor-owner-policy\"]"
}
@ -287,7 +287,7 @@
"logic": "POSITIVE",
"decisionStrategy": "UNANIMOUS",
"config": {
"resources": "[\"todo-1\",\"7240d0db-8ff0-41ec-98b2-34a096273b92\",\"7240d0db-8ff0-41ec-98b2-34a096273b91\",\"7240d0db-8ff0-41ec-98b2-34a096273b93\",\"7240d0db-8ff0-41ec-98b2-34a096273b94\",\"240d0db-8ff0-41ec-98b2-34a096273b95\"]",
"resources": "[\"todo-1\",\"7240d0db-8ff0-41ec-98b2-34a096273b92\",\"7240d0db-8ff0-41ec-98b2-34a096273b91\",\"7240d0db-8ff0-41ec-98b2-34a096273b93\",\"7240d0db-8ff0-41ec-98b2-34a096273b94\",\"7240d0db-8ff0-41ec-98b2-34a096273b95\"]",
"scopes": "[\"can_delete_todo\"]",
"applyPolicies": "[\"admin-or-editor-owner-policy\"]"
}

View file

@ -4,7 +4,7 @@
"request": {
"subject": {
"type": "user",
"id":"CiRmZDA2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
"id": "CiRmZDA2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
},
"action": {
"name": "can_read_user"
@ -20,7 +20,7 @@
"request": {
"subject": {
"type": "user",
"id":"CiRmZDA2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
"id": "CiRmZDA2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
},
"action": {
"name": "can_read_user"
@ -36,7 +36,7 @@
"request": {
"subject": {
"type": "user",
"id":"CiRmZDA2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
"id": "CiRmZDA2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
},
"action": {
"name": "can_read_todos"
@ -52,7 +52,7 @@
"request": {
"subject": {
"type": "user",
"id":"CiRmZDA2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
"id": "CiRmZDA2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
},
"action": {
"name": "can_create_todo"
@ -68,7 +68,7 @@
"request": {
"subject": {
"type": "user",
"id":"CiRmZDA2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
"id": "CiRmZDA2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
},
"action": {
"name": "can_update_todo"
@ -87,7 +87,7 @@
"request": {
"subject": {
"type": "user",
"id":"CiRmZDA2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
"id": "CiRmZDA2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
},
"action": {
"name": "can_update_todo"
@ -106,7 +106,7 @@
"request": {
"subject": {
"type": "user",
"id":"CiRmZDA2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
"id": "CiRmZDA2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
},
"action": {
"name": "can_delete_todo"
@ -125,7 +125,7 @@
"request": {
"subject": {
"type": "user",
"id":"CiRmZDA2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
"id": "CiRmZDA2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
},
"action": {
"name": "can_delete_todo"
@ -145,7 +145,7 @@
"request": {
"subject": {
"type": "user",
"id":"CiRmZDE2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
"id": "CiRmZDE2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
},
"action": {
"name": "can_read_user"
@ -161,7 +161,7 @@
"request": {
"subject": {
"type": "user",
"id":"CiRmZDE2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
"id": "CiRmZDE2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
},
"action": {
"name": "can_read_user"
@ -177,7 +177,7 @@
"request": {
"subject": {
"type": "user",
"id":"CiRmZDE2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
"id": "CiRmZDE2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
},
"action": {
"name": "can_read_todos"
@ -193,7 +193,7 @@
"request": {
"subject": {
"type": "user",
"id":"CiRmZDE2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
"id": "CiRmZDE2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
},
"action": {
"name": "can_create_todo"
@ -209,7 +209,7 @@
"request": {
"subject": {
"type": "user",
"id":"CiRmZDE2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
"id": "CiRmZDE2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
},
"action": {
"name": "can_update_todo"
@ -228,7 +228,7 @@
"request": {
"subject": {
"type": "user",
"id":"CiRmZDE2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
"id": "CiRmZDE2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
},
"action": {
"name": "can_update_todo"
@ -247,7 +247,7 @@
"request": {
"subject": {
"type": "user",
"id":"CiRmZDE2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
"id": "CiRmZDE2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
},
"action": {
"name": "can_delete_todo"
@ -266,7 +266,7 @@
"request": {
"subject": {
"type": "user",
"id":"CiRmZDE2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
"id": "CiRmZDE2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
},
"action": {
"name": "can_delete_todo"
@ -286,7 +286,7 @@
"request": {
"subject": {
"type": "user",
"id":"CiRmZDI2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
"id": "CiRmZDI2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
},
"action": {
"name": "can_read_user"
@ -302,7 +302,7 @@
"request": {
"subject": {
"type": "user",
"id":"CiRmZDI2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
"id": "CiRmZDI2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
},
"action": {
"name": "can_read_user"
@ -318,7 +318,7 @@
"request": {
"subject": {
"type": "user",
"id":"CiRmZDI2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
"id": "CiRmZDI2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
},
"action": {
"name": "can_read_todos"
@ -334,7 +334,7 @@
"request": {
"subject": {
"type": "user",
"id":"CiRmZDI2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
"id": "CiRmZDI2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
},
"action": {
"name": "can_create_todo"
@ -350,7 +350,7 @@
"request": {
"subject": {
"type": "user",
"id":"CiRmZDI2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
"id": "CiRmZDI2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
},
"action": {
"name": "can_update_todo"
@ -369,7 +369,7 @@
"request": {
"subject": {
"type": "user",
"id":"CiRmZDI2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
"id": "CiRmZDI2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
},
"action": {
"name": "can_update_todo"
@ -388,7 +388,7 @@
"request": {
"subject": {
"type": "user",
"id":"CiRmZDI2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
"id": "CiRmZDI2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
},
"action": {
"name": "can_delete_todo"
@ -407,7 +407,7 @@
"request": {
"subject": {
"type": "user",
"id":"CiRmZDI2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
"id": "CiRmZDI2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
},
"action": {
"name": "can_delete_todo"
@ -426,7 +426,7 @@
"request": {
"subject": {
"type": "user",
"id":"CiRmZDM2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
"id": "CiRmZDM2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
},
"action": {
"name": "can_read_user"
@ -442,7 +442,7 @@
"request": {
"subject": {
"type": "user",
"id":"CiRmZDM2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
"id": "CiRmZDM2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
},
"action": {
"name": "can_read_user"
@ -458,7 +458,7 @@
"request": {
"subject": {
"type": "user",
"id":"CiRmZDM2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
"id": "CiRmZDM2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
},
"action": {
"name": "can_read_todos"
@ -474,7 +474,7 @@
"request": {
"subject": {
"type": "user",
"id":"CiRmZDM2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
"id": "CiRmZDM2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
},
"action": {
"name": "can_create_todo"
@ -490,7 +490,7 @@
"request": {
"subject": {
"type": "user",
"id":"CiRmZDM2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
"id": "CiRmZDM2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
},
"action": {
"name": "can_update_todo"
@ -509,7 +509,7 @@
"request": {
"subject": {
"type": "user",
"id":"CiRmZDM2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
"id": "CiRmZDM2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
},
"action": {
"name": "can_update_todo"
@ -528,7 +528,7 @@
"request": {
"subject": {
"type": "user",
"id":"CiRmZDM2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
"id": "CiRmZDM2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
},
"action": {
"name": "can_delete_todo"
@ -547,7 +547,7 @@
"request": {
"subject": {
"type": "user",
"id":"CiRmZDM2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
"id": "CiRmZDM2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
},
"action": {
"name": "can_delete_todo"
@ -566,7 +566,7 @@
"request": {
"subject": {
"type": "user",
"id":"CiRmZDQ2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
"id": "CiRmZDQ2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
},
"action": {
"name": "can_read_user"
@ -582,7 +582,7 @@
"request": {
"subject": {
"type": "user",
"id":"CiRmZDQ2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
"id": "CiRmZDQ2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
},
"action": {
"name": "can_read_user"
@ -598,7 +598,7 @@
"request": {
"subject": {
"type": "user",
"id":"CiRmZDQ2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
"id": "CiRmZDQ2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
},
"action": {
"name": "can_read_todos"
@ -614,7 +614,7 @@
"request": {
"subject": {
"type": "user",
"id":"CiRmZDQ2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
"id": "CiRmZDQ2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
},
"action": {
"name": "can_create_todo"
@ -630,7 +630,7 @@
"request": {
"subject": {
"type": "user",
"id":"CiRmZDQ2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
"id": "CiRmZDQ2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
},
"action": {
"name": "can_update_todo"
@ -649,14 +649,14 @@
"request": {
"subject": {
"type": "user",
"id":"CiRmZDQ2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
"id": "CiRmZDQ2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
},
"action": {
"name": "can_update_todo"
},
"resource": {
"type": "todo",
"id": "240d0db-8ff0-41ec-98b2-34a096273b95",
"id": "7240d0db-8ff0-41ec-98b2-34a096273b95",
"properties": {
"ownerID": "jerry@the-smiths.com"
}
@ -668,7 +668,7 @@
"request": {
"subject": {
"type": "user",
"id":"CiRmZDQ2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
"id": "CiRmZDQ2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
},
"action": {
"name": "can_delete_todo"
@ -687,14 +687,14 @@
"request": {
"subject": {
"type": "user",
"id":"CiRmZDQ2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
"id": "CiRmZDQ2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
},
"action": {
"name": "can_delete_todo"
},
"resource": {
"type": "todo",
"id": "240d0db-8ff0-41ec-98b2-34a096273b95",
"id": "7240d0db-8ff0-41ec-98b2-34a096273b95",
"properties": {
"ownerID": "jerry@the-smiths.com"
}
@ -702,5 +702,103 @@
},
"expected": false
}
],
"evaluations": [
{
"request": {
"subject": {
"type": "user",
"id": "CiRmZDA2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
},
"action": {
"name": "can_update_todo"
},
"evaluations": [
{
"resource": {
"type": "todo",
"id": "7240d0db-8ff0-41ec-98b2-34a096273b92",
"properties": {
"ownerID": "rick@the-citadel.com"
}
}
},
{
"resource": {
"type": "todo",
"id": "7240d0db-8ff0-41ec-98b2-34a096273b95",
"properties": {
"ownerID": "jerry@the-smiths.com"
}
}
}
]
},
"expected": [ { "decision": true }, { "decision": true } ]
},
{
"request": {
"subject": {
"type": "user",
"id": "CiRmZDE2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
},
"action": {
"name": "can_update_todo"
},
"evaluations": [
{
"resource": {
"type": "todo",
"id": "7240d0db-8ff0-41ec-98b2-34a096273b92",
"properties": {
"ownerID": "rick@the-citadel.com"
}
}
},
{
"resource": {
"type": "todo",
"id": "7240d0db-8ff0-41ec-98b2-34a096273b91",
"properties": {
"ownerID": "morty@the-citadel.com"
}
}
}
]
},
"expected": [ { "decision": false }, { "decision": true } ]
},
{
"request": {
"subject": {
"type": "user",
"id": "CiRmZDQ2MTRkMy1jMzlhLTQ3ODEtYjdiZC04Yjk2ZjVhNTEwMGQSBWxvY2Fs"
},
"action": {
"name": "can_update_todo"
},
"evaluations": [
{
"resource": {
"type": "todo",
"id": "7240d0db-8ff0-41ec-98b2-34a096273b92",
"properties": {
"ownerID": "rick@the-citadel.com"
}
}
},
{
"resource": {
"type": "todo",
"id": "7240d0db-8ff0-41ec-98b2-34a096273b95",
"properties": {
"ownerID": "jerry@the-smiths.com"
}
}
}
]
},
"expected": [ { "decision": false }, { "decision": false } ]
}
]
}