From aa783474f3e5f3a0ad00c3697cc511e38da875da Mon Sep 17 00:00:00 2001 From: Mudit Chaudhary Date: Sun, 9 Nov 2025 09:59:50 -0500 Subject: [PATCH 1/2] adds unrefined enum tests Signed-off-by: Mudit Chaudhary --- .../test/java/com/cedarpolicy/AuthTests.java | 433 ++++++++++++++++++ .../cedarpolicy/EntityValidationTests.java | 166 +++++++ .../java/com/cedarpolicy/SchemaTests.java | 118 +++++ .../java/com/cedarpolicy/ValidationTests.java | 153 +++++++ .../src/test/resources/enum_entities.json | 34 ++ .../src/test/resources/enum_policies.cedar | 49 ++ .../test/resources/enum_schema.cedarschema | 21 + CedarJava/src/test/resources/enum_schema.json | 61 +++ .../test/resources/invalid_enum_entities.json | 23 + 9 files changed, 1058 insertions(+) create mode 100644 CedarJava/src/test/resources/enum_entities.json create mode 100644 CedarJava/src/test/resources/enum_policies.cedar create mode 100644 CedarJava/src/test/resources/enum_schema.cedarschema create mode 100644 CedarJava/src/test/resources/enum_schema.json create mode 100644 CedarJava/src/test/resources/invalid_enum_entities.json diff --git a/CedarJava/src/test/java/com/cedarpolicy/AuthTests.java b/CedarJava/src/test/java/com/cedarpolicy/AuthTests.java index 8da4afe3..6b2791ee 100644 --- a/CedarJava/src/test/java/com/cedarpolicy/AuthTests.java +++ b/CedarJava/src/test/java/com/cedarpolicy/AuthTests.java @@ -18,8 +18,12 @@ import java.util.HashMap; import java.util.HashSet; +import java.util.Optional; + +import static com.cedarpolicy.TestUtil.loadSchemaResource; import com.cedarpolicy.model.AuthorizationRequest; +import com.cedarpolicy.model.AuthorizationResponse; import com.cedarpolicy.model.PartialAuthorizationRequest; import com.cedarpolicy.model.PartialAuthorizationResponse; import com.cedarpolicy.model.AuthorizationResponse.SuccessOrFailure; @@ -29,11 +33,13 @@ import com.cedarpolicy.model.policy.Policy; import com.cedarpolicy.model.Context; import com.cedarpolicy.model.policy.PolicySet; +import com.cedarpolicy.model.schema.Schema; import com.cedarpolicy.value.EntityTypeName; import com.cedarpolicy.value.EntityUID; import com.cedarpolicy.value.Unknown; import com.cedarpolicy.value.Value; import com.cedarpolicy.value.PrimBool; +import com.cedarpolicy.value.PrimString; import com.cedarpolicy.model.entity.Entities; import org.junit.jupiter.api.Test; @@ -63,6 +69,18 @@ private void assertAllowed(AuthorizationRequest q, PolicySet policySet, Set entities) { + assertDoesNotThrow(() -> { + final AuthorizationResponse response = new BasicAuthorizationEngine().isAuthorized(request, policySet, entities); + if (response.success.isPresent()) { + fail(String.format("Expected a failure, but got this success: %s", response.toString())); + } else { + final var errors = assertDoesNotThrow(() -> response.errors.get()); + assertTrue(errors.size() > 0); + } + }); + } + @Test public void simple() { var alice = new EntityUID(EntityTypeName.parse("User").get(), "alice"); @@ -271,6 +289,421 @@ public void residualWithUnknownValue() { }); } + /** + * Build entities for enum authorization tests. + */ + private Set buildEntitiesForEnumTests() { + EntityTypeName userType = EntityTypeName.parse("User").get(); + EntityTypeName taskType = EntityTypeName.parse("Task").get(); + EntityTypeName colorType = EntityTypeName.parse("Color").get(); + EntityTypeName actionType = EntityTypeName.parse("Action").get(); + EntityTypeName applicationType = EntityTypeName.parse("Application").get(); + + Set entities = new HashSet<>(); + + // Users + Entity alice = new Entity(userType.of("alice"), new HashMap<>() {{ + put("name", new PrimString("Alice")); + }}, new HashSet<>()); + + Entity bob = new Entity(userType.of("bob"), new HashMap<>() {{ + put("name", new PrimString("Bob")); + }}, new HashSet<>()); + + // Tasks + Entity task1 = new Entity(taskType.of("task1"), new HashMap<>() {{ + put("owner", alice.getEUID()); + put("name", new PrimString("Complete project")); + put("status", new EntityUID(colorType, "Red")); + }}, new HashSet<>()); + + Entity task2 = new Entity(taskType.of("task2"), new HashMap<>() {{ + put("owner", bob.getEUID()); + put("name", new PrimString("Review code")); + put("status", new EntityUID(colorType, "Green")); + }}, new HashSet<>()); + + // Actions + Entity updateTaskAction = new Entity(actionType.of("UpdateTask"), new HashMap<>(), new HashSet<>()); + Entity createListAction = new Entity(actionType.of("CreateList"), new HashMap<>(), new HashSet<>()); + + // Application enum entity + Entity tinyTodoApp = new Entity(applicationType.of("TinyTodo"), new HashMap<>(), new HashSet<>()); + + entities.add(alice); + entities.add(bob); + entities.add(task1); + entities.add(task2); + entities.add(updateTaskAction); + entities.add(createListAction); + entities.add(tinyTodoApp); + + return entities; + } + + /** + * Test RFC example policy with enum entities - should be allowed. + */ + @Test + public void enumAuthorizationRFCExampleAllow() { + EntityUID principal = new EntityUID(EntityTypeName.parse("User").get(), "alice"); + EntityUID action = new EntityUID(EntityTypeName.parse("Action").get(), "UpdateTask"); + EntityUID resource = new EntityUID(EntityTypeName.parse("Task").get(), "task2"); // task2 has Green status + + Set entities = buildEntitiesForEnumTests(); + + // RFC policy: allow if principal owns task and status is not Red + var policies = new HashSet(); + policies.add(new Policy(""" + permit( + principal, + action == Action::"UpdateTask", + resource + ) when { + principal == resource.owner && + resource.status != Color::"Red" + }; + """, "rfcPolicy")); + + var policySet = new PolicySet(policies); + AuthorizationRequest request = new AuthorizationRequest(principal, action, resource, new HashMap<>()); + + // Should be denied because alice doesn't own task2 (bob does) + var auth = new BasicAuthorizationEngine(); + assertDoesNotThrow(() -> { + final var response = auth.isAuthorized(request, policySet, entities); + assertEquals(response.type, SuccessOrFailure.Success); + final var success = response.success.get(); + assertFalse(success.isAllowed()); // Should be denied due to ownership + }); + } + + /** + * Test RFC example policy with enum entities - should be denied due to Red status. + */ + @Test + public void enumAuthorizationRFCExampleDenyByStatus() { + EntityUID principal = new EntityUID(EntityTypeName.parse("User").get(), "alice"); + EntityUID action = new EntityUID(EntityTypeName.parse("Action").get(), "UpdateTask"); + EntityUID resource = new EntityUID(EntityTypeName.parse("Task").get(), "task1"); // task1 has Red status + + Set entities = buildEntitiesForEnumTests(); + + // RFC policy: allow if principal owns task and status is not Red + var policies = new HashSet(); + policies.add(new Policy(""" + permit( + principal, + action == Action::"UpdateTask", + resource + ) when { + principal == resource.owner && + resource.status != Color::"Red" + }; + """, "rfcPolicy")); + + var policySet = new PolicySet(policies); + AuthorizationRequest request = new AuthorizationRequest(principal, action, resource, new HashMap<>()); + + // Should be denied because task1 has Red status (even though alice owns it) + var auth = new BasicAuthorizationEngine(); + assertDoesNotThrow(() -> { + final var response = auth.isAuthorized(request, policySet, entities); + assertEquals(response.type, SuccessOrFailure.Success); + final var success = response.success.get(); + assertFalse(success.isAllowed()); // Should be denied due to Red status + }); + } + + /** + * Test enum equality in authorization policy. + */ + @Test + public void enumAuthorizationEqualityComparison() { + EntityUID principal = new EntityUID(EntityTypeName.parse("User").get(), "bob"); + EntityUID action = new EntityUID(EntityTypeName.parse("Action").get(), "UpdateTask"); + EntityUID resource = new EntityUID(EntityTypeName.parse("Task").get(), "task2"); // task2 has Green status + + Set entities = buildEntitiesForEnumTests(); + + // Policy: allow if status is exactly Green + var policies = new HashSet(); + policies.add(new Policy(""" + permit( + principal == User::"bob", + action == Action::"UpdateTask", + resource + ) when { + resource.status == Color::"Green" + }; + """, "greenPolicy")); + + var policySet = new PolicySet(policies); + AuthorizationRequest request = new AuthorizationRequest(principal, action, resource, new HashMap<>()); + + assertAllowed(request, policySet, entities); + } + + /** + * Test Application enum authorization from RFC example. + */ + @Test + public void enumAuthorizationApplicationEnum() { + EntityUID principal = new EntityUID(EntityTypeName.parse("User").get(), "alice"); + EntityUID action = new EntityUID(EntityTypeName.parse("Action").get(), "CreateList"); + EntityUID resource = new EntityUID(EntityTypeName.parse("Application").get(), "TinyTodo"); + + Set entities = buildEntitiesForEnumTests(); + + // Policy from RFC: allow CreateList on TinyTodo application + var policies = new HashSet(); + policies.add(new Policy(""" + permit( + principal, + action == Action::"CreateList", + resource == Application::"TinyTodo" + ); + """, "appPolicy")); + + var policySet = new PolicySet(policies); + AuthorizationRequest request = new AuthorizationRequest(principal, action, resource, new HashMap<>()); + + assertAllowed(request, policySet, entities); + } + + /** + * Test multiple enum values in OR condition. + */ + @Test + public void enumAuthorizationMultipleValues() { + EntityUID principal = new EntityUID(EntityTypeName.parse("User").get(), "bob"); + EntityUID action = new EntityUID(EntityTypeName.parse("Action").get(), "UpdateTask"); + EntityUID resource = new EntityUID(EntityTypeName.parse("Task").get(), "task2"); // Green status + + Set entities = buildEntitiesForEnumTests(); + + // Policy: allow if status is Blue OR Green + var policies = new HashSet(); + policies.add(new Policy(""" + permit( + principal, + action == Action::"UpdateTask", + resource + ) when { + resource.status == Color::"Blue" || + resource.status == Color::"Green" + }; + """, "multiEnumPolicy")); + + var policySet = new PolicySet(policies); + AuthorizationRequest request = new AuthorizationRequest(principal, action, resource, new HashMap<>()); + + assertAllowed(request, policySet, entities); + } + + /** + * Test forbid policy with enum entities. + */ + @Test + public void enumAuthorizationForbidPolicy() { + EntityUID principal = new EntityUID(EntityTypeName.parse("User").get(), "bob"); + EntityUID action = new EntityUID(EntityTypeName.parse("Action").get(), "UpdateTask"); + EntityUID resource = new EntityUID(EntityTypeName.parse("Task").get(), "task1"); // Red status, alice owner + + Set entities = buildEntitiesForEnumTests(); + + var policies = new HashSet(); + // Permit policy + policies.add(new Policy(""" + permit( + principal, + action == Action::"UpdateTask", + resource + ); + """, "permitAll")); + + // Forbid policy from RFC: forbid if status is Red and principal is not owner + policies.add(new Policy(""" + forbid( + principal, + action == Action::"UpdateTask", + resource + ) when { + resource.status == Color::"Red" && + principal != resource.owner + }; + """, "forbidRedNonOwner")); + + var policySet = new PolicySet(policies); + AuthorizationRequest request = new AuthorizationRequest(principal, action, resource, new HashMap<>()); + + // Should be denied because bob is not owner of task1 and it has Red status + var auth = new BasicAuthorizationEngine(); + assertDoesNotThrow(() -> { + final var response = auth.isAuthorized(request, policySet, entities); + assertEquals(response.type, SuccessOrFailure.Success); + final var success = response.success.get(); + assertFalse(success.isAllowed()); + }); + } + + /** + * Test authorization with schema validation - positive case with valid enum entities. + */ + @Test + public void enumAuthorizationWithSchemaValidationPositive() { + EntityTypeName userType = EntityTypeName.parse("User").get(); + EntityTypeName actionType = EntityTypeName.parse("Action").get(); + EntityTypeName applicationType = EntityTypeName.parse("Application").get(); + + // Create valid entities + Entity principalEntity = new Entity(userType.of("alice"), new HashMap<>() {{ + put("name", new PrimString("Alice")); + }}, new HashSet<>()); + + Entity actionEntity = new Entity(actionType.of("CreateList"), new HashMap<>(), new HashSet<>()); + Entity resourceEntity = new Entity(applicationType.of("TinyTodo"), new HashMap<>(), new HashSet<>()); + + Set entities = new HashSet<>(); + entities.add(principalEntity); + entities.add(actionEntity); + entities.add(resourceEntity); + + var policies = new HashSet(); + policies.add(new Policy(""" + permit( + principal, + action == Action::"CreateList", + resource == Application::"TinyTodo" + ); + """, "validEnumPolicy")); + + var policySet = new PolicySet(policies); + + // Create authorization request with schema validation enabled + AuthorizationRequest request = new AuthorizationRequest( + principalEntity, + actionEntity, + resourceEntity, + Optional.of(new HashMap<>()), + Optional.of(ENUM_SCHEMA), + true // Enable request validation + ); + + var auth = new BasicAuthorizationEngine(); + assertDoesNotThrow(() -> { + final var response = auth.isAuthorized(request, policySet, entities); + assertEquals(response.type, SuccessOrFailure.Success); + final var success = response.success.get(); + assertTrue(success.isAllowed()); + }); + } + + /** + * Test authorization with schema validation - negative case with invalid enum entity. + */ + @Test + public void enumAuthorizationWithSchemaValidationNegative() { + EntityTypeName userType = EntityTypeName.parse("User").get(); + EntityTypeName actionType = EntityTypeName.parse("Action").get(); + EntityTypeName applicationType = EntityTypeName.parse("Application").get(); + + // Create entities with invalid enum value + Entity principalEntity = new Entity(userType.of("alice"), new HashMap<>() {{ + put("name", new PrimString("Alice")); + }}, new HashSet<>()); + + Entity actionEntity = new Entity(actionType.of("CreateList"), new HashMap<>(), new HashSet<>()); + // Use INVALID enum value - "InvalidApp" is not in the Application enum + Entity resourceEntity = new Entity(applicationType.of("InvalidApp"), new HashMap<>(), new HashSet<>()); + + Set entities = new HashSet<>(); + entities.add(principalEntity); + entities.add(actionEntity); + entities.add(resourceEntity); + + var policies = new HashSet(); + policies.add(new Policy(""" + permit( + principal, + action == Action::"CreateList", + resource == Application::"InvalidApp" + ); + """, "invalidEnumPolicy")); + + var policySet = new PolicySet(policies); + + // Create authorization request with schema validation enabled + AuthorizationRequest request = new AuthorizationRequest( + principalEntity, + actionEntity, + resourceEntity, + Optional.of(new HashMap<>()), + Optional.of(ENUM_SCHEMA), + true // Enable request validation - this should catch the invalid enum + ); + + // Should return failure response due to invalid enum value when schema validation is enabled + assertFailure(request, policySet, entities); + } + + /** + * Test authorization with invalid enum in task status attribute. + */ + @Test + public void enumAuthorizationWithInvalidTaskStatus() { + EntityTypeName userType = EntityTypeName.parse("User").get(); + EntityTypeName actionType = EntityTypeName.parse("Action").get(); + EntityTypeName taskType = EntityTypeName.parse("Task").get(); + EntityTypeName colorType = EntityTypeName.parse("Color").get(); + + Entity principalEntity = new Entity(userType.of("alice"), new HashMap<>() {{ + put("name", new PrimString("Alice")); + }}, new HashSet<>()); + + Entity actionEntity = new Entity(actionType.of("UpdateTask"), new HashMap<>(), new HashSet<>()); + + // Create task with invalid enum status value "Purple" (not in Color enum) + Entity taskEntity = new Entity(taskType.of("task1"), new HashMap<>() {{ + put("owner", principalEntity.getEUID()); + put("name", new PrimString("Test task")); + put("status", new EntityUID(colorType, "Purple")); // Invalid enum value + }}, new HashSet<>()); + + Set entities = new HashSet<>(); + entities.add(principalEntity); + entities.add(actionEntity); + entities.add(taskEntity); + + var policies = new HashSet(); + policies.add(new Policy(""" + permit( + principal, + action == Action::"UpdateTask", + resource + ) when { + resource.status != Color::"Red" + }; + """, "taskPolicy")); + + var policySet = new PolicySet(policies); + + // Create authorization request with schema validation enabled + AuthorizationRequest request = new AuthorizationRequest( + principalEntity, + actionEntity, + taskEntity, + Optional.of(new HashMap<>()), + Optional.of(ENUM_SCHEMA), + true // Enable request validation + ); + + // Should return failure response due to invalid enum value in task status + assertFailure(request, policySet, entities); + } + + private static final Schema ENUM_SCHEMA = loadSchemaResource("/enum_schema.json"); + private void assumePartialEvaluation(Executable executable) { try { executable.execute(); diff --git a/CedarJava/src/test/java/com/cedarpolicy/EntityValidationTests.java b/CedarJava/src/test/java/com/cedarpolicy/EntityValidationTests.java index b5a9a356..a19446c7 100644 --- a/CedarJava/src/test/java/com/cedarpolicy/EntityValidationTests.java +++ b/CedarJava/src/test/java/com/cedarpolicy/EntityValidationTests.java @@ -35,9 +35,13 @@ import com.cedarpolicy.model.schema.Schema; import com.cedarpolicy.pbt.EntityGen; import com.cedarpolicy.value.EntityTypeName; +import com.cedarpolicy.value.EntityUID; import com.cedarpolicy.value.PrimBool; import com.cedarpolicy.value.PrimString; +import java.util.HashMap; +import java.util.HashSet; + /** * Tests for entity validator */ @@ -193,6 +197,166 @@ public void testEntityWithUnknownTagWithCedarSchema() throws AuthException { "Expected to match regex but was: '%s'".formatted(errMsg)); } + /** + * Test that valid enum entities are accepted. + */ + @Test + public void testValidEnumEntities() throws AuthException { + // Create valid entities using enum types + EntityTypeName userType = EntityTypeName.parse("User").get(); + EntityTypeName taskType = EntityTypeName.parse("Task").get(); + EntityTypeName colorType = EntityTypeName.parse("Color").get(); + + Entity user = new Entity(userType.of("alice"), new HashMap<>() {{ + put("name", new PrimString("Alice")); + }}, new HashSet<>()); + + Entity task = new Entity(taskType.of("task1"), new HashMap<>() {{ + put("owner", user.getEUID()); + put("name", new PrimString("Complete project")); + put("status", new EntityUID(colorType, "Red")); + }}, new HashSet<>()); + + EntityValidationRequest request = new EntityValidationRequest(ENUM_SCHEMA, List.of(user, task)); + engine.validateEntities(request); + } + + /** + * Test that enum entities with invalid enum values are rejected. + */ + @Test + public void testEnumEntitiesWithInvalidValues() throws AuthException { + EntityTypeName userType = EntityTypeName.parse("User").get(); + EntityTypeName taskType = EntityTypeName.parse("Task").get(); + EntityTypeName colorType = EntityTypeName.parse("Color").get(); + + Entity user = new Entity(userType.of("alice"), new HashMap<>() {{ + put("name", new PrimString("Alice")); + }}, new HashSet<>()); + + // Create task with invalid enum value "Purple" (not in Color enum) + Entity task = new Entity(taskType.of("task1"), new HashMap<>() {{ + put("owner", user.getEUID()); + put("name", new PrimString("Complete project")); + put("status", new EntityUID(colorType, "Purple")); // Invalid enum value + }}, new HashSet<>()); + + EntityValidationRequest request = new EntityValidationRequest(ENUM_SCHEMA, List.of(user, task)); + + BadRequestException exception = assertThrows(BadRequestException.class, + () -> engine.validateEntities(request)); + + String errMsg = exception.getErrors().get(0); + assertTrue(errMsg.contains("Purple") || errMsg.contains("Color"), + "Expected error about invalid enum value but was: '%s'".formatted(errMsg)); + } + + /** + * Test that enum entities cannot have attributes according to RFC. + */ + @Test + public void testEnumEntitiesCannotHaveAttributes() throws AuthException { + EntityTypeName colorType = EntityTypeName.parse("Color").get(); + + // Try to create enum entity with attributes (should fail) + Entity enumEntity = new Entity(colorType.of("Red"), new HashMap<>() {{ + put("shade", new PrimString("Dark")); // Enum entities shouldn't have attributes + }}, new HashSet<>()); + + EntityValidationRequest request = new EntityValidationRequest(ENUM_SCHEMA, List.of(enumEntity)); + + BadRequestException exception = assertThrows(BadRequestException.class, + () -> engine.validateEntities(request)); + + String errMsg = exception.getErrors().get(0); + assertTrue(errMsg.contains("attribute") && (errMsg.contains("Color") || errMsg.contains("Red")), + "Expected error about enum entity having attributes but was: '%s'".formatted(errMsg)); + } + + /** + * Test that enum entities cannot have parents according to RFC. + */ + @Test + public void testEnumEntitiesCannotHaveParents() throws AuthException { + EntityTypeName colorType = EntityTypeName.parse("Color").get(); + EntityTypeName userType = EntityTypeName.parse("User").get(); + + Entity user = new Entity(userType.of("alice"), new HashMap<>() {{ + put("name", new PrimString("Alice")); + }}, new HashSet<>()); + + // Try to create enum entity with parent (should fail) + Entity enumEntity = new Entity(colorType.of("Red"), new HashMap<>(), new HashSet<>() {{ + add(user.getEUID()); // Enum entities shouldn't have parents + }}); + + EntityValidationRequest request = new EntityValidationRequest(ENUM_SCHEMA, List.of(user, enumEntity)); + + BadRequestException exception = assertThrows(BadRequestException.class, + () -> engine.validateEntities(request)); + + String errMsg = exception.getErrors().get(0); + assertTrue(errMsg.contains("parent") || errMsg.contains("ancestor") || errMsg.contains("Color"), + "Expected error about enum entity having parents but was: '%s'".formatted(errMsg)); + } + + /** + * Test enum entity validation with Cedar schema format. + */ + @Test + public void testEnumEntitiesWithCedarSchema() throws AuthException { + EntityTypeName userType = EntityTypeName.parse("User").get(); + EntityTypeName taskType = EntityTypeName.parse("Task").get(); + EntityTypeName colorType = EntityTypeName.parse("Color").get(); + + Entity user = new Entity(userType.of("bob"), new HashMap<>() {{ + put("name", new PrimString("Bob")); + }}, new HashSet<>()); + + Entity task = new Entity(taskType.of("task2"), new HashMap<>() {{ + put("owner", user.getEUID()); + put("name", new PrimString("Review code")); + put("status", new EntityUID(colorType, "Green")); + }}, new HashSet<>()); + + EntityValidationRequest cedarRequest = new EntityValidationRequest(ENUM_SCHEMA_CEDAR, List.of(user, task)); + engine.validateEntities(cedarRequest); + } + + /** + * Test Application enum validation from RFC example. + */ + @Test + public void testApplicationEnumValidation() throws AuthException { + EntityTypeName applicationType = EntityTypeName.parse("Application").get(); + + // Valid Application enum entity + Entity application = new Entity(applicationType.of("TinyTodo"), new HashMap<>(), new HashSet<>()); + + EntityValidationRequest request = new EntityValidationRequest(ENUM_SCHEMA, List.of(application)); + engine.validateEntities(request); + } + + /** + * Test invalid Application enum value. + */ + @Test + public void testInvalidApplicationEnumValue() throws AuthException { + EntityTypeName applicationType = EntityTypeName.parse("Application").get(); + + // Invalid Application enum entity - using "InvalidApp" which is not in the enum + Entity application = new Entity(applicationType.of("InvalidApp"), new HashMap<>(), new HashSet<>()); + + EntityValidationRequest request = new EntityValidationRequest(ENUM_SCHEMA, List.of(application)); + + BadRequestException exception = assertThrows(BadRequestException.class, + () -> engine.validateEntities(request)); + + String errMsg = exception.getErrors().get(0); + assertTrue(errMsg.contains("InvalidApp") || errMsg.contains("Application"), + "Expected error about invalid Application enum value but was: '%s'".formatted(errMsg)); + } + @BeforeAll public static void setUp() { @@ -204,4 +368,6 @@ public static void setUp() { private static final Schema ROLE_SCHEMA = loadSchemaResource("/role_schema.json"); private static final Schema ROLE_SCHEMA_CEDAR = loadCedarSchemaResource("/role_schema.cedarschema"); + private static final Schema ENUM_SCHEMA = loadSchemaResource("/enum_schema.json"); + private static final Schema ENUM_SCHEMA_CEDAR = loadCedarSchemaResource("/enum_schema.cedarschema"); } diff --git a/CedarJava/src/test/java/com/cedarpolicy/SchemaTests.java b/CedarJava/src/test/java/com/cedarpolicy/SchemaTests.java index 1883e9a2..2279164c 100644 --- a/CedarJava/src/test/java/com/cedarpolicy/SchemaTests.java +++ b/CedarJava/src/test/java/com/cedarpolicy/SchemaTests.java @@ -18,6 +18,8 @@ import java.util.Optional; +import static com.cedarpolicy.TestUtil.loadSchemaResource; +import static com.cedarpolicy.TestUtil.loadCedarSchemaResource; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -235,4 +237,120 @@ void testMalformedSchema() { assertThrows(InternalException.class, malformedSchema::toJsonFormat); } } + + @Nested + @DisplayName("Enum Schema Tests") + class EnumSchemaTests { + + @Test + @DisplayName("Should parse JSON schema with enum entities") + void testParseJsonEnumSchema() { + assertDoesNotThrow(() -> { + Schema enumSchema = loadSchemaResource("/enum_schema.json"); + assertNotNull(enumSchema, "Enum schema should not be null"); + }); + } + + @Test + @DisplayName("Should parse Cedar schema with enum entities") + void testParseCedarEnumSchema() { + assertDoesNotThrow(() -> { + Schema enumSchema = loadCedarSchemaResource("/enum_schema.cedarschema"); + assertNotNull(enumSchema, "Enum schema should not be null"); + }); + } + + @Test + @DisplayName("Should parse inline enum entity definitions") + void testParseInlineEnumDefinitions() { + assertDoesNotThrow(() -> { + // Test Cedar format inline enum + Schema.parse(JsonOrCedar.Cedar, """ + entity Color enum ["Red", "Blue", "Green"]; + entity Application enum ["TinyTodo"]; + """); + + // Test JSON format inline enum + Schema.parse(JsonOrCedar.Json, """ + { + "": { + "entityTypes": { + "Color": { + "enum": ["Red", "Blue", "Green"] + }, + "Application": { + "enum": ["TinyTodo"] + } + }, + "actions": {} + } + } + """); + }); + } + + @Test + @DisplayName("Should reject empty enum definitions") + void testRejectEmptyEnums() { + // Test Cedar format empty enum + assertThrows(Exception.class, () -> { + Schema.parse(JsonOrCedar.Cedar, "entity Color enum [];"); + }); + + // Test JSON format empty enum + assertThrows(Exception.class, () -> { + Schema.parse(JsonOrCedar.Json, """ + { + "": { + "entityTypes": { + "Color": { + "enum": [] + } + }, + "actions": {} + } + } + """); + }); + } + + @Test + @DisplayName("Should convert enum schemas between JSON and Cedar formats") + void testEnumSchemaFormatConversion() throws Exception { + // Test Cedar to JSON conversion + Schema cedarEnumSchema = Schema.parse(JsonOrCedar.Cedar, """ + entity Color enum ["Red", "Blue", "Green"]; + entity User; + action view appliesTo { principal: [User], resource: [User] }; + """); + + JsonNode jsonResult = cedarEnumSchema.toJsonFormat(); + assertNotNull(jsonResult, "JSON conversion result should not be null"); + + // Test JSON to Cedar conversion + String jsonEnumSchema = """ + { + "": { + "entityTypes": { + "Color": { + "enum": ["Red", "Blue", "Green"] + }, + "User": {} + }, + "actions": { + "view": { + "appliesTo": { + "principalTypes": ["User"], + "resourceTypes": ["User"] + } + } + } + } + } + """; + Schema jsonSchemaObj = Schema.parse(JsonOrCedar.Json, jsonEnumSchema); + String cedarResult = jsonSchemaObj.toCedarFormat(); + assertNotNull(cedarResult, "Cedar conversion result should not be null"); + } + } } diff --git a/CedarJava/src/test/java/com/cedarpolicy/ValidationTests.java b/CedarJava/src/test/java/com/cedarpolicy/ValidationTests.java index 2478d400..959bda22 100644 --- a/CedarJava/src/test/java/com/cedarpolicy/ValidationTests.java +++ b/CedarJava/src/test/java/com/cedarpolicy/ValidationTests.java @@ -285,4 +285,157 @@ private void reset() { private static final Schema PHOTOFLASH_SCHEMA = loadSchemaResource("/photoflash_schema.json"); private static final Schema LIBRARY_SCHEMA = loadSchemaResource("/library_schema.json"); private static final Schema LEVEL_SCHEMA = loadSchemaResource("/level_schema.json"); + private static final Schema ENUM_SCHEMA = loadSchemaResource("/enum_schema.json"); + + /** Test enum entity validation with valid enum values. */ + @Test + public void givenEnumSchemaAndValidEnumUsageReturnsValid() { + givenSchema(ENUM_SCHEMA); + givenPolicy( + "policy0", + "permit(" + + " principal == User::\"alice\"," + + " action == Action::\"UpdateTask\"," + + " resource == Task::\"task1\"" + + ") when {" + + " resource.status == Color::\"Red\"" + + "};"); + ValidationResponse response = whenValidated(); + thenIsValid(response); + ValidationResponse levelResponse = whenLevelValidated(1); + thenIsValid(levelResponse); + } + + /** Test enum entity validation with invalid enum values. */ + @Test + public void givenEnumSchemaAndInvalidEnumValueReturnsInvalid() { + givenSchema(ENUM_SCHEMA); + givenPolicy( + "policy0", + "permit(" + + " principal == User::\"alice\"," + + " action == Action::\"UpdateTask\"," + + " resource == Task::\"task1\"" + + ") when {" + + " resource.status != Color::\"Purple\"" // Invalid enum value + + "};"); + ValidationResponse response = whenValidated(); + thenIsNotValid(response); + ValidationResponse levelResponse = whenLevelValidated(1); + thenIsNotValid(levelResponse); + } + + /** Test enum entity validation with case-sensitive enum values. */ + @Test + public void givenEnumSchemaAndWrongCaseEnumValueReturnsInvalid() { + givenSchema(ENUM_SCHEMA); + givenPolicy( + "policy0", + "permit(" + + " principal == User::\"alice\"," + + " action == Action::\"UpdateTask\"," + + " resource == Task::\"task1\"" + + ") when {" + + " resource.status != Color::\"red\"" // Wrong case - should be "Red" + + "};"); + ValidationResponse response = whenValidated(); + thenIsNotValid(response); + ValidationResponse levelResponse = whenLevelValidated(1); + thenIsNotValid(levelResponse); + } + + /** Test RFC example policy with enum entities. */ + @Test + public void givenEnumSchemaAndRFCExamplePolicyReturnsValid() { + givenSchema(ENUM_SCHEMA); + // This is the exact policy from the RFC + givenPolicy( + "policy0", + "permit(" + + " principal," + + " action == Action::\"UpdateTask\"," + + " resource" + + ") when {" + + " principal == resource.owner &&" + + " resource.status != Color::\"Red\"" + + "};"); + ValidationResponse response = whenValidated(); + thenIsValid(response); + ValidationResponse levelResponse = whenLevelValidated(1); + thenIsValid(levelResponse); + } + + /** Test Application enum from RFC example. */ + @Test + public void givenEnumSchemaAndApplicationEnumPolicyReturnsValid() { + givenSchema(ENUM_SCHEMA); + givenPolicy( + "policy0", + "permit(" + + " principal," + + " action == Action::\"CreateList\"," + + " resource == Application::\"TinyTodo\"" + + ");"); + ValidationResponse response = whenValidated(); + thenIsValid(response); + ValidationResponse levelResponse = whenLevelValidated(1); + thenIsValid(levelResponse); + } + + /** Test invalid Application enum usage. */ + @Test + public void givenEnumSchemaAndInvalidApplicationEnumReturnsInvalid() { + givenSchema(ENUM_SCHEMA); + givenPolicy( + "policy0", + "permit(" + + " principal," + + " action == Action::\"CreateList\"," + + " resource == Application::\"TinyTODO\"" // Typo in enum value + + ");"); + ValidationResponse response = whenValidated(); + thenIsNotValid(response); + ValidationResponse levelResponse = whenLevelValidated(1); + thenIsNotValid(levelResponse); + } + + /** Test multiple enum comparisons in policy. */ + @Test + public void givenEnumSchemaAndMultipleEnumComparisonsReturnsValid() { + givenSchema(ENUM_SCHEMA); + givenPolicy( + "policy0", + "permit(" + + " principal," + + " action == Action::\"UpdateTask\"," + + " resource" + + ") when {" + + " resource.status == Color::\"Blue\" ||" + + " resource.status == Color::\"Green\"" + + "};"); + ValidationResponse response = whenValidated(); + thenIsValid(response); + ValidationResponse levelResponse = whenLevelValidated(1); + thenIsValid(levelResponse); + } + + /** Test forbid policy with enum entities. */ + @Test + public void givenEnumSchemaAndForbidPolicyWithEnumsReturnsValid() { + givenSchema(ENUM_SCHEMA); + givenPolicy( + "policy0", + "forbid(" + + " principal," + + " action == Action::\"UpdateTask\"," + + " resource" + + ") when {" + + " resource.status == Color::\"Red\" &&" + + " principal != resource.owner" + + "};"); + ValidationResponse response = whenValidated(); + thenIsValid(response); + ValidationResponse levelResponse = whenLevelValidated(1); + thenIsValid(levelResponse); + } } diff --git a/CedarJava/src/test/resources/enum_entities.json b/CedarJava/src/test/resources/enum_entities.json new file mode 100644 index 00000000..4f504387 --- /dev/null +++ b/CedarJava/src/test/resources/enum_entities.json @@ -0,0 +1,34 @@ +[ + { + "uid": {"type": "User", "id": "alice"}, + "attrs": { + "name": "Alice" + }, + "parents": [] + }, + { + "uid": {"type": "User", "id": "bob"}, + "attrs": { + "name": "Bob" + }, + "parents": [] + }, + { + "uid": {"type": "Task", "id": "task1"}, + "attrs": { + "owner": {"type": "User", "id": "alice"}, + "name": "Complete project", + "status": {"type": "Color", "id": "Red"} + }, + "parents": [] + }, + { + "uid": {"type": "Task", "id": "task2"}, + "attrs": { + "owner": {"type": "User", "id": "bob"}, + "name": "Review code", + "status": {"type": "Color", "id": "Green"} + }, + "parents": [] + } +] diff --git a/CedarJava/src/test/resources/enum_policies.cedar b/CedarJava/src/test/resources/enum_policies.cedar new file mode 100644 index 00000000..fff7ebde --- /dev/null +++ b/CedarJava/src/test/resources/enum_policies.cedar @@ -0,0 +1,49 @@ +// Valid enum policy from RFC example +permit( + principal, + action == Action::"UpdateTask", + resource +) +when { + principal == resource.owner && + resource.status != Color::"Red" +}; + +// Valid enum equality comparison +permit( + principal == User::"alice", + action == Action::"UpdateTask", + resource +) +when { + resource.status == Color::"Green" +}; + +// Valid application enum usage +permit( + principal, + action == Action::"CreateList", + resource == Application::"TinyTodo" +); + +// Policy with multiple enum comparisons +permit( + principal, + action == Action::"UpdateTask", + resource +) +when { + resource.status == Color::"Blue" || + resource.status == Color::"Green" +}; + +// Policy testing enum in conditions +forbid( + principal, + action == Action::"UpdateTask", + resource +) +when { + resource.status == Color::"Red" && + principal != resource.owner +}; diff --git a/CedarJava/src/test/resources/enum_schema.cedarschema b/CedarJava/src/test/resources/enum_schema.cedarschema new file mode 100644 index 00000000..520cde8b --- /dev/null +++ b/CedarJava/src/test/resources/enum_schema.cedarschema @@ -0,0 +1,21 @@ +entity User = { + name: String, +}; + +entity Color enum ["Red", "Blue", "Green"]; + +entity Application enum ["TinyTodo"]; + +entity Status enum ["Active", "Inactive", "Pending"]; + +entity Task = { + owner: User, + name: String, + status: Color, +}; + +action UpdateTask + appliesTo { principal: [User], resource: [Task] }; + +action CreateList + appliesTo { principal: [User], resource: [Application] }; diff --git a/CedarJava/src/test/resources/enum_schema.json b/CedarJava/src/test/resources/enum_schema.json new file mode 100644 index 00000000..0bd5f92e --- /dev/null +++ b/CedarJava/src/test/resources/enum_schema.json @@ -0,0 +1,61 @@ +{ + "": { + "entityTypes": { + "User": { + "shape": { + "type": "Record", + "attributes": { + "name": { + "type": "String", + "required": true + } + } + } + }, + "Color": { + "enum": ["Red", "Blue", "Green"] + }, + "Application": { + "enum": ["TinyTodo"] + }, + "Status": { + "enum": ["Active", "Inactive", "Pending"] + }, + "Task": { + "shape": { + "type": "Record", + "attributes": { + "owner": { + "type": "Entity", + "name": "User", + "required": true + }, + "name": { + "type": "String", + "required": true + }, + "status": { + "type": "Entity", + "name": "Color", + "required": true + } + } + } + } + }, + "actions": { + "UpdateTask": { + "appliesTo": { + "principalTypes": ["User"], + "resourceTypes": ["Task"] + } + }, + "CreateList": { + "appliesTo": { + "principalTypes": ["User"], + "resourceTypes": ["Application"] + } + } + } + } +} diff --git a/CedarJava/src/test/resources/invalid_enum_entities.json b/CedarJava/src/test/resources/invalid_enum_entities.json new file mode 100644 index 00000000..add9da05 --- /dev/null +++ b/CedarJava/src/test/resources/invalid_enum_entities.json @@ -0,0 +1,23 @@ +[ + { + "uid": {"type": "User", "id": "alice"}, + "attrs": { + "name": "Alice" + }, + "parents": [] + }, + { + "uid": {"type": "Color", "id": "Purple"}, + "attrs": {}, + "parents": [] + }, + { + "uid": {"type": "Task", "id": "task1"}, + "attrs": { + "owner": {"type": "User", "id": "alice"}, + "name": "Complete project", + "status": {"type": "Color", "id": "Purple"} + }, + "parents": [] + } +] From fabbdd2b6e31a216f57bf11d83264289dfb99c67 Mon Sep 17 00:00:00 2001 From: Mudit Chaudhary Date: Sun, 9 Nov 2025 11:46:00 -0500 Subject: [PATCH 2/2] refactors enum tests Signed-off-by: Mudit Chaudhary --- .../test/java/com/cedarpolicy/AuthTests.java | 433 ------------------ .../cedarpolicy/EntityValidationTests.java | 133 +++--- .../java/com/cedarpolicy/SchemaTests.java | 88 ++-- .../java/com/cedarpolicy/ValidationTests.java | 171 +------ .../com/cedarpolicy/pbt/IntegrationTests.java | 239 ++++++++++ 5 files changed, 344 insertions(+), 720 deletions(-) diff --git a/CedarJava/src/test/java/com/cedarpolicy/AuthTests.java b/CedarJava/src/test/java/com/cedarpolicy/AuthTests.java index 6b2791ee..8da4afe3 100644 --- a/CedarJava/src/test/java/com/cedarpolicy/AuthTests.java +++ b/CedarJava/src/test/java/com/cedarpolicy/AuthTests.java @@ -18,12 +18,8 @@ import java.util.HashMap; import java.util.HashSet; -import java.util.Optional; - -import static com.cedarpolicy.TestUtil.loadSchemaResource; import com.cedarpolicy.model.AuthorizationRequest; -import com.cedarpolicy.model.AuthorizationResponse; import com.cedarpolicy.model.PartialAuthorizationRequest; import com.cedarpolicy.model.PartialAuthorizationResponse; import com.cedarpolicy.model.AuthorizationResponse.SuccessOrFailure; @@ -33,13 +29,11 @@ import com.cedarpolicy.model.policy.Policy; import com.cedarpolicy.model.Context; import com.cedarpolicy.model.policy.PolicySet; -import com.cedarpolicy.model.schema.Schema; import com.cedarpolicy.value.EntityTypeName; import com.cedarpolicy.value.EntityUID; import com.cedarpolicy.value.Unknown; import com.cedarpolicy.value.Value; import com.cedarpolicy.value.PrimBool; -import com.cedarpolicy.value.PrimString; import com.cedarpolicy.model.entity.Entities; import org.junit.jupiter.api.Test; @@ -69,18 +63,6 @@ private void assertAllowed(AuthorizationRequest q, PolicySet policySet, Set entities) { - assertDoesNotThrow(() -> { - final AuthorizationResponse response = new BasicAuthorizationEngine().isAuthorized(request, policySet, entities); - if (response.success.isPresent()) { - fail(String.format("Expected a failure, but got this success: %s", response.toString())); - } else { - final var errors = assertDoesNotThrow(() -> response.errors.get()); - assertTrue(errors.size() > 0); - } - }); - } - @Test public void simple() { var alice = new EntityUID(EntityTypeName.parse("User").get(), "alice"); @@ -289,421 +271,6 @@ public void residualWithUnknownValue() { }); } - /** - * Build entities for enum authorization tests. - */ - private Set buildEntitiesForEnumTests() { - EntityTypeName userType = EntityTypeName.parse("User").get(); - EntityTypeName taskType = EntityTypeName.parse("Task").get(); - EntityTypeName colorType = EntityTypeName.parse("Color").get(); - EntityTypeName actionType = EntityTypeName.parse("Action").get(); - EntityTypeName applicationType = EntityTypeName.parse("Application").get(); - - Set entities = new HashSet<>(); - - // Users - Entity alice = new Entity(userType.of("alice"), new HashMap<>() {{ - put("name", new PrimString("Alice")); - }}, new HashSet<>()); - - Entity bob = new Entity(userType.of("bob"), new HashMap<>() {{ - put("name", new PrimString("Bob")); - }}, new HashSet<>()); - - // Tasks - Entity task1 = new Entity(taskType.of("task1"), new HashMap<>() {{ - put("owner", alice.getEUID()); - put("name", new PrimString("Complete project")); - put("status", new EntityUID(colorType, "Red")); - }}, new HashSet<>()); - - Entity task2 = new Entity(taskType.of("task2"), new HashMap<>() {{ - put("owner", bob.getEUID()); - put("name", new PrimString("Review code")); - put("status", new EntityUID(colorType, "Green")); - }}, new HashSet<>()); - - // Actions - Entity updateTaskAction = new Entity(actionType.of("UpdateTask"), new HashMap<>(), new HashSet<>()); - Entity createListAction = new Entity(actionType.of("CreateList"), new HashMap<>(), new HashSet<>()); - - // Application enum entity - Entity tinyTodoApp = new Entity(applicationType.of("TinyTodo"), new HashMap<>(), new HashSet<>()); - - entities.add(alice); - entities.add(bob); - entities.add(task1); - entities.add(task2); - entities.add(updateTaskAction); - entities.add(createListAction); - entities.add(tinyTodoApp); - - return entities; - } - - /** - * Test RFC example policy with enum entities - should be allowed. - */ - @Test - public void enumAuthorizationRFCExampleAllow() { - EntityUID principal = new EntityUID(EntityTypeName.parse("User").get(), "alice"); - EntityUID action = new EntityUID(EntityTypeName.parse("Action").get(), "UpdateTask"); - EntityUID resource = new EntityUID(EntityTypeName.parse("Task").get(), "task2"); // task2 has Green status - - Set entities = buildEntitiesForEnumTests(); - - // RFC policy: allow if principal owns task and status is not Red - var policies = new HashSet(); - policies.add(new Policy(""" - permit( - principal, - action == Action::"UpdateTask", - resource - ) when { - principal == resource.owner && - resource.status != Color::"Red" - }; - """, "rfcPolicy")); - - var policySet = new PolicySet(policies); - AuthorizationRequest request = new AuthorizationRequest(principal, action, resource, new HashMap<>()); - - // Should be denied because alice doesn't own task2 (bob does) - var auth = new BasicAuthorizationEngine(); - assertDoesNotThrow(() -> { - final var response = auth.isAuthorized(request, policySet, entities); - assertEquals(response.type, SuccessOrFailure.Success); - final var success = response.success.get(); - assertFalse(success.isAllowed()); // Should be denied due to ownership - }); - } - - /** - * Test RFC example policy with enum entities - should be denied due to Red status. - */ - @Test - public void enumAuthorizationRFCExampleDenyByStatus() { - EntityUID principal = new EntityUID(EntityTypeName.parse("User").get(), "alice"); - EntityUID action = new EntityUID(EntityTypeName.parse("Action").get(), "UpdateTask"); - EntityUID resource = new EntityUID(EntityTypeName.parse("Task").get(), "task1"); // task1 has Red status - - Set entities = buildEntitiesForEnumTests(); - - // RFC policy: allow if principal owns task and status is not Red - var policies = new HashSet(); - policies.add(new Policy(""" - permit( - principal, - action == Action::"UpdateTask", - resource - ) when { - principal == resource.owner && - resource.status != Color::"Red" - }; - """, "rfcPolicy")); - - var policySet = new PolicySet(policies); - AuthorizationRequest request = new AuthorizationRequest(principal, action, resource, new HashMap<>()); - - // Should be denied because task1 has Red status (even though alice owns it) - var auth = new BasicAuthorizationEngine(); - assertDoesNotThrow(() -> { - final var response = auth.isAuthorized(request, policySet, entities); - assertEquals(response.type, SuccessOrFailure.Success); - final var success = response.success.get(); - assertFalse(success.isAllowed()); // Should be denied due to Red status - }); - } - - /** - * Test enum equality in authorization policy. - */ - @Test - public void enumAuthorizationEqualityComparison() { - EntityUID principal = new EntityUID(EntityTypeName.parse("User").get(), "bob"); - EntityUID action = new EntityUID(EntityTypeName.parse("Action").get(), "UpdateTask"); - EntityUID resource = new EntityUID(EntityTypeName.parse("Task").get(), "task2"); // task2 has Green status - - Set entities = buildEntitiesForEnumTests(); - - // Policy: allow if status is exactly Green - var policies = new HashSet(); - policies.add(new Policy(""" - permit( - principal == User::"bob", - action == Action::"UpdateTask", - resource - ) when { - resource.status == Color::"Green" - }; - """, "greenPolicy")); - - var policySet = new PolicySet(policies); - AuthorizationRequest request = new AuthorizationRequest(principal, action, resource, new HashMap<>()); - - assertAllowed(request, policySet, entities); - } - - /** - * Test Application enum authorization from RFC example. - */ - @Test - public void enumAuthorizationApplicationEnum() { - EntityUID principal = new EntityUID(EntityTypeName.parse("User").get(), "alice"); - EntityUID action = new EntityUID(EntityTypeName.parse("Action").get(), "CreateList"); - EntityUID resource = new EntityUID(EntityTypeName.parse("Application").get(), "TinyTodo"); - - Set entities = buildEntitiesForEnumTests(); - - // Policy from RFC: allow CreateList on TinyTodo application - var policies = new HashSet(); - policies.add(new Policy(""" - permit( - principal, - action == Action::"CreateList", - resource == Application::"TinyTodo" - ); - """, "appPolicy")); - - var policySet = new PolicySet(policies); - AuthorizationRequest request = new AuthorizationRequest(principal, action, resource, new HashMap<>()); - - assertAllowed(request, policySet, entities); - } - - /** - * Test multiple enum values in OR condition. - */ - @Test - public void enumAuthorizationMultipleValues() { - EntityUID principal = new EntityUID(EntityTypeName.parse("User").get(), "bob"); - EntityUID action = new EntityUID(EntityTypeName.parse("Action").get(), "UpdateTask"); - EntityUID resource = new EntityUID(EntityTypeName.parse("Task").get(), "task2"); // Green status - - Set entities = buildEntitiesForEnumTests(); - - // Policy: allow if status is Blue OR Green - var policies = new HashSet(); - policies.add(new Policy(""" - permit( - principal, - action == Action::"UpdateTask", - resource - ) when { - resource.status == Color::"Blue" || - resource.status == Color::"Green" - }; - """, "multiEnumPolicy")); - - var policySet = new PolicySet(policies); - AuthorizationRequest request = new AuthorizationRequest(principal, action, resource, new HashMap<>()); - - assertAllowed(request, policySet, entities); - } - - /** - * Test forbid policy with enum entities. - */ - @Test - public void enumAuthorizationForbidPolicy() { - EntityUID principal = new EntityUID(EntityTypeName.parse("User").get(), "bob"); - EntityUID action = new EntityUID(EntityTypeName.parse("Action").get(), "UpdateTask"); - EntityUID resource = new EntityUID(EntityTypeName.parse("Task").get(), "task1"); // Red status, alice owner - - Set entities = buildEntitiesForEnumTests(); - - var policies = new HashSet(); - // Permit policy - policies.add(new Policy(""" - permit( - principal, - action == Action::"UpdateTask", - resource - ); - """, "permitAll")); - - // Forbid policy from RFC: forbid if status is Red and principal is not owner - policies.add(new Policy(""" - forbid( - principal, - action == Action::"UpdateTask", - resource - ) when { - resource.status == Color::"Red" && - principal != resource.owner - }; - """, "forbidRedNonOwner")); - - var policySet = new PolicySet(policies); - AuthorizationRequest request = new AuthorizationRequest(principal, action, resource, new HashMap<>()); - - // Should be denied because bob is not owner of task1 and it has Red status - var auth = new BasicAuthorizationEngine(); - assertDoesNotThrow(() -> { - final var response = auth.isAuthorized(request, policySet, entities); - assertEquals(response.type, SuccessOrFailure.Success); - final var success = response.success.get(); - assertFalse(success.isAllowed()); - }); - } - - /** - * Test authorization with schema validation - positive case with valid enum entities. - */ - @Test - public void enumAuthorizationWithSchemaValidationPositive() { - EntityTypeName userType = EntityTypeName.parse("User").get(); - EntityTypeName actionType = EntityTypeName.parse("Action").get(); - EntityTypeName applicationType = EntityTypeName.parse("Application").get(); - - // Create valid entities - Entity principalEntity = new Entity(userType.of("alice"), new HashMap<>() {{ - put("name", new PrimString("Alice")); - }}, new HashSet<>()); - - Entity actionEntity = new Entity(actionType.of("CreateList"), new HashMap<>(), new HashSet<>()); - Entity resourceEntity = new Entity(applicationType.of("TinyTodo"), new HashMap<>(), new HashSet<>()); - - Set entities = new HashSet<>(); - entities.add(principalEntity); - entities.add(actionEntity); - entities.add(resourceEntity); - - var policies = new HashSet(); - policies.add(new Policy(""" - permit( - principal, - action == Action::"CreateList", - resource == Application::"TinyTodo" - ); - """, "validEnumPolicy")); - - var policySet = new PolicySet(policies); - - // Create authorization request with schema validation enabled - AuthorizationRequest request = new AuthorizationRequest( - principalEntity, - actionEntity, - resourceEntity, - Optional.of(new HashMap<>()), - Optional.of(ENUM_SCHEMA), - true // Enable request validation - ); - - var auth = new BasicAuthorizationEngine(); - assertDoesNotThrow(() -> { - final var response = auth.isAuthorized(request, policySet, entities); - assertEquals(response.type, SuccessOrFailure.Success); - final var success = response.success.get(); - assertTrue(success.isAllowed()); - }); - } - - /** - * Test authorization with schema validation - negative case with invalid enum entity. - */ - @Test - public void enumAuthorizationWithSchemaValidationNegative() { - EntityTypeName userType = EntityTypeName.parse("User").get(); - EntityTypeName actionType = EntityTypeName.parse("Action").get(); - EntityTypeName applicationType = EntityTypeName.parse("Application").get(); - - // Create entities with invalid enum value - Entity principalEntity = new Entity(userType.of("alice"), new HashMap<>() {{ - put("name", new PrimString("Alice")); - }}, new HashSet<>()); - - Entity actionEntity = new Entity(actionType.of("CreateList"), new HashMap<>(), new HashSet<>()); - // Use INVALID enum value - "InvalidApp" is not in the Application enum - Entity resourceEntity = new Entity(applicationType.of("InvalidApp"), new HashMap<>(), new HashSet<>()); - - Set entities = new HashSet<>(); - entities.add(principalEntity); - entities.add(actionEntity); - entities.add(resourceEntity); - - var policies = new HashSet(); - policies.add(new Policy(""" - permit( - principal, - action == Action::"CreateList", - resource == Application::"InvalidApp" - ); - """, "invalidEnumPolicy")); - - var policySet = new PolicySet(policies); - - // Create authorization request with schema validation enabled - AuthorizationRequest request = new AuthorizationRequest( - principalEntity, - actionEntity, - resourceEntity, - Optional.of(new HashMap<>()), - Optional.of(ENUM_SCHEMA), - true // Enable request validation - this should catch the invalid enum - ); - - // Should return failure response due to invalid enum value when schema validation is enabled - assertFailure(request, policySet, entities); - } - - /** - * Test authorization with invalid enum in task status attribute. - */ - @Test - public void enumAuthorizationWithInvalidTaskStatus() { - EntityTypeName userType = EntityTypeName.parse("User").get(); - EntityTypeName actionType = EntityTypeName.parse("Action").get(); - EntityTypeName taskType = EntityTypeName.parse("Task").get(); - EntityTypeName colorType = EntityTypeName.parse("Color").get(); - - Entity principalEntity = new Entity(userType.of("alice"), new HashMap<>() {{ - put("name", new PrimString("Alice")); - }}, new HashSet<>()); - - Entity actionEntity = new Entity(actionType.of("UpdateTask"), new HashMap<>(), new HashSet<>()); - - // Create task with invalid enum status value "Purple" (not in Color enum) - Entity taskEntity = new Entity(taskType.of("task1"), new HashMap<>() {{ - put("owner", principalEntity.getEUID()); - put("name", new PrimString("Test task")); - put("status", new EntityUID(colorType, "Purple")); // Invalid enum value - }}, new HashSet<>()); - - Set entities = new HashSet<>(); - entities.add(principalEntity); - entities.add(actionEntity); - entities.add(taskEntity); - - var policies = new HashSet(); - policies.add(new Policy(""" - permit( - principal, - action == Action::"UpdateTask", - resource - ) when { - resource.status != Color::"Red" - }; - """, "taskPolicy")); - - var policySet = new PolicySet(policies); - - // Create authorization request with schema validation enabled - AuthorizationRequest request = new AuthorizationRequest( - principalEntity, - actionEntity, - taskEntity, - Optional.of(new HashMap<>()), - Optional.of(ENUM_SCHEMA), - true // Enable request validation - ); - - // Should return failure response due to invalid enum value in task status - assertFailure(request, policySet, entities); - } - - private static final Schema ENUM_SCHEMA = loadSchemaResource("/enum_schema.json"); - private void assumePartialEvaluation(Executable executable) { try { executable.execute(); diff --git a/CedarJava/src/test/java/com/cedarpolicy/EntityValidationTests.java b/CedarJava/src/test/java/com/cedarpolicy/EntityValidationTests.java index a19446c7..681cbae5 100644 --- a/CedarJava/src/test/java/com/cedarpolicy/EntityValidationTests.java +++ b/CedarJava/src/test/java/com/cedarpolicy/EntityValidationTests.java @@ -207,15 +207,19 @@ public void testValidEnumEntities() throws AuthException { EntityTypeName taskType = EntityTypeName.parse("Task").get(); EntityTypeName colorType = EntityTypeName.parse("Color").get(); - Entity user = new Entity(userType.of("alice"), new HashMap<>() {{ - put("name", new PrimString("Alice")); - }}, new HashSet<>()); - - Entity task = new Entity(taskType.of("task1"), new HashMap<>() {{ - put("owner", user.getEUID()); - put("name", new PrimString("Complete project")); - put("status", new EntityUID(colorType, "Red")); - }}, new HashSet<>()); + Entity user = new Entity(userType.of("alice"), new HashMap<>() { + { + put("name", new PrimString("Alice")); + } + }, new HashSet<>()); + + Entity task = new Entity(taskType.of("task1"), new HashMap<>() { + { + put("owner", user.getEUID()); + put("name", new PrimString("Complete project")); + put("status", new EntityUID(colorType, "Red")); + } + }, new HashSet<>()); EntityValidationRequest request = new EntityValidationRequest(ENUM_SCHEMA, List.of(user, task)); engine.validateEntities(request); @@ -230,21 +234,24 @@ public void testEnumEntitiesWithInvalidValues() throws AuthException { EntityTypeName taskType = EntityTypeName.parse("Task").get(); EntityTypeName colorType = EntityTypeName.parse("Color").get(); - Entity user = new Entity(userType.of("alice"), new HashMap<>() {{ - put("name", new PrimString("Alice")); - }}, new HashSet<>()); + Entity user = new Entity(userType.of("alice"), new HashMap<>() { + { + put("name", new PrimString("Alice")); + } + }, new HashSet<>()); // Create task with invalid enum value "Purple" (not in Color enum) - Entity task = new Entity(taskType.of("task1"), new HashMap<>() {{ - put("owner", user.getEUID()); - put("name", new PrimString("Complete project")); - put("status", new EntityUID(colorType, "Purple")); // Invalid enum value - }}, new HashSet<>()); + Entity task = new Entity(taskType.of("task1"), new HashMap<>() { + { + put("owner", user.getEUID()); + put("name", new PrimString("Complete project")); + put("status", new EntityUID(colorType, "Purple")); // Invalid enum value + } + }, new HashSet<>()); EntityValidationRequest request = new EntityValidationRequest(ENUM_SCHEMA, List.of(user, task)); - BadRequestException exception = assertThrows(BadRequestException.class, - () -> engine.validateEntities(request)); + BadRequestException exception = assertThrows(BadRequestException.class, () -> engine.validateEntities(request)); String errMsg = exception.getErrors().get(0); assertTrue(errMsg.contains("Purple") || errMsg.contains("Color"), @@ -252,21 +259,22 @@ public void testEnumEntitiesWithInvalidValues() throws AuthException { } /** - * Test that enum entities cannot have attributes according to RFC. + * Test that enum entities cannot have attributes. */ @Test public void testEnumEntitiesCannotHaveAttributes() throws AuthException { EntityTypeName colorType = EntityTypeName.parse("Color").get(); // Try to create enum entity with attributes (should fail) - Entity enumEntity = new Entity(colorType.of("Red"), new HashMap<>() {{ - put("shade", new PrimString("Dark")); // Enum entities shouldn't have attributes - }}, new HashSet<>()); + Entity enumEntity = new Entity(colorType.of("Red"), new HashMap<>() { + { + put("shade", new PrimString("Dark")); // Enum entities shouldn't have attributes + } + }, new HashSet<>()); EntityValidationRequest request = new EntityValidationRequest(ENUM_SCHEMA, List.of(enumEntity)); - BadRequestException exception = assertThrows(BadRequestException.class, - () -> engine.validateEntities(request)); + BadRequestException exception = assertThrows(BadRequestException.class, () -> engine.validateEntities(request)); String errMsg = exception.getErrors().get(0); assertTrue(errMsg.contains("attribute") && (errMsg.contains("Color") || errMsg.contains("Red")), @@ -274,26 +282,29 @@ public void testEnumEntitiesCannotHaveAttributes() throws AuthException { } /** - * Test that enum entities cannot have parents according to RFC. + * Test that enum entities cannot have parents. */ @Test public void testEnumEntitiesCannotHaveParents() throws AuthException { EntityTypeName colorType = EntityTypeName.parse("Color").get(); EntityTypeName userType = EntityTypeName.parse("User").get(); - Entity user = new Entity(userType.of("alice"), new HashMap<>() {{ - put("name", new PrimString("Alice")); - }}, new HashSet<>()); + Entity user = new Entity(userType.of("alice"), new HashMap<>() { + { + put("name", new PrimString("Alice")); + } + }, new HashSet<>()); // Try to create enum entity with parent (should fail) - Entity enumEntity = new Entity(colorType.of("Red"), new HashMap<>(), new HashSet<>() {{ - add(user.getEUID()); // Enum entities shouldn't have parents - }}); + Entity enumEntity = new Entity(colorType.of("Red"), new HashMap<>(), new HashSet<>() { + { + add(user.getEUID()); // Enum entities shouldn't have parents + } + }); EntityValidationRequest request = new EntityValidationRequest(ENUM_SCHEMA, List.of(user, enumEntity)); - BadRequestException exception = assertThrows(BadRequestException.class, - () -> engine.validateEntities(request)); + BadRequestException exception = assertThrows(BadRequestException.class, () -> engine.validateEntities(request)); String errMsg = exception.getErrors().get(0); assertTrue(errMsg.contains("parent") || errMsg.contains("ancestor") || errMsg.contains("Color"), @@ -309,54 +320,24 @@ public void testEnumEntitiesWithCedarSchema() throws AuthException { EntityTypeName taskType = EntityTypeName.parse("Task").get(); EntityTypeName colorType = EntityTypeName.parse("Color").get(); - Entity user = new Entity(userType.of("bob"), new HashMap<>() {{ - put("name", new PrimString("Bob")); - }}, new HashSet<>()); + Entity user = new Entity(userType.of("bob"), new HashMap<>() { + { + put("name", new PrimString("Bob")); + } + }, new HashSet<>()); - Entity task = new Entity(taskType.of("task2"), new HashMap<>() {{ - put("owner", user.getEUID()); - put("name", new PrimString("Review code")); - put("status", new EntityUID(colorType, "Green")); - }}, new HashSet<>()); + Entity task = new Entity(taskType.of("task2"), new HashMap<>() { + { + put("owner", user.getEUID()); + put("name", new PrimString("Review code")); + put("status", new EntityUID(colorType, "Green")); + } + }, new HashSet<>()); EntityValidationRequest cedarRequest = new EntityValidationRequest(ENUM_SCHEMA_CEDAR, List.of(user, task)); engine.validateEntities(cedarRequest); } - /** - * Test Application enum validation from RFC example. - */ - @Test - public void testApplicationEnumValidation() throws AuthException { - EntityTypeName applicationType = EntityTypeName.parse("Application").get(); - - // Valid Application enum entity - Entity application = new Entity(applicationType.of("TinyTodo"), new HashMap<>(), new HashSet<>()); - - EntityValidationRequest request = new EntityValidationRequest(ENUM_SCHEMA, List.of(application)); - engine.validateEntities(request); - } - - /** - * Test invalid Application enum value. - */ - @Test - public void testInvalidApplicationEnumValue() throws AuthException { - EntityTypeName applicationType = EntityTypeName.parse("Application").get(); - - // Invalid Application enum entity - using "InvalidApp" which is not in the enum - Entity application = new Entity(applicationType.of("InvalidApp"), new HashMap<>(), new HashSet<>()); - - EntityValidationRequest request = new EntityValidationRequest(ENUM_SCHEMA, List.of(application)); - - BadRequestException exception = assertThrows(BadRequestException.class, - () -> engine.validateEntities(request)); - - String errMsg = exception.getErrors().get(0); - assertTrue(errMsg.contains("InvalidApp") || errMsg.contains("Application"), - "Expected error about invalid Application enum value but was: '%s'".formatted(errMsg)); - } - @BeforeAll public static void setUp() { diff --git a/CedarJava/src/test/java/com/cedarpolicy/SchemaTests.java b/CedarJava/src/test/java/com/cedarpolicy/SchemaTests.java index 2279164c..c3ab0e31 100644 --- a/CedarJava/src/test/java/com/cedarpolicy/SchemaTests.java +++ b/CedarJava/src/test/java/com/cedarpolicy/SchemaTests.java @@ -243,7 +243,6 @@ void testMalformedSchema() { class EnumSchemaTests { @Test - @DisplayName("Should parse JSON schema with enum entities") void testParseJsonEnumSchema() { assertDoesNotThrow(() -> { Schema enumSchema = loadSchemaResource("/enum_schema.json"); @@ -261,36 +260,6 @@ void testParseCedarEnumSchema() { } @Test - @DisplayName("Should parse inline enum entity definitions") - void testParseInlineEnumDefinitions() { - assertDoesNotThrow(() -> { - // Test Cedar format inline enum - Schema.parse(JsonOrCedar.Cedar, """ - entity Color enum ["Red", "Blue", "Green"]; - entity Application enum ["TinyTodo"]; - """); - - // Test JSON format inline enum - Schema.parse(JsonOrCedar.Json, """ - { - "": { - "entityTypes": { - "Color": { - "enum": ["Red", "Blue", "Green"] - }, - "Application": { - "enum": ["TinyTodo"] - } - }, - "actions": {} - } - } - """); - }); - } - - @Test - @DisplayName("Should reject empty enum definitions") void testRejectEmptyEnums() { // Test Cedar format empty enum assertThrows(Exception.class, () -> { @@ -300,54 +269,53 @@ void testRejectEmptyEnums() { // Test JSON format empty enum assertThrows(Exception.class, () -> { Schema.parse(JsonOrCedar.Json, """ - { - "": { - "entityTypes": { - "Color": { - "enum": [] - } - }, - "actions": {} + { + "": { + "entityTypes": { + "Color": { + "enum": [] + } + }, + "actions": {} + } } - } - """); + """); }); } @Test - @DisplayName("Should convert enum schemas between JSON and Cedar formats") void testEnumSchemaFormatConversion() throws Exception { // Test Cedar to JSON conversion Schema cedarEnumSchema = Schema.parse(JsonOrCedar.Cedar, """ - entity Color enum ["Red", "Blue", "Green"]; - entity User; - action view appliesTo { principal: [User], resource: [User] }; - """); + entity Color enum ["Red", "Blue", "Green"]; + entity User; + action view appliesTo { principal: [User], resource: [User] }; + """); JsonNode jsonResult = cedarEnumSchema.toJsonFormat(); assertNotNull(jsonResult, "JSON conversion result should not be null"); // Test JSON to Cedar conversion String jsonEnumSchema = """ - { - "": { - "entityTypes": { - "Color": { - "enum": ["Red", "Blue", "Green"] + { + "": { + "entityTypes": { + "Color": { + "enum": ["Red", "Blue", "Green"] + }, + "User": {} }, - "User": {} - }, - "actions": { - "view": { - "appliesTo": { - "principalTypes": ["User"], - "resourceTypes": ["User"] + "actions": { + "view": { + "appliesTo": { + "principalTypes": ["User"], + "resourceTypes": ["User"] + } } } } } - } - """; + """; Schema jsonSchemaObj = Schema.parse(JsonOrCedar.Json, jsonEnumSchema); String cedarResult = jsonSchemaObj.toCedarFormat(); assertNotNull(cedarResult, "Cedar conversion result should not be null"); diff --git a/CedarJava/src/test/java/com/cedarpolicy/ValidationTests.java b/CedarJava/src/test/java/com/cedarpolicy/ValidationTests.java index 959bda22..d3943c70 100644 --- a/CedarJava/src/test/java/com/cedarpolicy/ValidationTests.java +++ b/CedarJava/src/test/java/com/cedarpolicy/ValidationTests.java @@ -222,6 +222,26 @@ public void validateLevelPolicyFailsWhenExpected() { thenIsNotValid(levelResponse); } + /** Test enum entity validation with valid enum values. */ + @Test + public void givenEnumSchemaAndValidEnumUsageReturnsValid() { + givenSchema(ENUM_SCHEMA); + givenPolicy("policy0", "permit(" + " principal == User::\"alice\"," + " action == Action::\"UpdateTask\"," + + " resource == Task::\"task1\"" + ") when {" + " resource.status == Color::\"Red\"" + "};"); + ValidationResponse response = whenValidated(); + thenIsValid(response); + } + + /** Test enum entity validation with invalid enum values. */ + @Test + public void givenEnumSchemaAndInvalidEnumValueReturnsInvalid() { + givenSchema(ENUM_SCHEMA); + givenPolicy("policy0", "permit(" + " principal == User::\"alice\"," + " action == Action::\"UpdateTask\"," + + " resource == Task::\"task1\"" + ") when {" + " resource.status != Color::\"Purple\"" + "};"); + ValidationResponse response = whenValidated(); + thenIsNotValid(response); + } + private void givenSchema(Schema testSchema) { this.schema = testSchema; } @@ -287,155 +307,4 @@ private void reset() { private static final Schema LEVEL_SCHEMA = loadSchemaResource("/level_schema.json"); private static final Schema ENUM_SCHEMA = loadSchemaResource("/enum_schema.json"); - /** Test enum entity validation with valid enum values. */ - @Test - public void givenEnumSchemaAndValidEnumUsageReturnsValid() { - givenSchema(ENUM_SCHEMA); - givenPolicy( - "policy0", - "permit(" - + " principal == User::\"alice\"," - + " action == Action::\"UpdateTask\"," - + " resource == Task::\"task1\"" - + ") when {" - + " resource.status == Color::\"Red\"" - + "};"); - ValidationResponse response = whenValidated(); - thenIsValid(response); - ValidationResponse levelResponse = whenLevelValidated(1); - thenIsValid(levelResponse); - } - - /** Test enum entity validation with invalid enum values. */ - @Test - public void givenEnumSchemaAndInvalidEnumValueReturnsInvalid() { - givenSchema(ENUM_SCHEMA); - givenPolicy( - "policy0", - "permit(" - + " principal == User::\"alice\"," - + " action == Action::\"UpdateTask\"," - + " resource == Task::\"task1\"" - + ") when {" - + " resource.status != Color::\"Purple\"" // Invalid enum value - + "};"); - ValidationResponse response = whenValidated(); - thenIsNotValid(response); - ValidationResponse levelResponse = whenLevelValidated(1); - thenIsNotValid(levelResponse); - } - - /** Test enum entity validation with case-sensitive enum values. */ - @Test - public void givenEnumSchemaAndWrongCaseEnumValueReturnsInvalid() { - givenSchema(ENUM_SCHEMA); - givenPolicy( - "policy0", - "permit(" - + " principal == User::\"alice\"," - + " action == Action::\"UpdateTask\"," - + " resource == Task::\"task1\"" - + ") when {" - + " resource.status != Color::\"red\"" // Wrong case - should be "Red" - + "};"); - ValidationResponse response = whenValidated(); - thenIsNotValid(response); - ValidationResponse levelResponse = whenLevelValidated(1); - thenIsNotValid(levelResponse); - } - - /** Test RFC example policy with enum entities. */ - @Test - public void givenEnumSchemaAndRFCExamplePolicyReturnsValid() { - givenSchema(ENUM_SCHEMA); - // This is the exact policy from the RFC - givenPolicy( - "policy0", - "permit(" - + " principal," - + " action == Action::\"UpdateTask\"," - + " resource" - + ") when {" - + " principal == resource.owner &&" - + " resource.status != Color::\"Red\"" - + "};"); - ValidationResponse response = whenValidated(); - thenIsValid(response); - ValidationResponse levelResponse = whenLevelValidated(1); - thenIsValid(levelResponse); - } - - /** Test Application enum from RFC example. */ - @Test - public void givenEnumSchemaAndApplicationEnumPolicyReturnsValid() { - givenSchema(ENUM_SCHEMA); - givenPolicy( - "policy0", - "permit(" - + " principal," - + " action == Action::\"CreateList\"," - + " resource == Application::\"TinyTodo\"" - + ");"); - ValidationResponse response = whenValidated(); - thenIsValid(response); - ValidationResponse levelResponse = whenLevelValidated(1); - thenIsValid(levelResponse); - } - - /** Test invalid Application enum usage. */ - @Test - public void givenEnumSchemaAndInvalidApplicationEnumReturnsInvalid() { - givenSchema(ENUM_SCHEMA); - givenPolicy( - "policy0", - "permit(" - + " principal," - + " action == Action::\"CreateList\"," - + " resource == Application::\"TinyTODO\"" // Typo in enum value - + ");"); - ValidationResponse response = whenValidated(); - thenIsNotValid(response); - ValidationResponse levelResponse = whenLevelValidated(1); - thenIsNotValid(levelResponse); - } - - /** Test multiple enum comparisons in policy. */ - @Test - public void givenEnumSchemaAndMultipleEnumComparisonsReturnsValid() { - givenSchema(ENUM_SCHEMA); - givenPolicy( - "policy0", - "permit(" - + " principal," - + " action == Action::\"UpdateTask\"," - + " resource" - + ") when {" - + " resource.status == Color::\"Blue\" ||" - + " resource.status == Color::\"Green\"" - + "};"); - ValidationResponse response = whenValidated(); - thenIsValid(response); - ValidationResponse levelResponse = whenLevelValidated(1); - thenIsValid(levelResponse); - } - - /** Test forbid policy with enum entities. */ - @Test - public void givenEnumSchemaAndForbidPolicyWithEnumsReturnsValid() { - givenSchema(ENUM_SCHEMA); - givenPolicy( - "policy0", - "forbid(" - + " principal," - + " action == Action::\"UpdateTask\"," - + " resource" - + ") when {" - + " resource.status == Color::\"Red\" &&" - + " principal != resource.owner" - + "};"); - ValidationResponse response = whenValidated(); - thenIsValid(response); - ValidationResponse levelResponse = whenLevelValidated(1); - thenIsValid(levelResponse); - } } diff --git a/CedarJava/src/test/java/com/cedarpolicy/pbt/IntegrationTests.java b/CedarJava/src/test/java/com/cedarpolicy/pbt/IntegrationTests.java index 04dd86b8..dfc46fb7 100644 --- a/CedarJava/src/test/java/com/cedarpolicy/pbt/IntegrationTests.java +++ b/CedarJava/src/test/java/com/cedarpolicy/pbt/IntegrationTests.java @@ -30,6 +30,7 @@ import com.cedarpolicy.model.policy.Policy; import com.cedarpolicy.model.policy.PolicySet; import com.cedarpolicy.model.policy.TemplateLink; +import com.cedarpolicy.model.schema.Schema; import com.cedarpolicy.value.DateTime; import com.cedarpolicy.value.Decimal; import com.cedarpolicy.value.Duration; @@ -895,4 +896,242 @@ public void testSchemaParsingAllow() { true); assertAllowed(request, policySet, entities); } + + private static final Schema ENUM_SCHEMA = loadSchemaResource("/enum_schema.json"); + + /** + * Build entities for enum authorization tests. + */ + private Set buildEntitiesForEnumTests() { + EntityTypeName userType = EntityTypeName.parse("User").get(); + EntityTypeName taskType = EntityTypeName.parse("Task").get(); + EntityTypeName colorType = EntityTypeName.parse("Color").get(); + EntityTypeName actionType = EntityTypeName.parse("Action").get(); + EntityTypeName applicationType = EntityTypeName.parse("Application").get(); + + Set entities = new HashSet<>(); + + // Users + Entity alice = new Entity(userType.of("alice"), new HashMap<>() { + { + put("name", new PrimString("Alice")); + } + }, new HashSet<>()); + + Entity bob = new Entity(userType.of("bob"), new HashMap<>() { + { + put("name", new PrimString("Bob")); + } + }, new HashSet<>()); + + // Tasks + Entity task1 = new Entity(taskType.of("task1"), new HashMap<>() { + { + put("owner", alice.getEUID()); + put("name", new PrimString("Complete project")); + put("status", new EntityUID(colorType, "Red")); + } + }, new HashSet<>()); + + Entity task2 = new Entity(taskType.of("task2"), new HashMap<>() { + { + put("owner", bob.getEUID()); + put("name", new PrimString("Review code")); + put("status", new EntityUID(colorType, "Green")); + } + }, new HashSet<>()); + + // Actions + Entity updateTaskAction = new Entity(actionType.of("UpdateTask"), new HashMap<>(), new HashSet<>()); + Entity createListAction = new Entity(actionType.of("CreateList"), new HashMap<>(), new HashSet<>()); + + // Application enum entity + Entity tinyTodoApp = new Entity(applicationType.of("TinyTodo"), new HashMap<>(), new HashSet<>()); + + entities.add(alice); + entities.add(bob); + entities.add(task1); + entities.add(task2); + entities.add(updateTaskAction); + entities.add(createListAction); + entities.add(tinyTodoApp); + + return entities; + } + + /** + * Test authorization requests which results in Deny due to enum type + */ + @Test + public void testValidEnumAuthorizationDeny() { + EntityUID principal = new EntityUID(EntityTypeName.parse("User").get(), "alice"); + EntityUID action = new EntityUID(EntityTypeName.parse("Action").get(), "UpdateTask"); + EntityUID resource = new EntityUID(EntityTypeName.parse("Task").get(), "task2"); // task2 has Green status + + Set entities = buildEntitiesForEnumTests(); + + var policies = new HashSet(); + policies.add(new Policy(""" + permit( + principal, + action == Action::"UpdateTask", + resource + ) when { + resource.status != Color::"Green" + }; + """, "notGreenPolicy")); + + var policySet = new PolicySet(policies); + AuthorizationRequest request = new AuthorizationRequest(principal, action, resource, new HashMap<>()); + + assertNotAllowed(request, policySet, entities); + } + + /** + * Test authorization requests which results in Permit due to enum type + */ + @Test + public void testValidEnumAuthorizationAllow() { + EntityUID principal = new EntityUID(EntityTypeName.parse("User").get(), "bob"); + EntityUID action = new EntityUID(EntityTypeName.parse("Action").get(), "UpdateTask"); + EntityUID resource = new EntityUID(EntityTypeName.parse("Task").get(), "task2"); // task2 has Green status + + Set entities = buildEntitiesForEnumTests(); + + // Policy: allow if status is exactly Green + var policies = new HashSet(); + policies.add(new Policy(""" + permit( + principal == User::"bob", + action == Action::"UpdateTask", + resource + ) when { + resource.status == Color::"Green" + }; + """, "greenPolicy")); + + var policySet = new PolicySet(policies); + AuthorizationRequest request = new AuthorizationRequest(principal, action, resource, new HashMap<>()); + + assertAllowed(request, policySet, entities); + } + + /** + * Test authorization requests where policy references multiple enums + */ + @Test + public void testEnumAuthorizationMultipleValues() { + EntityUID principal = new EntityUID(EntityTypeName.parse("User").get(), "bob"); + EntityUID action = new EntityUID(EntityTypeName.parse("Action").get(), "UpdateTask"); + EntityUID resource = new EntityUID(EntityTypeName.parse("Task").get(), "task2"); // Green status + + Set entities = buildEntitiesForEnumTests(); + + // Policy: allow if status is Blue OR Green + var policies = new HashSet(); + policies.add(new Policy(""" + permit( + principal, + action == Action::"UpdateTask", + resource + ) when { + resource.status == Color::"Blue" || + resource.status == Color::"Green" + }; + """, "multiEnumPolicy")); + + var policySet = new PolicySet(policies); + AuthorizationRequest request = new AuthorizationRequest(principal, action, resource, new HashMap<>()); + + assertAllowed(request, policySet, entities); + } + + /** + * Test authorization with schema validation - positive case with valid enum entities. + */ + @Test + public void testValidEnumAuthorizationWithValidation() { + EntityTypeName userType = EntityTypeName.parse("User").get(); + EntityTypeName actionType = EntityTypeName.parse("Action").get(); + EntityTypeName applicationType = EntityTypeName.parse("Application").get(); + + // Create valid entities + Entity principalEntity = new Entity(userType.of("alice"), new HashMap<>() { + { + put("name", new PrimString("Alice")); + } + }, new HashSet<>()); + + Entity actionEntity = new Entity(actionType.of("CreateList"), new HashMap<>(), new HashSet<>()); + Entity resourceEntity = new Entity(applicationType.of("TinyTodo"), new HashMap<>(), new HashSet<>()); + + Set entities = new HashSet<>(); + entities.add(principalEntity); + entities.add(actionEntity); + entities.add(resourceEntity); + + var policies = new HashSet(); + policies.add(new Policy(""" + permit( + principal, + action == Action::"CreateList", + resource == Application::"TinyTodo" + ); + """, "validEnumPolicy")); + + var policySet = new PolicySet(policies); + + // Create authorization request with schema validation enabled + AuthorizationRequest request = new AuthorizationRequest(principalEntity, actionEntity, resourceEntity, + Optional.of(new HashMap<>()), Optional.of(ENUM_SCHEMA), true + ); + + assertAllowed(request, policySet, entities); + } + + /** + * Test authorization with schema validation - negative case with invalid enum entity. + */ + @Test + public void testInvalidEnumAuthorizationWithValidation() { + EntityTypeName userType = EntityTypeName.parse("User").get(); + EntityTypeName actionType = EntityTypeName.parse("Action").get(); + EntityTypeName applicationType = EntityTypeName.parse("Application").get(); + + // Create entities with invalid enum value + Entity principalEntity = new Entity(userType.of("alice"), new HashMap<>() { + { + put("name", new PrimString("Alice")); + } + }, new HashSet<>()); + + Entity actionEntity = new Entity(actionType.of("CreateList"), new HashMap<>(), new HashSet<>()); + // Use INVALID enum value - "InvalidApp" is not in the Application enum + Entity resourceEntity = new Entity(applicationType.of("InvalidApp"), new HashMap<>(), new HashSet<>()); + + Set entities = new HashSet<>(); + entities.add(principalEntity); + entities.add(actionEntity); + entities.add(resourceEntity); + + var policies = new HashSet(); + policies.add(new Policy(""" + permit( + principal, + action == Action::"CreateList", + resource == Application::"InvalidApp" + ); + """, "invalidEnumPolicy")); + + var policySet = new PolicySet(policies); + + // Create authorization request with schema validation enabled + AuthorizationRequest request = new AuthorizationRequest(principalEntity, actionEntity, resourceEntity, + Optional.of(new HashMap<>()), Optional.of(ENUM_SCHEMA), true // Enable request validation - this should + // catch the invalid enum + ); + + // Should return failure response due to invalid enum value when schema validation is enabled + assertFailure(request, policySet, entities); + } }