8000 AuthZen Evaluations API by ryanemerson · Pull Request #47838 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
}
Expand Down Expand Up @@ -80,6 +83,43 @@ public record EvaluationRequest(
@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) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -56,7 +57,7 @@

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:";
Expand All @@ -78,6 +79,64 @@ public Response evaluate(AuthZen.EvaluationRequest request) {
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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, it should be possible to move the evaluation semantic logic to the policy engine. It would be a nice capability to add to authorization services.


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);
Expand Down Expand Up @@ -143,8 +202,7 @@ public Response evaluate(AuthZen.EvaluationRequest request) {
.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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,19 @@ public Object getConfig() {

return Map.of(
"policy_decision_point", realmUri,
"access_evaluation_endpoint", accessEvaluationEndpoint(realmUri)
"access_evaluation_endpoint", accessEvaluationEndpoint(realmUri),
"access_evaluations_endpoint", accessEvaluationsEndpoint(realmUri)
);
}

public static String accessEvaluationEndpoint(String realmUri) {
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() {
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -68,7 +70,9 @@ private EvaluationResult evaluate(Object req, Map<String, String> headers) throw
}
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());
Expand All @@ -77,6 +81,38 @@ private EvaluationResult evaluate(Object req, Map<String, String> headers) throw
}
}

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))) {
Expand Down Expand Up @@ -124,10 +160,29 @@ public String header(String name) {
}
}

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;
Expand Down Expand Up @@ -189,4 +244,114 @@ public AuthZen.EvaluationRequest build() {
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) {
resourc 103F0 eProperties = 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);
}
}
}
Loading
Loading
0