diff --git a/CedarJava/src/main/java/com/cedarpolicy/model/entity/Entity.java b/CedarJava/src/main/java/com/cedarpolicy/model/entity/Entity.java index ecef28c..00366e8 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/model/entity/Entity.java +++ b/CedarJava/src/main/java/com/cedarpolicy/model/entity/Entity.java @@ -25,8 +25,8 @@ /** * An entity is the kind of object about which authorization decisions are made; principals, * actions, and resources are all a kind of entity. Each entity is defined by its entity type, a - * unique identifier (UID), zero or more attributes mapped to values, and zero or more parent - * entities. + * unique identifier (UID), zero or more attributes mapped to values, zero or more parent + * entities, and zero or more tags. */ public class Entity { private final EntityUID euid; @@ -37,6 +37,9 @@ public class Entity { /** Set of entity EUIDs that are parents to this entity. */ public final Set parentsEUIDs; + /** Tags on this entity (RFC 82) */ + public final Map tags; + /** * Create an entity from an EntityUIDs, a map of attributes, and a set of parent EntityUIDs. * @@ -45,9 +48,22 @@ public class Entity { * @param parentsEUIDs Set of parent entities' EUIDs. */ public Entity(EntityUID uid, Map attributes, Set parentsEUIDs) { + this(uid, attributes, parentsEUIDs, new HashMap<>()); + } + + /** + * Create an entity from an EntityUIDs, a map of attributes, a set of parent EntityUIDs, and a map of tags. + * + * @param uid EUID of the Entity. + * @param attributes Key/Value map of attributes. + * @param parentsEUIDs Set of parent entities' EUIDs. + * @param tags Key/Value map of tags. + */ + public Entity(EntityUID uid, Map attributes, Set parentsEUIDs, Map tags) { this.attrs = new HashMap<>(attributes); this.euid = uid; this.parentsEUIDs = parentsEUIDs; + this.tags = new HashMap<>(tags); } @Override @@ -66,7 +82,15 @@ public String toString() { .map(e -> e.getKey() + ": " + e.getValue()) .collect(Collectors.joining("\n\t\t")); } - return euid.toString() + parentStr + attributeStr; + String tagsStr = ""; + if (!tags.isEmpty()) { + tagsStr = + "\n\ttags:\n\t\t" + + tags.entrySet().stream() + .map(e -> e.getKey() + ": " + e.getValue()) + .collect(Collectors.joining("\n\t\t")); + } + return euid.toString() + parentStr + attributeStr + tagsStr; } @@ -79,10 +103,18 @@ public EntityUID getEUID() { } /** - * Get this Entities parents + * Get this Entity's parents * @return the set of parent EntityUIDs */ public Set getParents() { return parentsEUIDs; } + + /** + * Get this Entity's tags + * @return the map of tags + */ + public Map getTags() { + return tags; + } } diff --git a/CedarJava/src/main/java/com/cedarpolicy/serializer/EntitySerializer.java b/CedarJava/src/main/java/com/cedarpolicy/serializer/EntitySerializer.java index fccf4e5..c7cd142 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/serializer/EntitySerializer.java +++ b/CedarJava/src/main/java/com/cedarpolicy/serializer/EntitySerializer.java @@ -37,6 +37,7 @@ public void serialize( jsonGenerator.writeObjectField("attrs", entity.attrs); jsonGenerator.writeObjectField("parents", entity.getParents().stream().map(EntityUID::asJson).collect(Collectors.toSet())); + jsonGenerator.writeObjectField("tags", entity.tags); jsonGenerator.writeEndObject(); } } diff --git a/CedarJava/src/test/java/com/cedarpolicy/EntityValidationTests.java b/CedarJava/src/test/java/com/cedarpolicy/EntityValidationTests.java index 812e1a6..6cfc2b3 100644 --- a/CedarJava/src/test/java/com/cedarpolicy/EntityValidationTests.java +++ b/CedarJava/src/test/java/com/cedarpolicy/EntityValidationTests.java @@ -35,7 +35,7 @@ import com.cedarpolicy.pbt.EntityGen; import com.cedarpolicy.value.EntityTypeName; import com.cedarpolicy.value.PrimBool; - +import com.cedarpolicy.value.PrimString; /** * Tests for entity validator @@ -96,6 +96,24 @@ public void testEntitiesWithCyclicParentRelationship() throws AuthException { "Expected to match regex but was: '%s'".formatted(errMsg)); } + /** + * Test that an entity with a tag not specified in the schema throws an exception. + */ + @Test + public void testEntityWithUnknownTag() throws AuthException { + Entity entity = EntityValidationTests.entityGen.arbitraryEntity(); + entity.tags.put("test", new PrimString("value")); + + EntityValidationRequest request = new EntityValidationRequest(ROLE_SCHEMA, List.of(entity)); + + 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)); + } + @BeforeAll public static void setUp() { diff --git a/CedarJava/src/test/java/com/cedarpolicy/SharedIntegrationTests.java b/CedarJava/src/test/java/com/cedarpolicy/SharedIntegrationTests.java index 631dbff..68cb7cf 100644 --- a/CedarJava/src/test/java/com/cedarpolicy/SharedIntegrationTests.java +++ b/CedarJava/src/test/java/com/cedarpolicy/SharedIntegrationTests.java @@ -26,6 +26,8 @@ import com.cedarpolicy.model.AuthorizationResponse; import com.cedarpolicy.model.ValidationRequest; import com.cedarpolicy.model.ValidationResponse; +import com.cedarpolicy.model.ValidationResponse.ValidationError; +import com.cedarpolicy.model.ValidationResponse.ValidationSuccessResponse; import com.cedarpolicy.model.AuthorizationSuccessResponse.Decision; import com.cedarpolicy.model.exception.AuthException; import com.cedarpolicy.model.exception.BadRequestException; @@ -51,6 +53,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -173,6 +176,12 @@ private static class JsonEntity { value = "UWF_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD", justification = "Initialized by Jackson.") public List parents; + + /** Entity tags, where the value string is a Cedar literal value. */ + @SuppressFBWarnings( + value = "UWF_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD", + justification = "Initialized by Jackson.") + public Map tags; } /** @@ -296,8 +305,10 @@ private Entity loadEntity(JsonEntity je) { .map(euid -> EntityUID.parseFromJson(euid).get()) .collect(Collectors.toSet()); + // Support tags while also supporting old JsonEntity objects that don't specify tags + Map tags = je.tags != null ? je.tags : new HashMap<>(); - return new Entity(EntityUID.parseFromJson(je.uid).get(), je.attrs, parents); + return new Entity(EntityUID.parseFromJson(je.uid).get(), je.attrs, parents, tags); } /** @@ -326,7 +337,13 @@ private void executeJsonValidationTest(PolicySet policies, Schema schema, Boolea ValidationResponse result = auth.validate(validationQuery); assertEquals(result.type, ValidationResponse.SuccessOrFailure.Success); if (shouldValidate) { - assertTrue(result.validationPassed()); + ValidationSuccessResponse validationSuccessResponse = result.success.get(); + + // Assemble the validation failure messages, if any + List valErrList = List.copyOf(validationSuccessResponse.validationErrors); + String validationErrorMessages = valErrList.stream().map(e -> e.getError().message).collect(Collectors.joining(", ")); + + assertTrue(result.validationPassed(), validationErrorMessages); } } catch (BadRequestException e) { // A `BadRequestException` is the results of a parsing error.