From 55da0e99fd6b2ec2830b70e6f05a8154583a368a Mon Sep 17 00:00:00 2001 From: Mudit Chaudhary Date: Mon, 10 Feb 2025 21:05:08 +0000 Subject: [PATCH 01/16] changes CedarJson visibility, registers EntityDeserializer to CedarJson Signed-off-by: Mudit Chaudhary --- CedarJava/src/main/java/com/cedarpolicy/CedarJson.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CedarJava/src/main/java/com/cedarpolicy/CedarJson.java b/CedarJava/src/main/java/com/cedarpolicy/CedarJson.java index 898b3525..6900fc21 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/CedarJson.java +++ b/CedarJava/src/main/java/com/cedarpolicy/CedarJson.java @@ -26,6 +26,7 @@ import com.cedarpolicy.serializer.SchemaSerializer; import com.cedarpolicy.serializer.ValueDeserializer; import com.cedarpolicy.serializer.ValueSerializer; +import com.cedarpolicy.serializer.EntityDeserializer; import com.cedarpolicy.value.Value; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; @@ -33,7 +34,7 @@ import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; -final class CedarJson { +public final class CedarJson { private static final ObjectMapper OBJECT_MAPPER = createObjectMapper(); private CedarJson() { @@ -41,7 +42,7 @@ private CedarJson() { } public static ObjectMapper objectMapper() { - return OBJECT_MAPPER; + return OBJECT_MAPPER.copy(); } public static ObjectWriter objectWriter() { @@ -62,6 +63,7 @@ private static ObjectMapper createObjectMapper() { module.addSerializer(PolicySet.class, new PolicySetSerializer()); module.addSerializer(Value.class, new ValueSerializer()); module.addDeserializer(Value.class, new ValueDeserializer()); + module.addDeserializer(Entity.class, new EntityDeserializer()); mapper.registerModule(module); mapper.registerModule(new Jdk8Module()); From 51a9866478e14af60fd73601cf8010ef995a208d Mon Sep 17 00:00:00 2001 From: Mudit Chaudhary Date: Mon, 10 Feb 2025 21:05:49 +0000 Subject: [PATCH 02/16] adds Entity deserializer Signed-off-by: Mudit Chaudhary --- .../serializer/EntityDeserializer.java | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 CedarJava/src/main/java/com/cedarpolicy/serializer/EntityDeserializer.java diff --git a/CedarJava/src/main/java/com/cedarpolicy/serializer/EntityDeserializer.java b/CedarJava/src/main/java/com/cedarpolicy/serializer/EntityDeserializer.java new file mode 100644 index 00000000..881f4e09 --- /dev/null +++ b/CedarJava/src/main/java/com/cedarpolicy/serializer/EntityDeserializer.java @@ -0,0 +1,168 @@ +/* + * Copyright Cedar Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.cedarpolicy.serializer; + +import com.cedarpolicy.model.exception.InvalidValueDeserializationException; +import com.cedarpolicy.value.Value; +import com.cedarpolicy.value.EntityUID; +import com.cedarpolicy.model.entity.Entity; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.IOException; +import java.util.Map; +import java.util.HashMap; +import java.util.Set; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +/** + * Deserialize Json to Entity. + */ +public class EntityDeserializer extends JsonDeserializer { + + @Override + /** + * Deserializes a JSON input into an Entity object. + * + * @param parser The JsonParser providing the JSON input to deserialize + * @param context The deserialization context + * + * @return An Entity object constructed from the JSON input + * @throws IOException If there is an error reading + * from the JsonParser + * @throws InvalidValueDeserializationException If the JSON input is invalid or + * missing required fields + */ + public Entity deserialize(JsonParser parser, DeserializationContext context) + throws IOException, InvalidValueDeserializationException { + final JsonNode node = parser.getCodec().readTree(parser); + final ObjectMapper mapper = (ObjectMapper) parser.getCodec(); + + EntityUID euid; + if (node.has("uid")) { + JsonNode uidNode = node.get("uid"); + euid = parseEntityUID(parser, uidNode); + } else { + String msg = "\"uid\" not found"; + throw new InvalidValueDeserializationException(parser, msg, node.asToken(), Entity.class); + } + + Map attrs; + if (node.has("attrs")) { + JsonNode attrsNode = node.get("attrs"); + if (attrsNode.isObject()) { + attrs = parseValueMap(mapper, attrsNode); + } else { + String msg = "\"attrs\" must be a JSON object"; + throw new InvalidValueDeserializationException(parser, msg, attrsNode.asToken(), Entity.class); + } + } else { + String msg = "\"attrs\" not found"; + throw new InvalidValueDeserializationException(parser, msg, node.asToken(), Entity.class); + } + + Set parentEUIDs; + if (node.has("parents")) { + JsonNode parentsNode = node.get("parents"); + if (parentsNode.isArray()) { + parentEUIDs = StreamSupport.stream(parentsNode.spliterator(), false) + .map(parentNode -> { + try { + return parseEntityUID(parser, parentNode); + } catch (InvalidValueDeserializationException e) { + throw new RuntimeException(e); + } + }) + .collect(Collectors.toSet()); + } else { + String msg = "\"parents\" field must be a JSON array"; + throw new InvalidValueDeserializationException(parser, msg, parentsNode.asToken(), Entity.class); + } + } else { + String msg = "\"parents\" not found"; + throw new InvalidValueDeserializationException(parser, msg, node.asToken(), Entity.class); + } + + Map tags = new HashMap<>(); + if (node.has("tags")) { + JsonNode tagsNode = node.get("tags"); + if (tagsNode.isObject()) { + tags.putAll(parseValueMap(mapper, tagsNode)); + } else { + String msg = "\"tags\" must be a JSON object"; + throw new InvalidValueDeserializationException(parser, msg, tagsNode.asToken(), Entity.class); + } + } + return new Entity(euid, attrs, parentEUIDs, tags); + } + + /** + * Parses a JSON node into an EntityUID object. + * + * @param parser The JsonParser used for error reporting + * @param entityUIDJson The JsonNode containing the entity UID data to parse. + * Must have "type" and "id" fields. + * + * @return An EntityUID object constructed from the JSON data + * @throws InvalidValueDeserializationException if the required fields are + * missing or invalid + */ + private EntityUID parseEntityUID(JsonParser parser, JsonNode entityUIDJson) + throws InvalidValueDeserializationException { + if (entityUIDJson.has("type") && entityUIDJson.has("id")) { + JsonEUID jsonEuid = new JsonEUID(entityUIDJson.get("type").asText(), entityUIDJson.get("id").asText()); + return EntityUID.parseFromJson(jsonEuid).get(); + } else { + String msg = "\"type\" or \"id\" not found"; + throw new InvalidValueDeserializationException(parser, msg, entityUIDJson.asToken(), Entity.class); + } + } + + /** + * Parses a JSON node containing key-value pairs into a Map of String to Value + * objects. + * + * @param mapper The ObjectMapper used to convert JSON nodes to Value + * objects + * @param valueMapJson The JsonNode containing the key-value pairs to parse + * + * @return A Map where keys are Strings and values are Value objects + * @throws RuntimeException if there is an error converting a JSON node to a + * Value + */ + private Map parseValueMap(ObjectMapper mapper, JsonNode valueMapJson) { + Map valueMap = StreamSupport + .stream(Spliterators.spliteratorUnknownSize(valueMapJson.fields(), Spliterator.ORDERED), false) + .collect(Collectors.toMap( + Map.Entry::getKey, + entry -> { + try { + return mapper.treeToValue(entry.getValue(), Value.class); + } catch (IOException e) { + throw new RuntimeException(e); + } + })); + return valueMap; + } +} From 10a09b6abbd26d9a59b0604ced16a080ea379c6e Mon Sep 17 00:00:00 2001 From: Mudit Chaudhary Date: Mon, 10 Feb 2025 21:08:43 +0000 Subject: [PATCH 03/16] updates .gitignore for CedarJava Signed-off-by: Mudit Chaudhary --- CedarJava/.gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CedarJava/.gitignore b/CedarJava/.gitignore index c5959e38..e23adf0b 100644 --- a/CedarJava/.gitignore +++ b/CedarJava/.gitignore @@ -13,6 +13,6 @@ .factorypath .project .settings/ - +bin/ # Ignore changes to gradle.properties because we enter passwords here for releases /gradle.properties From c7ef50a3645ae7978d76753a76da2635b2dce3d0 Mon Sep 17 00:00:00 2001 From: Mudit Chaudhary Date: Tue, 11 Feb 2025 14:30:39 +0000 Subject: [PATCH 04/16] adds parse from JSON for Entity Signed-off-by: Mudit Chaudhary --- .../com/cedarpolicy/model/entity/Entity.java | 85 ++++++++++++++----- 1 file changed, 63 insertions(+), 22 deletions(-) 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 bdb6dfe6..a2b16e91 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/model/entity/Entity.java +++ b/CedarJava/src/main/java/com/cedarpolicy/model/entity/Entity.java @@ -18,14 +18,24 @@ import com.cedarpolicy.value.EntityUID; import com.cedarpolicy.value.Value; +import com.cedarpolicy.CedarJson; +import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.core.JsonProcessingException; + +import java.io.IOException; +import java.nio.file.Files; import java.util.*; import java.util.stream.Collectors; +import java.nio.file.Path; /** - * 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, zero or more parent + * 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, zero or + * more parent * entities, and zero or more tags. */ public class Entity { @@ -41,10 +51,11 @@ public class Entity { public final Map tags; /** - * Create an entity from an EntityUIDs, a map of attributes, and a set of parent EntityUIDs. + * Create an entity from an EntityUIDs, a map of attributes, and a set of parent + * EntityUIDs. * - * @param uid EUID of the Entity. - * @param attributes Key/Value map of attributes. + * @param uid EUID of the Entity. + * @param attributes Key/Value map of attributes. * @param parentsEUIDs Set of parent entities' EUIDs. */ public Entity(EntityUID uid, Map attributes, Set parentsEUIDs) { @@ -52,12 +63,13 @@ public Entity(EntityUID uid, Map attributes, Set paren } /** - * Create an entity from an EntityUIDs, a map of attributes, a set of parent EntityUIDs, and a map of tags. + * 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 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. + * @param tags Key/Value map of tags. */ public Entity(EntityUID uid, Map attributes, Set parentsEUIDs, Map tags) { this.attrs = new HashMap<>(attributes); @@ -68,7 +80,7 @@ public Entity(EntityUID uid, Map attributes, Set paren /** * Get the value for the given attribute, or null if not present. - * + * * @param attribute Attribute key * @return Attribute value for the given key or null if not present * @throws IllegalArgumentException if attribute is null @@ -90,26 +102,24 @@ public String toString() { } String attributeStr = ""; if (!attrs.isEmpty()) { - attributeStr = - "\n\tattrs:\n\t\t" - + attrs.entrySet().stream() - .map(e -> e.getKey() + ": " + e.getValue()) - .collect(Collectors.joining("\n\t\t")); + attributeStr = "\n\tattrs:\n\t\t" + + attrs.entrySet().stream() + .map(e -> e.getKey() + ": " + e.getValue()) + .collect(Collectors.joining("\n\t\t")); } 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")); + 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; } - /** * Get the entity uid + * * @return Entity UID */ public EntityUID getEUID() { @@ -118,6 +128,7 @@ public EntityUID getEUID() { /** * Get this Entity's parents + * * @return the set of parent EntityUIDs */ public Set getParents() { @@ -126,9 +137,39 @@ public Set getParents() { /** * Get this Entity's tags + * * @return the map of tags */ public Map getTags() { return tags; } + + /** + * Parse Entity from a JSON string + * + * @param jsonString The JSON string representation of an Entity + * + * @return Entity object parsed from the JSON string + * @throws JsonProcessingException if the JSON string cannot be parsed into an + * Entity + */ + public static Entity parse(String jsonString) throws JsonProcessingException { + ObjectReader reader = CedarJson.objectReader(); + return reader.forType(Entity.class).readValue(jsonString); + } + + /** + * Parse Entity from a file containing JSON representation of an Entity + * + * @param filePath Path to the file containing Entity JSON + * + * @return Entity object parsed from the file contents + * @throws IOException if there is an error reading the file + * @throws JsonProcessingException if the file contents cannot be parsed into an + * Entity + */ + public static Entity parse(Path filePath) throws IOException, JsonProcessingException { + String jsonString = Files.readString(filePath); + return parse(jsonString); + } } From 20a4fbc9eae5945d0a1c50c672899b86f330ba8f Mon Sep 17 00:00:00 2001 From: Mudit Chaudhary Date: Tue, 11 Feb 2025 15:31:02 +0000 Subject: [PATCH 05/16] refactors Entity.parse Signed-off-by: Mudit Chaudhary --- .../src/main/java/com/cedarpolicy/model/entity/Entity.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) 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 a2b16e91..501a6dbb 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/model/entity/Entity.java +++ b/CedarJava/src/main/java/com/cedarpolicy/model/entity/Entity.java @@ -18,9 +18,8 @@ import com.cedarpolicy.value.EntityUID; import com.cedarpolicy.value.Value; -import com.cedarpolicy.CedarJson; +import static com.cedarpolicy.CedarJson.objectReader; -import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.core.JsonProcessingException; import java.io.IOException; @@ -154,8 +153,7 @@ public Map getTags() { * Entity */ public static Entity parse(String jsonString) throws JsonProcessingException { - ObjectReader reader = CedarJson.objectReader(); - return reader.forType(Entity.class).readValue(jsonString); + return objectReader().forType(Entity.class).readValue(jsonString); } /** From efe88ef21274dc2b8abca095294eb52ee8101341 Mon Sep 17 00:00:00 2001 From: Mudit Chaudhary Date: Tue, 11 Feb 2025 15:45:30 +0000 Subject: [PATCH 06/16] adds tests for Entity parse from JSON Signed-off-by: Mudit Chaudhary --- .../java/com/cedarpolicy/EntityTests.java | 128 +++++++++++++++++- .../src/test/resources/invalid_entity.json | 1 + .../src/test/resources/valid_entity.json | 25 ++++ 3 files changed, 149 insertions(+), 5 deletions(-) create mode 100644 CedarJava/src/test/resources/invalid_entity.json create mode 100644 CedarJava/src/test/resources/valid_entity.json diff --git a/CedarJava/src/test/java/com/cedarpolicy/EntityTests.java b/CedarJava/src/test/java/com/cedarpolicy/EntityTests.java index 94c78379..1f6eaa22 100644 --- a/CedarJava/src/test/java/com/cedarpolicy/EntityTests.java +++ b/CedarJava/src/test/java/com/cedarpolicy/EntityTests.java @@ -14,21 +14,25 @@ * limitations under the License. */ - package com.cedarpolicy; +package com.cedarpolicy; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.util.HashMap; -import java.util.HashSet; - import static org.junit.jupiter.api.Assertions.assertEquals; import com.cedarpolicy.value.*; import com.cedarpolicy.model.entity.Entity; +import static com.cedarpolicy.CedarJson.objectWriter; +import com.fasterxml.jackson.core.JsonProcessingException; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.HashSet; public class EntityTests { + private static final String TEST_RESOURCES_DIR = "src/test/resources/"; @Test public void getAttrTests() { @@ -49,4 +53,118 @@ public void getAttrTests() { // Test key not found assertEquals(principal.getAttr("decimalAttr"), null); } + + @Test + public void givenValidJSONStringParseReturns() throws JsonProcessingException { + String validEntityJson = """ + {"uid":{"type":"Photo","id":"pic01"}, + "attrs":{ + "dummyIP": {"__extn":{"fn":"ip","arg":"192.168.1.100"}}, + "dummyUser": {"__entity":{"type":"User","id":"Alice"}}, + "nestedAttr":{ + "managerName": "Someone", + "skip":{ + "name": "something", + "who": {"__entity":{"type":"User","id":"Alice"}} + } + } + }, + "parents":[{"type":"Photo","id":"pic01"}], + "tags": { + "dummyTagIP": {"__extn":{"fn":"ip","arg":"192.168.1.100"}}, + "dummyTagUser": {"__entity":{"type":"User::Tag","id":"Alice"}}, + "nestedTagAttr":{ + "managerTagName": "Someone", + "skipTag":{ + "name": "somethingTag", + "who": {"__entity":{"type":"UserFromStringTag","id":"AliceTag"}} + } + } + } + } + """; + + Entity entity = Entity.parse(validEntityJson); + String jsonRepresentation = objectWriter().writeValueAsString(entity); + String expectedRepresentation = "{\"uid\":{\"type\":\"Photo\",\"id\":\"pic01\"}," + + "\"attrs\":{\"dummyIP\":{\"__extn\":{\"fn\":\"ip\",\"arg\":\"192.168.1.100\"}}," + + "\"nestedAttr\":{\"skip\":{\"name\":\"something\",\"who\":{\"__entity\":{\"id\":\"Alice\",\"type\":\"User\"}}}," + + "\"managerName\":\"Someone\"},\"dummyUser\":{\"__entity\":{\"id\":\"Alice\",\"type\":\"User\"}}}," + + "\"parents\":[{\"type\":\"Photo\",\"id\":\"pic01\"}]," + + "\"tags\":{\"dummyTagIP\":{\"__extn\":{\"fn\":\"ip\",\"arg\":\"192.168.1.100\"}}," + + "\"nestedTagAttr\":{\"skipTag\":{\"name\":\"somethingTag\"," + + "\"who\":{\"__entity\":{\"id\":\"AliceTag\",\"type\":\"UserFromStringTag\"}}},\"managerTagName\":\"Someone\"}," + + "\"dummyTagUser\":{\"__entity\":{\"id\":\"Alice\",\"type\":\"User::Tag\"}}}}"; + + assertEquals(expectedRepresentation, jsonRepresentation); + + validEntityJson = """ + {"uid":{"type":"Photo","id":"pic01"}, + "attrs":{}, + "parents":[]} + """; + entity = Entity.parse(validEntityJson); + jsonRepresentation = objectWriter().writeValueAsString(entity); + expectedRepresentation = "{\"uid\":{\"type\":\"Photo\",\"id\":\"pic01\"}," + + "\"attrs\":{}," + + "\"parents\":[]," + + "\"tags\":{}}"; + + assertEquals(expectedRepresentation, jsonRepresentation); + } + + @Test + public void givenInvalidJSONStringParseThrows() throws JsonProcessingException { + String invalidEntityJson = """ + {"uid":{"type":"Photo","id":"pic01"}} + """; + + assertThrows(JsonProcessingException.class, () -> { + Entity.parse(invalidEntityJson); + }); + + String invalidEntityJson2 = """ + {"uid":{"type":"Photo","id":"pic01"}}, + "parents":{}, + "attrs":[] + """; + + assertThrows(JsonProcessingException.class, () -> { + Entity.parse(invalidEntityJson2); + }); + + String invalidEntityJson3 = """ + {"uid":{"type":"Photo","id":"pic01"}}, + "parents":[], + "attrs":{}, + "tags":[] + """; + + assertThrows(JsonProcessingException.class, () -> { + Entity.parse(invalidEntityJson3); + }); + } + + public void givenValidJSONFileParseReturns() throws JsonProcessingException, IOException { + + Entity entity = Entity.parse(Path.of(TEST_RESOURCES_DIR + "valid_entity.json")); + String jsonRepresentation = objectWriter().writeValueAsString(entity); + String expectedRepresentation = "{\"uid\":{\"type\":\"Photo\",\"id\":\"pic01\"}," + + "\"attrs\":{\"dummyIP\":{\"__extn\":{\"fn\":\"ip\",\"arg\":\"192.168.1.100\"}}," + + "\"nestedAttr\":{\"skip\":{\"name\":\"something\",\"who\":{\"__entity\":{\"id\":\"Alice\",\"type\":\"User\"}}}," + + "\"managerName\":\"Someone\"},\"dummyUser\":{\"__entity\":{\"id\":\"Alice\",\"type\":\"User\"}}}," + + "\"parents\":[{\"type\":\"Photo\",\"id\":\"pic01\"}]," + + "\"tags\":{\"dummyTagIP\":{\"__extn\":{\"fn\":\"ip\",\"arg\":\"192.168.1.100\"}}," + + "\"nestedTagAttr\":{\"skipTag\":{\"name\":\"somethingTag\"," + + "\"who\":{\"__entity\":{\"id\":\"AliceTag\",\"type\":\"UserFromStringTag\"}}},\"managerTagName\":\"Someone\"}," + + "\"dummyTagUser\":{\"__entity\":{\"id\":\"Alice\",\"type\":\"User::Tag\"}}}}"; + + assertEquals(expectedRepresentation, jsonRepresentation); + } + + public void givenInvalidJSONFileParseThrows() throws JsonProcessingException, IOException { + assertThrows(JsonProcessingException.class, () -> { + Entity.parse(Path.of(TEST_RESOURCES_DIR + "invalid_entity.json")); + }); + } } diff --git a/CedarJava/src/test/resources/invalid_entity.json b/CedarJava/src/test/resources/invalid_entity.json new file mode 100644 index 00000000..f7d9e8f1 --- /dev/null +++ b/CedarJava/src/test/resources/invalid_entity.json @@ -0,0 +1 @@ +{"uid":{"type":"Photo","id":"pic01"}} \ No newline at end of file diff --git a/CedarJava/src/test/resources/valid_entity.json b/CedarJava/src/test/resources/valid_entity.json new file mode 100644 index 00000000..c5b9dfe6 --- /dev/null +++ b/CedarJava/src/test/resources/valid_entity.json @@ -0,0 +1,25 @@ +{"uid":{"type":"Photo","id":"pic01"}, +"attrs":{ + "dummyIP": {"__extn":{"fn":"ip","arg":"192.168.1.100"}}, + "dummyUser": {"__entity":{"type":"User","id":"Alice"}}, + "nestedAttr":{ + "managerName": "Someone", + "skip":{ + "name": "something", + "who": {"__entity":{"type":"User","id":"Alice"}} + } + } + }, +"parents":[{"type":"Photo","id":"pic01"}], +"tags": { + "dummyTagIP": {"__extn":{"fn":"ip","arg":"192.168.1.100"}}, + "dummyTagUser": {"__entity":{"type":"User::Tag","id":"Alice"}}, + "nestedTagAttr":{ + "managerTagName": "Someone", + "skipTag":{ + "name": "somethingTag", + "who": {"__entity":{"type":"UserFromStringTag","id":"AliceTag"}} + } + } + } +} \ No newline at end of file From 9d62d6ed42c4edb826b93181301d344b8938acdc Mon Sep 17 00:00:00 2001 From: Mudit Chaudhary Date: Mon, 10 Feb 2025 21:05:08 +0000 Subject: [PATCH 07/16] changes CedarJson visibility, registers EntityDeserializer to CedarJson Signed-off-by: Mudit Chaudhary --- CedarJava/src/main/java/com/cedarpolicy/CedarJson.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CedarJava/src/main/java/com/cedarpolicy/CedarJson.java b/CedarJava/src/main/java/com/cedarpolicy/CedarJson.java index 898b3525..6900fc21 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/CedarJson.java +++ b/CedarJava/src/main/java/com/cedarpolicy/CedarJson.java @@ -26,6 +26,7 @@ import com.cedarpolicy.serializer.SchemaSerializer; import com.cedarpolicy.serializer.ValueDeserializer; import com.cedarpolicy.serializer.ValueSerializer; +import com.cedarpolicy.serializer.EntityDeserializer; import com.cedarpolicy.value.Value; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; @@ -33,7 +34,7 @@ import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; -final class CedarJson { +public final class CedarJson { private static final ObjectMapper OBJECT_MAPPER = createObjectMapper(); private CedarJson() { @@ -41,7 +42,7 @@ private CedarJson() { } public static ObjectMapper objectMapper() { - return OBJECT_MAPPER; + return OBJECT_MAPPER.copy(); } public static ObjectWriter objectWriter() { @@ -62,6 +63,7 @@ private static ObjectMapper createObjectMapper() { module.addSerializer(PolicySet.class, new PolicySetSerializer()); module.addSerializer(Value.class, new ValueSerializer()); module.addDeserializer(Value.class, new ValueDeserializer()); + module.addDeserializer(Entity.class, new EntityDeserializer()); mapper.registerModule(module); mapper.registerModule(new Jdk8Module()); From 8ca033692655d010ccb1a873ceb5af8f01036bca Mon Sep 17 00:00:00 2001 From: Mudit Chaudhary Date: Mon, 10 Feb 2025 21:05:49 +0000 Subject: [PATCH 08/16] adds Entity deserializer Signed-off-by: Mudit Chaudhary --- .../serializer/EntityDeserializer.java | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 CedarJava/src/main/java/com/cedarpolicy/serializer/EntityDeserializer.java diff --git a/CedarJava/src/main/java/com/cedarpolicy/serializer/EntityDeserializer.java b/CedarJava/src/main/java/com/cedarpolicy/serializer/EntityDeserializer.java new file mode 100644 index 00000000..881f4e09 --- /dev/null +++ b/CedarJava/src/main/java/com/cedarpolicy/serializer/EntityDeserializer.java @@ -0,0 +1,168 @@ +/* + * Copyright Cedar Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.cedarpolicy.serializer; + +import com.cedarpolicy.model.exception.InvalidValueDeserializationException; +import com.cedarpolicy.value.Value; +import com.cedarpolicy.value.EntityUID; +import com.cedarpolicy.model.entity.Entity; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.IOException; +import java.util.Map; +import java.util.HashMap; +import java.util.Set; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +/** + * Deserialize Json to Entity. + */ +public class EntityDeserializer extends JsonDeserializer { + + @Override + /** + * Deserializes a JSON input into an Entity object. + * + * @param parser The JsonParser providing the JSON input to deserialize + * @param context The deserialization context + * + * @return An Entity object constructed from the JSON input + * @throws IOException If there is an error reading + * from the JsonParser + * @throws InvalidValueDeserializationException If the JSON input is invalid or + * missing required fields + */ + public Entity deserialize(JsonParser parser, DeserializationContext context) + throws IOException, InvalidValueDeserializationException { + final JsonNode node = parser.getCodec().readTree(parser); + final ObjectMapper mapper = (ObjectMapper) parser.getCodec(); + + EntityUID euid; + if (node.has("uid")) { + JsonNode uidNode = node.get("uid"); + euid = parseEntityUID(parser, uidNode); + } else { + String msg = "\"uid\" not found"; + throw new InvalidValueDeserializationException(parser, msg, node.asToken(), Entity.class); + } + + Map attrs; + if (node.has("attrs")) { + JsonNode attrsNode = node.get("attrs"); + if (attrsNode.isObject()) { + attrs = parseValueMap(mapper, attrsNode); + } else { + String msg = "\"attrs\" must be a JSON object"; + throw new InvalidValueDeserializationException(parser, msg, attrsNode.asToken(), Entity.class); + } + } else { + String msg = "\"attrs\" not found"; + throw new InvalidValueDeserializationException(parser, msg, node.asToken(), Entity.class); + } + + Set parentEUIDs; + if (node.has("parents")) { + JsonNode parentsNode = node.get("parents"); + if (parentsNode.isArray()) { + parentEUIDs = StreamSupport.stream(parentsNode.spliterator(), false) + .map(parentNode -> { + try { + return parseEntityUID(parser, parentNode); + } catch (InvalidValueDeserializationException e) { + throw new RuntimeException(e); + } + }) + .collect(Collectors.toSet()); + } else { + String msg = "\"parents\" field must be a JSON array"; + throw new InvalidValueDeserializationException(parser, msg, parentsNode.asToken(), Entity.class); + } + } else { + String msg = "\"parents\" not found"; + throw new InvalidValueDeserializationException(parser, msg, node.asToken(), Entity.class); + } + + Map tags = new HashMap<>(); + if (node.has("tags")) { + JsonNode tagsNode = node.get("tags"); + if (tagsNode.isObject()) { + tags.putAll(parseValueMap(mapper, tagsNode)); + } else { + String msg = "\"tags\" must be a JSON object"; + throw new InvalidValueDeserializationException(parser, msg, tagsNode.asToken(), Entity.class); + } + } + return new Entity(euid, attrs, parentEUIDs, tags); + } + + /** + * Parses a JSON node into an EntityUID object. + * + * @param parser The JsonParser used for error reporting + * @param entityUIDJson The JsonNode containing the entity UID data to parse. + * Must have "type" and "id" fields. + * + * @return An EntityUID object constructed from the JSON data + * @throws InvalidValueDeserializationException if the required fields are + * missing or invalid + */ + private EntityUID parseEntityUID(JsonParser parser, JsonNode entityUIDJson) + throws InvalidValueDeserializationException { + if (entityUIDJson.has("type") && entityUIDJson.has("id")) { + JsonEUID jsonEuid = new JsonEUID(entityUIDJson.get("type").asText(), entityUIDJson.get("id").asText()); + return EntityUID.parseFromJson(jsonEuid).get(); + } else { + String msg = "\"type\" or \"id\" not found"; + throw new InvalidValueDeserializationException(parser, msg, entityUIDJson.asToken(), Entity.class); + } + } + + /** + * Parses a JSON node containing key-value pairs into a Map of String to Value + * objects. + * + * @param mapper The ObjectMapper used to convert JSON nodes to Value + * objects + * @param valueMapJson The JsonNode containing the key-value pairs to parse + * + * @return A Map where keys are Strings and values are Value objects + * @throws RuntimeException if there is an error converting a JSON node to a + * Value + */ + private Map parseValueMap(ObjectMapper mapper, JsonNode valueMapJson) { + Map valueMap = StreamSupport + .stream(Spliterators.spliteratorUnknownSize(valueMapJson.fields(), Spliterator.ORDERED), false) + .collect(Collectors.toMap( + Map.Entry::getKey, + entry -> { + try { + return mapper.treeToValue(entry.getValue(), Value.class); + } catch (IOException e) { + throw new RuntimeException(e); + } + })); + return valueMap; + } +} From 85065ec6f911d01d636bbe77a937dc3ce90f831d Mon Sep 17 00:00:00 2001 From: Mudit Chaudhary Date: Mon, 10 Feb 2025 21:08:43 +0000 Subject: [PATCH 09/16] updates .gitignore for CedarJava Signed-off-by: Mudit Chaudhary --- CedarJava/.gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CedarJava/.gitignore b/CedarJava/.gitignore index c5959e38..e23adf0b 100644 --- a/CedarJava/.gitignore +++ b/CedarJava/.gitignore @@ -13,6 +13,6 @@ .factorypath .project .settings/ - +bin/ # Ignore changes to gradle.properties because we enter passwords here for releases /gradle.properties From 1af537422fb275f87b77e571b136363e9607c1df Mon Sep 17 00:00:00 2001 From: Mudit Chaudhary Date: Tue, 11 Feb 2025 14:30:39 +0000 Subject: [PATCH 10/16] adds parse from JSON for Entity Signed-off-by: Mudit Chaudhary --- .../com/cedarpolicy/model/entity/Entity.java | 82 ++++++++++++++----- 1 file changed, 61 insertions(+), 21 deletions(-) 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 8b5e348b..032dfd85 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/model/entity/Entity.java +++ b/CedarJava/src/main/java/com/cedarpolicy/model/entity/Entity.java @@ -18,14 +18,24 @@ import com.cedarpolicy.value.EntityUID; import com.cedarpolicy.value.Value; +import com.cedarpolicy.CedarJson; +import com.fasterxml.jackson.databind.ObjectReader; +import com.fasterxml.jackson.core.JsonProcessingException; + +import java.io.IOException; +import java.nio.file.Files; import java.util.*; import java.util.stream.Collectors; +import java.nio.file.Path; /** - * 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, zero or more parent + * 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, zero or + * more parent * entities, and zero or more tags. */ public class Entity { @@ -62,8 +72,8 @@ public Entity(EntityUID uid, Set parentsEUIDs) { /** * Create an entity from an EntityUIDs, a map of attributes, and a set of parent EntityUIDs. * - * @param uid EUID of the Entity. - * @param attributes Key/Value map of attributes. + * @param uid EUID of the Entity. + * @param attributes Key/Value map of attributes. * @param parentsEUIDs Set of parent entities' EUIDs. */ public Entity(EntityUID uid, Map attributes, Set parentsEUIDs) { @@ -71,12 +81,13 @@ public Entity(EntityUID uid, Map attributes, Set paren } /** - * Create an entity from an EntityUIDs, a map of attributes, a set of parent EntityUIDs, and a map of tags. + * 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 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. + * @param tags Key/Value map of tags. */ public Entity(EntityUID uid, Map attributes, Set parentsEUIDs, Map tags) { this.attrs = new HashMap<>(attributes); @@ -87,7 +98,7 @@ public Entity(EntityUID uid, Map attributes, Set paren /** * Get the value for the given attribute, or null if not present. - * + * * @param attribute Attribute key * @return Attribute value for the given key or null if not present * @throws IllegalArgumentException if attribute is null @@ -109,26 +120,24 @@ public String toString() { } String attributeStr = ""; if (!attrs.isEmpty()) { - attributeStr = - "\n\tattrs:\n\t\t" - + attrs.entrySet().stream() - .map(e -> e.getKey() + ": " + e.getValue()) - .collect(Collectors.joining("\n\t\t")); + attributeStr = "\n\tattrs:\n\t\t" + + attrs.entrySet().stream() + .map(e -> e.getKey() + ": " + e.getValue()) + .collect(Collectors.joining("\n\t\t")); } 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")); + 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; } - /** * Get the entity uid + * * @return Entity UID */ public EntityUID getEUID() { @@ -137,6 +146,7 @@ public EntityUID getEUID() { /** * Get this Entity's parents + * * @return the set of parent EntityUIDs */ public Set getParents() { @@ -145,9 +155,39 @@ public Set getParents() { /** * Get this Entity's tags + * * @return the map of tags */ public Map getTags() { return tags; } + + /** + * Parse Entity from a JSON string + * + * @param jsonString The JSON string representation of an Entity + * + * @return Entity object parsed from the JSON string + * @throws JsonProcessingException if the JSON string cannot be parsed into an + * Entity + */ + public static Entity parse(String jsonString) throws JsonProcessingException { + ObjectReader reader = CedarJson.objectReader(); + return reader.forType(Entity.class).readValue(jsonString); + } + + /** + * Parse Entity from a file containing JSON representation of an Entity + * + * @param filePath Path to the file containing Entity JSON + * + * @return Entity object parsed from the file contents + * @throws IOException if there is an error reading the file + * @throws JsonProcessingException if the file contents cannot be parsed into an + * Entity + */ + public static Entity parse(Path filePath) throws IOException, JsonProcessingException { + String jsonString = Files.readString(filePath); + return parse(jsonString); + } } From 38f08d6e5085bd37af7f83160ac1721a7b0643da Mon Sep 17 00:00:00 2001 From: Mudit Chaudhary Date: Tue, 11 Feb 2025 15:31:02 +0000 Subject: [PATCH 11/16] refactors Entity.parse Signed-off-by: Mudit Chaudhary --- .../src/main/java/com/cedarpolicy/model/entity/Entity.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) 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 032dfd85..3de10884 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/model/entity/Entity.java +++ b/CedarJava/src/main/java/com/cedarpolicy/model/entity/Entity.java @@ -18,9 +18,8 @@ import com.cedarpolicy.value.EntityUID; import com.cedarpolicy.value.Value; -import com.cedarpolicy.CedarJson; +import static com.cedarpolicy.CedarJson.objectReader; -import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.core.JsonProcessingException; import java.io.IOException; @@ -172,8 +171,7 @@ public Map getTags() { * Entity */ public static Entity parse(String jsonString) throws JsonProcessingException { - ObjectReader reader = CedarJson.objectReader(); - return reader.forType(Entity.class).readValue(jsonString); + return objectReader().forType(Entity.class).readValue(jsonString); } /** From b116d7d73e7ef04a769d89f84f265ef80678049a Mon Sep 17 00:00:00 2001 From: Mudit Chaudhary Date: Tue, 11 Feb 2025 15:45:30 +0000 Subject: [PATCH 12/16] adds tests for Entity parse from JSON Signed-off-by: Mudit Chaudhary --- .../java/com/cedarpolicy/EntityTests.java | 126 +++++++++++++++++- .../src/test/resources/invalid_entity.json | 1 + .../src/test/resources/valid_entity.json | 25 ++++ 3 files changed, 148 insertions(+), 4 deletions(-) create mode 100644 CedarJava/src/test/resources/invalid_entity.json create mode 100644 CedarJava/src/test/resources/valid_entity.json diff --git a/CedarJava/src/test/java/com/cedarpolicy/EntityTests.java b/CedarJava/src/test/java/com/cedarpolicy/EntityTests.java index 84a0bb2a..594857c9 100644 --- a/CedarJava/src/test/java/com/cedarpolicy/EntityTests.java +++ b/CedarJava/src/test/java/com/cedarpolicy/EntityTests.java @@ -18,16 +18,21 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.util.HashMap; -import java.util.HashSet; - import static org.junit.jupiter.api.Assertions.assertEquals; import com.cedarpolicy.value.*; import com.cedarpolicy.model.entity.Entity; +import static com.cedarpolicy.CedarJson.objectWriter; + +import com.fasterxml.jackson.core.JsonProcessingException; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.HashSet; public class EntityTests { + private static final String TEST_RESOURCES_DIR = "src/test/resources/"; @Test public void getAttrTests() { @@ -81,4 +86,117 @@ public void newWithoutAttributesTests() { // Test the Entity's parents assertEquals(principal.getParents(), parents); } + + public void givenValidJSONStringParseReturns() throws JsonProcessingException { + String validEntityJson = """ + {"uid":{"type":"Photo","id":"pic01"}, + "attrs":{ + "dummyIP": {"__extn":{"fn":"ip","arg":"192.168.1.100"}}, + "dummyUser": {"__entity":{"type":"User","id":"Alice"}}, + "nestedAttr":{ + "managerName": "Someone", + "skip":{ + "name": "something", + "who": {"__entity":{"type":"User","id":"Alice"}} + } + } + }, + "parents":[{"type":"Photo","id":"pic01"}], + "tags": { + "dummyTagIP": {"__extn":{"fn":"ip","arg":"192.168.1.100"}}, + "dummyTagUser": {"__entity":{"type":"User::Tag","id":"Alice"}}, + "nestedTagAttr":{ + "managerTagName": "Someone", + "skipTag":{ + "name": "somethingTag", + "who": {"__entity":{"type":"UserFromStringTag","id":"AliceTag"}} + } + } + } + } + """; + + Entity entity = Entity.parse(validEntityJson); + String jsonRepresentation = objectWriter().writeValueAsString(entity); + String expectedRepresentation = "{\"uid\":{\"type\":\"Photo\",\"id\":\"pic01\"}," + + "\"attrs\":{\"dummyIP\":{\"__extn\":{\"fn\":\"ip\",\"arg\":\"192.168.1.100\"}}," + + "\"nestedAttr\":{\"skip\":{\"name\":\"something\",\"who\":{\"__entity\":{\"id\":\"Alice\",\"type\":\"User\"}}}," + + "\"managerName\":\"Someone\"},\"dummyUser\":{\"__entity\":{\"id\":\"Alice\",\"type\":\"User\"}}}," + + "\"parents\":[{\"type\":\"Photo\",\"id\":\"pic01\"}]," + + "\"tags\":{\"dummyTagIP\":{\"__extn\":{\"fn\":\"ip\",\"arg\":\"192.168.1.100\"}}," + + "\"nestedTagAttr\":{\"skipTag\":{\"name\":\"somethingTag\"," + + "\"who\":{\"__entity\":{\"id\":\"AliceTag\",\"type\":\"UserFromStringTag\"}}},\"managerTagName\":\"Someone\"}," + + "\"dummyTagUser\":{\"__entity\":{\"id\":\"Alice\",\"type\":\"User::Tag\"}}}}"; + + assertEquals(expectedRepresentation, jsonRepresentation); + + validEntityJson = """ + {"uid":{"type":"Photo","id":"pic01"}, + "attrs":{}, + "parents":[]} + """; + entity = Entity.parse(validEntityJson); + jsonRepresentation = objectWriter().writeValueAsString(entity); + expectedRepresentation = "{\"uid\":{\"type\":\"Photo\",\"id\":\"pic01\"}," + + "\"attrs\":{}," + + "\"parents\":[]," + + "\"tags\":{}}"; + + assertEquals(expectedRepresentation, jsonRepresentation); + } + + @Test + public void givenInvalidJSONStringParseThrows() throws JsonProcessingException { + String invalidEntityJson = """ + {"uid":{"type":"Photo","id":"pic01"}} + """; + + assertThrows(JsonProcessingException.class, () -> { + Entity.parse(invalidEntityJson); + }); + + String invalidEntityJson2 = """ + {"uid":{"type":"Photo","id":"pic01"}}, + "parents":{}, + "attrs":[] + """; + + assertThrows(JsonProcessingException.class, () -> { + Entity.parse(invalidEntityJson2); + }); + + String invalidEntityJson3 = """ + {"uid":{"type":"Photo","id":"pic01"}}, + "parents":[], + "attrs":{}, + "tags":[] + """; + + assertThrows(JsonProcessingException.class, () -> { + Entity.parse(invalidEntityJson3); + }); + } + + public void givenValidJSONFileParseReturns() throws JsonProcessingException, IOException { + + Entity entity = Entity.parse(Path.of(TEST_RESOURCES_DIR + "valid_entity.json")); + String jsonRepresentation = objectWriter().writeValueAsString(entity); + String expectedRepresentation = "{\"uid\":{\"type\":\"Photo\",\"id\":\"pic01\"}," + + "\"attrs\":{\"dummyIP\":{\"__extn\":{\"fn\":\"ip\",\"arg\":\"192.168.1.100\"}}," + + "\"nestedAttr\":{\"skip\":{\"name\":\"something\",\"who\":{\"__entity\":{\"id\":\"Alice\",\"type\":\"User\"}}}," + + "\"managerName\":\"Someone\"},\"dummyUser\":{\"__entity\":{\"id\":\"Alice\",\"type\":\"User\"}}}," + + "\"parents\":[{\"type\":\"Photo\",\"id\":\"pic01\"}]," + + "\"tags\":{\"dummyTagIP\":{\"__extn\":{\"fn\":\"ip\",\"arg\":\"192.168.1.100\"}}," + + "\"nestedTagAttr\":{\"skipTag\":{\"name\":\"somethingTag\"," + + "\"who\":{\"__entity\":{\"id\":\"AliceTag\",\"type\":\"UserFromStringTag\"}}},\"managerTagName\":\"Someone\"}," + + "\"dummyTagUser\":{\"__entity\":{\"id\":\"Alice\",\"type\":\"User::Tag\"}}}}"; + + assertEquals(expectedRepresentation, jsonRepresentation); + } + + public void givenInvalidJSONFileParseThrows() throws JsonProcessingException, IOException { + assertThrows(JsonProcessingException.class, () -> { + Entity.parse(Path.of(TEST_RESOURCES_DIR + "invalid_entity.json")); + }); + } } diff --git a/CedarJava/src/test/resources/invalid_entity.json b/CedarJava/src/test/resources/invalid_entity.json new file mode 100644 index 00000000..f7d9e8f1 --- /dev/null +++ b/CedarJava/src/test/resources/invalid_entity.json @@ -0,0 +1 @@ +{"uid":{"type":"Photo","id":"pic01"}} \ No newline at end of file diff --git a/CedarJava/src/test/resources/valid_entity.json b/CedarJava/src/test/resources/valid_entity.json new file mode 100644 index 00000000..c5b9dfe6 --- /dev/null +++ b/CedarJava/src/test/resources/valid_entity.json @@ -0,0 +1,25 @@ +{"uid":{"type":"Photo","id":"pic01"}, +"attrs":{ + "dummyIP": {"__extn":{"fn":"ip","arg":"192.168.1.100"}}, + "dummyUser": {"__entity":{"type":"User","id":"Alice"}}, + "nestedAttr":{ + "managerName": "Someone", + "skip":{ + "name": "something", + "who": {"__entity":{"type":"User","id":"Alice"}} + } + } + }, +"parents":[{"type":"Photo","id":"pic01"}], +"tags": { + "dummyTagIP": {"__extn":{"fn":"ip","arg":"192.168.1.100"}}, + "dummyTagUser": {"__entity":{"type":"User::Tag","id":"Alice"}}, + "nestedTagAttr":{ + "managerTagName": "Someone", + "skipTag":{ + "name": "somethingTag", + "who": {"__entity":{"type":"UserFromStringTag","id":"AliceTag"}} + } + } + } +} \ No newline at end of file From 44d1a1cb8bdedc8f11cd317f73a114a4736f906a Mon Sep 17 00:00:00 2001 From: Mudit Chaudhary Date: Tue, 11 Feb 2025 16:32:11 +0000 Subject: [PATCH 13/16] formats files Signed-off-by: Mudit Chaudhary --- .../java/com/cedarpolicy/model/entity/Entity.java | 11 +++++++---- .../src/test/java/com/cedarpolicy/EntityTests.java | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) 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 3de10884..fd51fc44 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/model/entity/Entity.java +++ b/CedarJava/src/main/java/com/cedarpolicy/model/entity/Entity.java @@ -50,7 +50,8 @@ public class Entity { public final Map tags; /** - * Create an entity from an EntityUID. It will have no attributes, parents, or tags. + * Create an entity from an EntityUID. It will have no attributes, parents, or + * tags. * * @param uid EUID of the Entity. */ @@ -59,9 +60,10 @@ public Entity(EntityUID uid) { } /** - * Create an entity from an EntityUID and a set of parent EntityUIDs. It will have no attributes or tags. + * Create an entity from an EntityUID and a set of parent EntityUIDs. It will + * have no attributes or tags. * - * @param uid EUID of the Entity. + * @param uid EUID of the Entity. * @param parentsEUIDs Set of parent entities' EUIDs. */ public Entity(EntityUID uid, Set parentsEUIDs) { @@ -69,7 +71,8 @@ public Entity(EntityUID uid, Set parentsEUIDs) { } /** - * Create an entity from an EntityUIDs, a map of attributes, and a set of parent EntityUIDs. + * Create an entity from an EntityUIDs, a map of attributes, and a set of parent + * EntityUIDs. * * @param uid EUID of the Entity. * @param attributes Key/Value map of attributes. diff --git a/CedarJava/src/test/java/com/cedarpolicy/EntityTests.java b/CedarJava/src/test/java/com/cedarpolicy/EntityTests.java index 594857c9..444a8ce7 100644 --- a/CedarJava/src/test/java/com/cedarpolicy/EntityTests.java +++ b/CedarJava/src/test/java/com/cedarpolicy/EntityTests.java @@ -86,7 +86,7 @@ public void newWithoutAttributesTests() { // Test the Entity's parents assertEquals(principal.getParents(), parents); } - + public void givenValidJSONStringParseReturns() throws JsonProcessingException { String validEntityJson = """ {"uid":{"type":"Photo","id":"pic01"}, From 7ff143d8f1db228706874ac230e397fa6478645d Mon Sep 17 00:00:00 2001 From: Mudit Chaudhary Date: Tue, 11 Feb 2025 16:36:04 +0000 Subject: [PATCH 14/16] fixes nits Signed-off-by: Mudit Chaudhary --- CedarJava/src/test/java/com/cedarpolicy/EntityTests.java | 1 - CedarJava/src/test/resources/invalid_entity.json | 2 +- CedarJava/src/test/resources/valid_entity.json | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/CedarJava/src/test/java/com/cedarpolicy/EntityTests.java b/CedarJava/src/test/java/com/cedarpolicy/EntityTests.java index 444a8ce7..2c872635 100644 --- a/CedarJava/src/test/java/com/cedarpolicy/EntityTests.java +++ b/CedarJava/src/test/java/com/cedarpolicy/EntityTests.java @@ -178,7 +178,6 @@ public void givenInvalidJSONStringParseThrows() throws JsonProcessingException { } public void givenValidJSONFileParseReturns() throws JsonProcessingException, IOException { - Entity entity = Entity.parse(Path.of(TEST_RESOURCES_DIR + "valid_entity.json")); String jsonRepresentation = objectWriter().writeValueAsString(entity); String expectedRepresentation = "{\"uid\":{\"type\":\"Photo\",\"id\":\"pic01\"}," diff --git a/CedarJava/src/test/resources/invalid_entity.json b/CedarJava/src/test/resources/invalid_entity.json index f7d9e8f1..d37cd9f8 100644 --- a/CedarJava/src/test/resources/invalid_entity.json +++ b/CedarJava/src/test/resources/invalid_entity.json @@ -1 +1 @@ -{"uid":{"type":"Photo","id":"pic01"}} \ No newline at end of file +{"uid":{"type":"Photo","id":"pic01"}} diff --git a/CedarJava/src/test/resources/valid_entity.json b/CedarJava/src/test/resources/valid_entity.json index c5b9dfe6..d35f2ca0 100644 --- a/CedarJava/src/test/resources/valid_entity.json +++ b/CedarJava/src/test/resources/valid_entity.json @@ -22,4 +22,4 @@ } } } -} \ No newline at end of file +} From 64fac0a3692ce079d134be0f0c7cb704a388527e Mon Sep 17 00:00:00 2001 From: Mudit Chaudhary Date: Tue, 11 Feb 2025 16:39:50 +0000 Subject: [PATCH 15/16] fixes nits -- javadoc Signed-off-by: Mudit Chaudhary --- .../java/com/cedarpolicy/serializer/EntityDeserializer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CedarJava/src/main/java/com/cedarpolicy/serializer/EntityDeserializer.java b/CedarJava/src/main/java/com/cedarpolicy/serializer/EntityDeserializer.java index 881f4e09..1f128b99 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/serializer/EntityDeserializer.java +++ b/CedarJava/src/main/java/com/cedarpolicy/serializer/EntityDeserializer.java @@ -41,7 +41,6 @@ */ public class EntityDeserializer extends JsonDeserializer { - @Override /** * Deserializes a JSON input into an Entity object. * @@ -54,6 +53,7 @@ public class EntityDeserializer extends JsonDeserializer { * @throws InvalidValueDeserializationException If the JSON input is invalid or * missing required fields */ + @Override public Entity deserialize(JsonParser parser, DeserializationContext context) throws IOException, InvalidValueDeserializationException { final JsonNode node = parser.getCodec().readTree(parser); From 94713c93e316049ac4078f3af7330032de4827ce Mon Sep 17 00:00:00 2001 From: Mudit Chaudhary Date: Tue, 11 Feb 2025 19:57:29 +0000 Subject: [PATCH 16/16] fixes formatting to avoid short wrapped lines Signed-off-by: Mudit Chaudhary --- .../com/cedarpolicy/model/entity/Entity.java | 45 ++++++---------- .../serializer/EntityDeserializer.java | 53 ++++++++----------- 2 files changed, 37 insertions(+), 61 deletions(-) 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 fd51fc44..ae05e0ae 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/model/entity/Entity.java +++ b/CedarJava/src/main/java/com/cedarpolicy/model/entity/Entity.java @@ -29,13 +29,9 @@ import java.nio.file.Path; /** - * 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, zero or - * more parent - * entities, and zero or more tags. + * 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, zero or more parent entities, and zero or more tags. */ public class Entity { private final EntityUID euid; @@ -50,8 +46,7 @@ public class Entity { public final Map tags; /** - * Create an entity from an EntityUID. It will have no attributes, parents, or - * tags. + * Create an entity from an EntityUID. It will have no attributes, parents, or tags. * * @param uid EUID of the Entity. */ @@ -60,8 +55,7 @@ public Entity(EntityUID uid) { } /** - * Create an entity from an EntityUID and a set of parent EntityUIDs. It will - * have no attributes or tags. + * Create an entity from an EntityUID and a set of parent EntityUIDs. It will have no attributes or tags. * * @param uid EUID of the Entity. * @param parentsEUIDs Set of parent entities' EUIDs. @@ -71,8 +65,7 @@ public Entity(EntityUID uid, Set parentsEUIDs) { } /** - * Create an entity from an EntityUIDs, a map of attributes, and a set of parent - * EntityUIDs. + * Create an entity from an EntityUIDs, a map of attributes, and a set of parent EntityUIDs. * * @param uid EUID of the Entity. * @param attributes Key/Value map of attributes. @@ -83,8 +76,7 @@ public Entity(EntityUID uid, Map attributes, Set paren } /** - * Create an entity from an EntityUIDs, a map of attributes, a set of parent - * EntityUIDs, and a map of tags. + * 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. @@ -102,6 +94,7 @@ public Entity(EntityUID uid, Map attributes, Set paren * Get the value for the given attribute, or null if not present. * * @param attribute Attribute key + * * @return Attribute value for the given key or null if not present * @throws IllegalArgumentException if attribute is null */ @@ -116,23 +109,19 @@ public Value getAttr(String attribute) { public String toString() { String parentStr = ""; if (!parentsEUIDs.isEmpty()) { - List parentStrs = new ArrayList(parentsEUIDs.stream() - .map(euid -> euid.toString()).collect(Collectors.toList())); + List parentStrs = new ArrayList( + parentsEUIDs.stream().map(euid -> euid.toString()).collect(Collectors.toList())); parentStr = "\n\tparents:\n\t\t" + String.join("\n\t\t", parentStrs); } String attributeStr = ""; if (!attrs.isEmpty()) { - attributeStr = "\n\tattrs:\n\t\t" - + attrs.entrySet().stream() - .map(e -> e.getKey() + ": " + e.getValue()) - .collect(Collectors.joining("\n\t\t")); + attributeStr = "\n\tattrs:\n\t\t" + attrs.entrySet().stream().map(e -> e.getKey() + ": " + e.getValue()) + .collect(Collectors.joining("\n\t\t")); } 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")); + 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; } @@ -170,8 +159,7 @@ public Map getTags() { * @param jsonString The JSON string representation of an Entity * * @return Entity object parsed from the JSON string - * @throws JsonProcessingException if the JSON string cannot be parsed into an - * Entity + * @throws JsonProcessingException if the JSON string cannot be parsed into an Entity */ public static Entity parse(String jsonString) throws JsonProcessingException { return objectReader().forType(Entity.class).readValue(jsonString); @@ -184,8 +172,7 @@ public static Entity parse(String jsonString) throws JsonProcessingException { * * @return Entity object parsed from the file contents * @throws IOException if there is an error reading the file - * @throws JsonProcessingException if the file contents cannot be parsed into an - * Entity + * @throws JsonProcessingException if the file contents cannot be parsed into an Entity */ public static Entity parse(Path filePath) throws IOException, JsonProcessingException { String jsonString = Files.readString(filePath); diff --git a/CedarJava/src/main/java/com/cedarpolicy/serializer/EntityDeserializer.java b/CedarJava/src/main/java/com/cedarpolicy/serializer/EntityDeserializer.java index 1f128b99..2fb1156b 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/serializer/EntityDeserializer.java +++ b/CedarJava/src/main/java/com/cedarpolicy/serializer/EntityDeserializer.java @@ -48,10 +48,8 @@ public class EntityDeserializer extends JsonDeserializer { * @param context The deserialization context * * @return An Entity object constructed from the JSON input - * @throws IOException If there is an error reading - * from the JsonParser - * @throws InvalidValueDeserializationException If the JSON input is invalid or - * missing required fields + * @throws IOException If there is an error reading from the JsonParser + * @throws InvalidValueDeserializationException If the JSON input is invalid or missing required fields */ @Override public Entity deserialize(JsonParser parser, DeserializationContext context) @@ -86,15 +84,13 @@ public Entity deserialize(JsonParser parser, DeserializationContext context) if (node.has("parents")) { JsonNode parentsNode = node.get("parents"); if (parentsNode.isArray()) { - parentEUIDs = StreamSupport.stream(parentsNode.spliterator(), false) - .map(parentNode -> { - try { - return parseEntityUID(parser, parentNode); - } catch (InvalidValueDeserializationException e) { - throw new RuntimeException(e); - } - }) - .collect(Collectors.toSet()); + parentEUIDs = StreamSupport.stream(parentsNode.spliterator(), false).map(parentNode -> { + try { + return parseEntityUID(parser, parentNode); + } catch (InvalidValueDeserializationException e) { + throw new RuntimeException(e); + } + }).collect(Collectors.toSet()); } else { String msg = "\"parents\" field must be a JSON array"; throw new InvalidValueDeserializationException(parser, msg, parentsNode.asToken(), Entity.class); @@ -121,12 +117,10 @@ public Entity deserialize(JsonParser parser, DeserializationContext context) * Parses a JSON node into an EntityUID object. * * @param parser The JsonParser used for error reporting - * @param entityUIDJson The JsonNode containing the entity UID data to parse. - * Must have "type" and "id" fields. + * @param entityUIDJson The JsonNode containing the entity UID data to parse. Must have "type" and "id" fields. * * @return An EntityUID object constructed from the JSON data - * @throws InvalidValueDeserializationException if the required fields are - * missing or invalid + * @throws InvalidValueDeserializationException if the required fields are missing or invalid */ private EntityUID parseEntityUID(JsonParser parser, JsonNode entityUIDJson) throws InvalidValueDeserializationException { @@ -140,29 +134,24 @@ private EntityUID parseEntityUID(JsonParser parser, JsonNode entityUIDJson) } /** - * Parses a JSON node containing key-value pairs into a Map of String to Value - * objects. + * Parses a JSON node containing key-value pairs into a Map of String to Value objects. * - * @param mapper The ObjectMapper used to convert JSON nodes to Value - * objects + * @param mapper The ObjectMapper used to convert JSON nodes to Value objects * @param valueMapJson The JsonNode containing the key-value pairs to parse * * @return A Map where keys are Strings and values are Value objects - * @throws RuntimeException if there is an error converting a JSON node to a - * Value + * @throws RuntimeException if there is an error converting a JSON node to a Value */ private Map parseValueMap(ObjectMapper mapper, JsonNode valueMapJson) { Map valueMap = StreamSupport .stream(Spliterators.spliteratorUnknownSize(valueMapJson.fields(), Spliterator.ORDERED), false) - .collect(Collectors.toMap( - Map.Entry::getKey, - entry -> { - try { - return mapper.treeToValue(entry.getValue(), Value.class); - } catch (IOException e) { - throw new RuntimeException(e); - } - })); + .collect(Collectors.toMap(Map.Entry::getKey, entry -> { + try { + return mapper.treeToValue(entry.getValue(), Value.class); + } catch (IOException e) { + throw new RuntimeException(e); + } + })); return valueMap; } }