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
4 changes: 4 additions & 0 deletions CedarJava/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
* Added Schema conversion APIs [#325](https://github.com/cedar-policy/cedar-java/pull/325)
* Added Level Validation [#327](https://github.com/cedar-policy/cedar-java/pull/327)
* Added DateTime extension support [#328](https://github.com/cedar-policy/cedar-java/pull/328)
* Added Duration extension support [#331](https://github.com/cedar-policy/cedar-java/pull/331)
* Added Offset function support [#331](https://github.com/cedar-policy/cedar-java/pull/331)
* Added PolicySet to JSON conversion API [#329](https://github.com/cedar-policy/cedar-java/pull/329)
* Added Cedar Schema support for Entity Validation [#332](https://github.com/cedar-policy/cedar-java/pull/332)

## 4.3.1
### Added
Expand Down
100 changes: 90 additions & 10 deletions CedarJava/src/test/java/com/cedarpolicy/EntityValidationTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.cedarpolicy;

import static com.cedarpolicy.TestUtil.loadSchemaResource;
import static com.cedarpolicy.TestUtil.loadCedarSchemaResource;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

Expand Down Expand Up @@ -51,10 +52,19 @@ public class EntityValidationTests {
public void testValidEntity() throws AuthException {
Entity entity = EntityValidationTests.entityGen.arbitraryEntity();

EntityValidationRequest r = new EntityValidationRequest(
ROLE_SCHEMA, List.of(entity));
EntityValidationRequest request = new EntityValidationRequest(ROLE_SCHEMA, List.of(entity));
engine.validateEntities(request);
}

/**
* Test that a valid entity with the schema in Cedar format is accepted.
*/
@Test
public void testValidEntityWithCedarSchema() throws AuthException {
Entity entity = EntityValidationTests.entityGen.arbitraryEntity();
EntityValidationRequest cedarFormatRequest = new EntityValidationRequest(ROLE_SCHEMA_CEDAR, List.of(entity));

engine.validateEntities(r);
engine.validateEntities(cedarFormatRequest);
}

/**
Expand All @@ -67,11 +77,32 @@ public void testEntityWithUnknownAttribute() throws AuthException {

EntityValidationRequest request = new EntityValidationRequest(ROLE_SCHEMA, List.of(entity));

BadRequestException exception = assertThrows(BadRequestException.class, () -> engine.validateEntities(request));
BadRequestException exception =
assertThrows(BadRequestException.class, () -> engine.validateEntities(request));

String errMsg = exception.getErrors().get(0);
assertTrue(errMsg.matches("attribute `test` on `Role::\".*\"` should not exist according to the schema"),
assertTrue(errMsg.matches(
"attribute `test` on `Role::\".*\"` should not exist according to the schema"),
"Expected to match regex but was: '%s'".formatted(errMsg));
}

/**
* Test that an entity with an attribute not specified in the schema in Cedar format throws an
* exception.
*/
@Test
public void testEntityWithUnknownAttributeWithCedarSchema() throws AuthException {
Entity entity = EntityValidationTests.entityGen.arbitraryEntity();
entity.attrs.put("test", new PrimBool(true));

EntityValidationRequest cedarFormatRequest = new EntityValidationRequest(ROLE_SCHEMA_CEDAR, List.of(entity));

BadRequestException exception =
assertThrows(BadRequestException.class, () -> engine.validateEntities(cedarFormatRequest));

String errMsg = exception.getErrors().get(0);
assertTrue(errMsg.matches("attribute `test` on `Role::\".*\"` should not exist according to the schema"),
"Expected to match regex but was: '%s'".formatted(errMsg));
}

/**
Expand All @@ -87,13 +118,40 @@ public void testEntitiesWithCyclicParentRelationship() throws AuthException {
childEntity.parentsEUIDs.add(parentEntity.getEUID());
parentEntity.parentsEUIDs.add(childEntity.getEUID());

EntityValidationRequest request = new EntityValidationRequest(ROLE_SCHEMA, List.of(parentEntity, childEntity));
EntityValidationRequest request =
new EntityValidationRequest(ROLE_SCHEMA, List.of(parentEntity, childEntity));

BadRequestException exception = assertThrows(BadRequestException.class, () -> engine.validateEntities(request));
BadRequestException exception =
assertThrows(BadRequestException.class, () -> engine.validateEntities(request));

String errMsg = exception.getErrors().get(0);
assertTrue(errMsg.matches("input graph has a cycle containing vertex `Role::\".*\"`"),
"Expected to match regex but was: '%s'".formatted(errMsg));
}

/**
* Test that entities with a cyclic parent relationship throw an exception with the schema in Cedar
* format.
*/
@Test
public void testEntitiesWithCyclicParentRelationshipWithCedarSchema() throws AuthException {
// Arrange
Entity childEntity = EntityValidationTests.entityGen.arbitraryEntity();
Entity parentEntity = EntityValidationTests.entityGen.arbitraryEntity();

// Create a cyclic parent relationship between the entities
childEntity.parentsEUIDs.add(parentEntity.getEUID());
parentEntity.parentsEUIDs.add(childEntity.getEUID());

EntityValidationRequest cedarFormatRequest =
new EntityValidationRequest(ROLE_SCHEMA_CEDAR, List.of(parentEntity, childEntity));

BadRequestException exception =
assertThrows(BadRequestException.class, () -> engine.validateEntities(cedarFormatRequest));

String errMsg = exception.getErrors().get(0);
assertTrue(errMsg.matches("input graph has a cycle containing vertex `Role::\".*\"`"),
"Expected to match regex but was: '%s'".formatted(errMsg));
}

/**
Expand All @@ -106,12 +164,33 @@ public void testEntityWithUnknownTag() throws AuthException {

EntityValidationRequest request = new EntityValidationRequest(ROLE_SCHEMA, List.of(entity));

BadRequestException exception = assertThrows(BadRequestException.class, () -> engine.validateEntities(request));
BadRequestException exception =
assertThrows(BadRequestException.class, () -> engine.validateEntities(request));

String errMsg = exception.getErrors().get(0);
assertTrue(
errMsg.matches("found a tag `test` on `Role::\".*\"`, "
+ "but no tags should exist on `Role::\".*\"` according to the schema"),
"Expected to match regex but was: '%s'".formatted(errMsg));
}

/**
* Test that an entity with a tag not specified in the schema in Cedar format throws an exception.
*/
@Test
public void testEntityWithUnknownTagWithCedarSchema() throws AuthException {
Entity entity = EntityValidationTests.entityGen.arbitraryEntity();
entity.tags.put("test", new PrimString("value"));

EntityValidationRequest cedarFormatRequest = new EntityValidationRequest(ROLE_SCHEMA_CEDAR, List.of(entity));

BadRequestException exception =
assertThrows(BadRequestException.class, () -> engine.validateEntities(cedarFormatRequest));

String errMsg = exception.getErrors().get(0);
assertTrue(errMsg.matches("found a tag `test` on `Role::\".*\"`, "
+ "but no tags should exist on `Role::\".*\"` according to the schema"),
"Expected to match regex but was: '%s'".formatted(errMsg));
+ "but no tags should exist on `Role::\".*\"` according to the schema"),
"Expected to match regex but was: '%s'".formatted(errMsg));
}

@BeforeAll
Expand All @@ -124,4 +203,5 @@ 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");
}
12 changes: 12 additions & 0 deletions CedarJava/src/test/java/com/cedarpolicy/TestUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,18 @@ public static Schema loadSchemaResource(String schemaFile) {
}
}

public static Schema loadCedarSchemaResource(String schemaFile) {
try {
String text = new String(Files.readAllBytes(
Paths.get(
ValidationTests.class.getResource(schemaFile).toURI())),
StandardCharsets.UTF_8);
return new Schema(JsonOrCedar.Cedar, Optional.empty(), Optional.of(text));
} catch (Exception e) {
throw new RuntimeException("Failed to load test schema file " + schemaFile, e);
}
}

public static PolicySet buildValidPolicySet() {
EntityTypeName principalType = EntityTypeName.parse("User").get();
Set<Policy> policies = new HashSet<>();
Expand Down
1 change: 1 addition & 0 deletions CedarJava/src/test/resources/role_schema.cedarschema
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
entity Role in [Role];
42 changes: 24 additions & 18 deletions CedarJavaFFI/src/interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,25 +151,31 @@ pub fn json_validate_entities(input: &str) -> serde_json::Result<String> {
/// returns unit value () which is null value when serialized to json.
pub fn validate_entities(input: &str) -> serde_json::Result<Answer> {
let validate_entity_call = from_str::<ValidateEntityCall>(&input)?;
match Schema::from_json_value(validate_entity_call.schema) {
Err(e) => Ok(Answer::fail_bad_request(vec![e.to_string()])),
Ok(schema) => {
match Entities::from_json_value(validate_entity_call.entities, Some(&schema)) {
Err(error) => {
let err_message = match error {
EntitiesError::Serialization(err) => err.to_string(),
EntitiesError::Deserialization(err) => err.to_string(),
EntitiesError::Duplicate(err) => err.to_string(),
EntitiesError::TransitiveClosureError(err) => err.to_string(),
EntitiesError::InvalidEntity(err) => err.to_string(),
};
Ok(Answer::fail_bad_request(vec![err_message]))
}
Ok(_entities) => Ok(Answer::Success {
result: "null".to_string(),
}),
}
let schema = match validate_entity_call.schema {
Value::String(cedarschema_str) => match Schema::from_cedarschema_str(&cedarschema_str) {
Ok(s) => s.0,
Err(e) => return Ok(Answer::fail_bad_request(vec![e.to_string()])),
},
cedarschema_json_obj => match Schema::from_json_value(cedarschema_json_obj) {
Ok(s) => s,
Err(e) => return Ok(Answer::fail_bad_request(vec![e.to_string()])),
},
};

match Entities::from_json_value(validate_entity_call.entities, Some(&schema)) {
Err(error) => {
let err_message = match error {
EntitiesError::Serialization(err) => err.to_string(),
EntitiesError::Deserialization(err) => err.to_string(),
EntitiesError::Duplicate(err) => err.to_string(),
EntitiesError::TransitiveClosureError(err) => err.to_string(),
EntitiesError::InvalidEntity(err) => err.to_string(),
};
Ok(Answer::fail_bad_request(vec![err_message]))
}
Ok(_entities) => Ok(Answer::Success {
result: "null".to_string(),
}),
}
}

Expand Down
Loading