diff --git a/CedarJava/src/main/java/com/cedarpolicy/BasicAuthorizationEngine.java b/CedarJava/src/main/java/com/cedarpolicy/BasicAuthorizationEngine.java index 9dded0b4..360c5e03 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/BasicAuthorizationEngine.java +++ b/CedarJava/src/main/java/com/cedarpolicy/BasicAuthorizationEngine.java @@ -113,7 +113,8 @@ private static final class AuthorizationRequest extends com.cedarpolicy.model.Au request.actionEUID, request.resourceEUID, request.context, - request.schema); + request.schema, + request.enable_request_validation); this.slice = slice; } } diff --git a/CedarJava/src/main/java/com/cedarpolicy/model/AuthorizationRequest.java b/CedarJava/src/main/java/com/cedarpolicy/model/AuthorizationRequest.java index 4b311fd1..3c4ac04b 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/model/AuthorizationRequest.java +++ b/CedarJava/src/main/java/com/cedarpolicy/model/AuthorizationRequest.java @@ -31,15 +31,17 @@ * determines if the policies allow for the given principal to perform the given action against the * given resource. * - *

An optional schema can be provided, but will not be used for validation unless you call - * validate(). The schema is provided to allow parsing Entities from JSON without escape sequences - * (in general, you don't need to worry about this if you construct your entities via the EntityUID - * class). + *

If the (optional) schema is provided, this will inform parsing the + * `context` from JSON: for instance, it will allow `__entity` and `__extn` + * escapes to be implicit, and it will error if attributes have the wrong types + * (e.g., string instead of integer). + * If the schema is provided and `enable_request_validation` is true, then the + * schema will also be used for request validation. */ public class AuthorizationRequest { /** EUID of the principal in the request. */ @JsonProperty("principal") - public final Optional principalEUID; + public final Optional principalEUID; /** EUID of the action in the request. */ @JsonProperty("action") public final EntityUID actionEUID; @@ -50,9 +52,17 @@ public class AuthorizationRequest { /** Key/Value map representing the context of the request. */ public final Optional> context; - /** JSON object representing the Schema. */ + /** JSON object representing the Schema. Used for schema-based parsing of + * `context`, and also (if `enable_request_validation` is `true`) for + * request validation. */ public final Optional schema; + /** If this is `true` and a schema is provided, perform request validation. + * If this is `false`, the schema will only be used for schema-based parsing + * of `context`, and not for request validation. + * If a schema is not provided, this option has no effect. */ + public final boolean enable_request_validation; + /** * Create an authorization request from the EUIDs and Context. * @@ -61,13 +71,17 @@ public class AuthorizationRequest { * @param resourceEUID Resource's EUID. * @param context Key/Value context. * @param schema Schema (optional). + * @param enable_request_validation Whether to use the schema for just + * schema-based parsing of `context` (false) or also for request validation + * (true). No effect if `schema` is not provided. */ public AuthorizationRequest( Optional principalEUID, EntityUID actionEUID, Optional resourceEUID, Optional> context, - Optional schema) { + Optional schema, + boolean enable_request_validation) { this.principalEUID = principalEUID; this.actionEUID = actionEUID; this.resourceEUID = resourceEUID; @@ -77,10 +91,11 @@ public AuthorizationRequest( this.context = Optional.of(new HashMap<>(context.get())); } this.schema = schema; + this.enable_request_validation = enable_request_validation; } /** - * Create a request in the empty context. + * Create a request without a schema. * * @param principalEUID Principal's EUID. * @param actionEUID Action's EUID. @@ -93,11 +108,12 @@ public AuthorizationRequest(EntityUID principalEUID, EntityUID actionEUID, Entit actionEUID, Optional.of(resourceEUID), Optional.of(context), - Optional.empty()); + Optional.empty(), + false); } /** - * Create a request without a schema. + * Create a request without a schema, using Entity objects for principal/action/resource. * * @param principalEUID Principal's EUID. * @param actionEUID Action's EUID. @@ -106,20 +122,32 @@ public AuthorizationRequest(EntityUID principalEUID, EntityUID actionEUID, Entit */ public AuthorizationRequest(Entity principalEUID, Entity actionEUID, Entity resourceEUID, Map context) { this( - Optional.of(principalEUID.getEUID()), + principalEUID.getEUID(), actionEUID.getEUID(), - Optional.of(resourceEUID.getEUID()), - Optional.of(context), - Optional.empty()); + resourceEUID.getEUID(), + context); } - public AuthorizationRequest(Optional principal, Entity action, Optional resource, Optional> context, Optional schema) { + /** + * Create a request from Entity objects and Context. + * + * @param principal + * @param action + * @param resource + * @param context + * @param schema + * @param enable_request_validation Whether to use the schema for just + * schema-based parsing of `context` (false) or also for request validation + * (true). No effect if `schema` is not provided. + */ + public AuthorizationRequest(Optional principal, Entity action, Optional resource, Optional> context, Optional schema, boolean enable_request_validation) { this( principal.map(e -> e.getEUID()), action.getEUID(), resource.map(e -> e.getEUID()), context, - schema + schema, + enable_request_validation ); } diff --git a/CedarJava/src/test/java/com/cedarpolicy/JSONTests.java b/CedarJava/src/test/java/com/cedarpolicy/JSONTests.java index 81cd4882..40500169 100644 --- a/CedarJava/src/test/java/com/cedarpolicy/JSONTests.java +++ b/CedarJava/src/test/java/com/cedarpolicy/JSONTests.java @@ -39,6 +39,7 @@ import com.fasterxml.jackson.core.exc.StreamConstraintsException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.BooleanNode; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; @@ -78,9 +79,9 @@ public void testRequest() { var moria = new EntityUID(EntityTypeName.parse("Mines").get(), "moria"); AuthorizationRequest q = new AuthorizationRequest(gandalf, opens, moria, new HashMap()); ObjectNode n = JsonNodeFactory.instance.objectNode(); - ObjectNode c = JsonNodeFactory.instance.objectNode(); - n.set("context", c); + n.set("context", JsonNodeFactory.instance.objectNode()); n.set("schema", JsonNodeFactory.instance.nullNode()); + n.set("enable_request_validation", JsonNodeFactory.instance.booleanNode(false)); n.set("principal", buildEuidObject("Wizard", "gandalf")); n.set("action", buildEuidObject("Action", "opens")); n.set("resource", buildEuidObject("Mines", "moria")); diff --git a/CedarJava/src/test/java/com/cedarpolicy/SharedIntegrationTests.java b/CedarJava/src/test/java/com/cedarpolicy/SharedIntegrationTests.java index 87d3ded1..f8470ce1 100644 --- a/CedarJava/src/test/java/com/cedarpolicy/SharedIntegrationTests.java +++ b/CedarJava/src/test/java/com/cedarpolicy/SharedIntegrationTests.java @@ -143,6 +143,9 @@ private static class JsonRequest { /** Context map used for the request. */ public Map context; + /** Whether to enable request validation for this request. Default true */ + public boolean enable_request_validation = true; + /** The expected decision that should be returned by the authorization engine. */ public AuthorizationResponse.Decision decision; @@ -294,7 +297,7 @@ private Set loadPolicies(String policiesFile) throws IOException { String policiesSrc = String.join("\n", Files.readAllLines(resolveIntegrationTestPath(policiesFile))); // Get a list of the policy sources for the individual policies in the - // file by splitting the full policy source on semicolons. This will + // file by splitting the full policy source on semicolons. This will // break if a semicolon shows up in a string, eid, or comment. String[] policyStrings = policiesSrc.split(";"); // Some of the corpus tests contain semicolons in strings and/or eids. @@ -305,7 +308,7 @@ private Set loadPolicies(String policiesFile) throws IOException { policyStrings = null; } } - + Set policies = new HashSet<>(); if (policyStrings == null) { // This case will only be reached for corpus tests. @@ -408,10 +411,11 @@ private void executeJsonRequestTest( request.principal == null ? Optional.empty() : Optional.of(EntityUID.parseFromJson(request.principal).get()), EntityUID.parseFromJson(request.action).get(), request.resource == null ? Optional.empty() : Optional.of(EntityUID.parseFromJson(request.resource).get()), - Optional.of(request.context), - Optional.of(schema)); + Optional.of(request.context), + Optional.of(schema), + request.enable_request_validation); Slice slice = new BasicSlice(policies, entities); - + try { AuthorizationResponse response = auth.isAuthorized(authRequest, slice); System.out.println(response.getErrors()); diff --git a/CedarJava/src/test/java/com/cedarpolicy/pbt/IntegrationTests.java b/CedarJava/src/test/java/com/cedarpolicy/pbt/IntegrationTests.java index 1f68038d..52e02c29 100644 --- a/CedarJava/src/test/java/com/cedarpolicy/pbt/IntegrationTests.java +++ b/CedarJava/src/test/java/com/cedarpolicy/pbt/IntegrationTests.java @@ -52,7 +52,7 @@ /** Integration tests. */ public class IntegrationTests { - + final EntityTypeName principalType; final EntityTypeName actionType; final EntityTypeName resourceType; @@ -368,11 +368,10 @@ public void testUnspecifiedResource() { Map currentContext = new HashMap<>(); AuthorizationRequest request = new AuthorizationRequest( - Optional.of(principal), + principal, action, - Optional.of(resource), - Optional.of(currentContext), - Optional.empty()); + resource, + currentContext); AuthorizationEngine authEngine = new BasicAuthorizationEngine(); AuthorizationResponse response = Assertions.assertDoesNotThrow(() -> authEngine.isAuthorized(request, slice)); @@ -599,7 +598,7 @@ public void testSchemaParsingDeny() { Set policies = new HashSet<>(); policies.add(policy); - // Schema says resource.owner is a bool, so we should get a parse failure, which causes + // Schema says resource.owner is a bool, so we should get a parse failure, which causes // `isAuthorized()` to throw a `BadRequestException`. Slice slice = new BasicSlice(policies, entities); Map currentContext = new HashMap<>(); @@ -609,7 +608,8 @@ public void testSchemaParsingDeny() { action, Optional.of(resource), Optional.of(currentContext), - Optional.of(loadSchemaResource("/schema_parsing_deny_schema.json"))); + Optional.of(loadSchemaResource("/schema_parsing_deny_schema.json")), + true); AuthorizationEngine authEngine = new BasicAuthorizationEngine(); Assertions.assertThrows(BadRequestException.class, () -> authEngine.isAuthorized(request, slice)); } @@ -668,7 +668,8 @@ public void testSchemaParsingAllow() { action, Optional.of(resource), Optional.of(currentContext), - Optional.of(loadSchemaResource("/schema_parsing_allow_schema.json"))); + Optional.of(loadSchemaResource("/schema_parsing_allow_schema.json")), + true); AuthorizationEngine authEngine = new BasicAuthorizationEngine(); AuthorizationResponse response = Assertions.assertDoesNotThrow(() -> authEngine.isAuthorized(request, slice));