Skip to content

Commit be7971c

Browse files
Adds functionality to parse Entity from JSON (#292)
Signed-off-by: Mudit Chaudhary <chmudit@amazon.com>
1 parent 6e1f655 commit be7971c

File tree

6 files changed

+360
-30
lines changed

6 files changed

+360
-30
lines changed

CedarJava/src/main/java/com/cedarpolicy/CedarJson.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,22 +26,23 @@
2626
import com.cedarpolicy.serializer.SchemaSerializer;
2727
import com.cedarpolicy.serializer.ValueDeserializer;
2828
import com.cedarpolicy.serializer.ValueSerializer;
29+
import com.cedarpolicy.serializer.EntityDeserializer;
2930
import com.cedarpolicy.value.Value;
3031
import com.fasterxml.jackson.databind.ObjectMapper;
3132
import com.fasterxml.jackson.databind.ObjectReader;
3233
import com.fasterxml.jackson.databind.ObjectWriter;
3334
import com.fasterxml.jackson.databind.module.SimpleModule;
3435
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
3536

36-
final class CedarJson {
37+
public final class CedarJson {
3738
private static final ObjectMapper OBJECT_MAPPER = createObjectMapper();
3839

3940
private CedarJson() {
4041
throw new IllegalStateException("Utility class");
4142
}
4243

4344
public static ObjectMapper objectMapper() {
44-
return OBJECT_MAPPER;
45+
return OBJECT_MAPPER.copy();
4546
}
4647

4748
public static ObjectWriter objectWriter() {
@@ -62,6 +63,7 @@ private static ObjectMapper createObjectMapper() {
6263
module.addSerializer(PolicySet.class, new PolicySetSerializer());
6364
module.addSerializer(Value.class, new ValueSerializer());
6465
module.addDeserializer(Value.class, new ValueDeserializer());
66+
module.addDeserializer(Entity.class, new EntityDeserializer());
6567
mapper.registerModule(module);
6668
mapper.registerModule(new Jdk8Module());
6769

CedarJava/src/main/java/com/cedarpolicy/model/entity/Entity.java

Lines changed: 52 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,20 @@
1818

1919
import com.cedarpolicy.value.EntityUID;
2020
import com.cedarpolicy.value.Value;
21+
import static com.cedarpolicy.CedarJson.objectReader;
2122

23+
import com.fasterxml.jackson.core.JsonProcessingException;
24+
25+
import java.io.IOException;
26+
import java.nio.file.Files;
2227
import java.util.*;
2328
import java.util.stream.Collectors;
29+
import java.nio.file.Path;
2430

2531
/**
26-
* An entity is the kind of object about which authorization decisions are made; principals,
27-
* actions, and resources are all a kind of entity. Each entity is defined by its entity type, a
28-
* unique identifier (UID), zero or more attributes mapped to values, zero or more parent
29-
* entities, and zero or more tags.
32+
* An entity is the kind of object about which authorization decisions are made; principals, actions, and resources are
33+
* all a kind of entity. Each entity is defined by its entity type, a unique identifier (UID), zero or more attributes
34+
* mapped to values, zero or more parent entities, and zero or more tags.
3035
*/
3136
public class Entity {
3237
private final EntityUID euid;
@@ -52,7 +57,7 @@ public Entity(EntityUID uid) {
5257
/**
5358
* Create an entity from an EntityUID and a set of parent EntityUIDs. It will have no attributes or tags.
5459
*
55-
* @param uid EUID of the Entity.
60+
* @param uid EUID of the Entity.
5661
* @param parentsEUIDs Set of parent entities' EUIDs.
5762
*/
5863
public Entity(EntityUID uid, Set<EntityUID> parentsEUIDs) {
@@ -62,8 +67,8 @@ public Entity(EntityUID uid, Set<EntityUID> parentsEUIDs) {
6267
/**
6368
* Create an entity from an EntityUIDs, a map of attributes, and a set of parent EntityUIDs.
6469
*
65-
* @param uid EUID of the Entity.
66-
* @param attributes Key/Value map of attributes.
70+
* @param uid EUID of the Entity.
71+
* @param attributes Key/Value map of attributes.
6772
* @param parentsEUIDs Set of parent entities' EUIDs.
6873
*/
6974
public Entity(EntityUID uid, Map<String, Value> attributes, Set<EntityUID> parentsEUIDs) {
@@ -73,10 +78,10 @@ public Entity(EntityUID uid, Map<String, Value> attributes, Set<EntityUID> paren
7378
/**
7479
* Create an entity from an EntityUIDs, a map of attributes, a set of parent EntityUIDs, and a map of tags.
7580
*
76-
* @param uid EUID of the Entity.
77-
* @param attributes Key/Value map of attributes.
81+
* @param uid EUID of the Entity.
82+
* @param attributes Key/Value map of attributes.
7883
* @param parentsEUIDs Set of parent entities' EUIDs.
79-
* @param tags Key/Value map of tags.
84+
* @param tags Key/Value map of tags.
8085
*/
8186
public Entity(EntityUID uid, Map<String, Value> attributes, Set<EntityUID> parentsEUIDs, Map<String, Value> tags) {
8287
this.attrs = new HashMap<>(attributes);
@@ -87,8 +92,9 @@ public Entity(EntityUID uid, Map<String, Value> attributes, Set<EntityUID> paren
8792

8893
/**
8994
* Get the value for the given attribute, or null if not present.
90-
*
95+
*
9196
* @param attribute Attribute key
97+
*
9298
* @return Attribute value for the given key or null if not present
9399
* @throws IllegalArgumentException if attribute is null
94100
*/
@@ -103,32 +109,26 @@ public Value getAttr(String attribute) {
103109
public String toString() {
104110
String parentStr = "";
105111
if (!parentsEUIDs.isEmpty()) {
106-
List<String> parentStrs = new ArrayList<String>(parentsEUIDs.stream()
107-
.map(euid -> euid.toString()).collect(Collectors.toList()));
112+
List<String> parentStrs = new ArrayList<String>(
113+
parentsEUIDs.stream().map(euid -> euid.toString()).collect(Collectors.toList()));
108114
parentStr = "\n\tparents:\n\t\t" + String.join("\n\t\t", parentStrs);
109115
}
110116
String attributeStr = "";
111117
if (!attrs.isEmpty()) {
112-
attributeStr =
113-
"\n\tattrs:\n\t\t"
114-
+ attrs.entrySet().stream()
115-
.map(e -> e.getKey() + ": " + e.getValue())
116-
.collect(Collectors.joining("\n\t\t"));
118+
attributeStr = "\n\tattrs:\n\t\t" + attrs.entrySet().stream().map(e -> e.getKey() + ": " + e.getValue())
119+
.collect(Collectors.joining("\n\t\t"));
117120
}
118121
String tagsStr = "";
119122
if (!tags.isEmpty()) {
120-
tagsStr =
121-
"\n\ttags:\n\t\t"
122-
+ tags.entrySet().stream()
123-
.map(e -> e.getKey() + ": " + e.getValue())
124-
.collect(Collectors.joining("\n\t\t"));
123+
tagsStr = "\n\ttags:\n\t\t" + tags.entrySet().stream().map(e -> e.getKey() + ": " + e.getValue())
124+
.collect(Collectors.joining("\n\t\t"));
125125
}
126126
return euid.toString() + parentStr + attributeStr + tagsStr;
127127
}
128128

129-
130129
/**
131130
* Get the entity uid
131+
*
132132
* @return Entity UID
133133
*/
134134
public EntityUID getEUID() {
@@ -137,6 +137,7 @@ public EntityUID getEUID() {
137137

138138
/**
139139
* Get this Entity's parents
140+
*
140141
* @return the set of parent EntityUIDs
141142
*/
142143
public Set<EntityUID> getParents() {
@@ -145,9 +146,36 @@ public Set<EntityUID> getParents() {
145146

146147
/**
147148
* Get this Entity's tags
149+
*
148150
* @return the map of tags
149151
*/
150152
public Map<String, Value> getTags() {
151153
return tags;
152154
}
155+
156+
/**
157+
* Parse Entity from a JSON string
158+
*
159+
* @param jsonString The JSON string representation of an Entity
160+
*
161+
* @return Entity object parsed from the JSON string
162+
* @throws JsonProcessingException if the JSON string cannot be parsed into an Entity
163+
*/
164+
public static Entity parse(String jsonString) throws JsonProcessingException {
165+
return objectReader().forType(Entity.class).readValue(jsonString);
166+
}
167+
168+
/**
169+
* Parse Entity from a file containing JSON representation of an Entity
170+
*
171+
* @param filePath Path to the file containing Entity JSON
172+
*
173+
* @return Entity object parsed from the file contents
174+
* @throws IOException if there is an error reading the file
175+
* @throws JsonProcessingException if the file contents cannot be parsed into an Entity
176+
*/
177+
public static Entity parse(Path filePath) throws IOException, JsonProcessingException {
178+
String jsonString = Files.readString(filePath);
179+
return parse(jsonString);
180+
}
153181
}
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/*
2+
* Copyright Cedar Contributors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.cedarpolicy.serializer;
18+
19+
import com.cedarpolicy.model.exception.InvalidValueDeserializationException;
20+
import com.cedarpolicy.value.Value;
21+
import com.cedarpolicy.value.EntityUID;
22+
import com.cedarpolicy.model.entity.Entity;
23+
24+
import com.fasterxml.jackson.core.JsonParser;
25+
import com.fasterxml.jackson.databind.DeserializationContext;
26+
import com.fasterxml.jackson.databind.JsonDeserializer;
27+
import com.fasterxml.jackson.databind.JsonNode;
28+
import com.fasterxml.jackson.databind.ObjectMapper;
29+
30+
import java.io.IOException;
31+
import java.util.Map;
32+
import java.util.HashMap;
33+
import java.util.Set;
34+
import java.util.Spliterator;
35+
import java.util.Spliterators;
36+
import java.util.stream.Collectors;
37+
import java.util.stream.StreamSupport;
38+
39+
/**
40+
* Deserialize Json to Entity.
41+
*/
42+
public class EntityDeserializer extends JsonDeserializer<Entity> {
43+
44+
/**
45+
* Deserializes a JSON input into an Entity object.
46+
*
47+
* @param parser The JsonParser providing the JSON input to deserialize
48+
* @param context The deserialization context
49+
*
50+
* @return An Entity object constructed from the JSON input
51+
* @throws IOException If there is an error reading from the JsonParser
52+
* @throws InvalidValueDeserializationException If the JSON input is invalid or missing required fields
53+
*/
54+
@Override
55+
public Entity deserialize(JsonParser parser, DeserializationContext context)
56+
throws IOException, InvalidValueDeserializationException {
57+
final JsonNode node = parser.getCodec().readTree(parser);
58+
final ObjectMapper mapper = (ObjectMapper) parser.getCodec();
59+
60+
EntityUID euid;
61+
if (node.has("uid")) {
62+
JsonNode uidNode = node.get("uid");
63+
euid = parseEntityUID(parser, uidNode);
64+
} else {
65+
String msg = "\"uid\" not found";
66+
throw new InvalidValueDeserializationException(parser, msg, node.asToken(), Entity.class);
67+
}
68+
69+
Map<String, Value> attrs;
70+
if (node.has("attrs")) {
71+
JsonNode attrsNode = node.get("attrs");
72+
if (attrsNode.isObject()) {
73+
attrs = parseValueMap(mapper, attrsNode);
74+
} else {
75+
String msg = "\"attrs\" must be a JSON object";
76+
throw new InvalidValueDeserializationException(parser, msg, attrsNode.asToken(), Entity.class);
77+
}
78+
} else {
79+
String msg = "\"attrs\" not found";
80+
throw new InvalidValueDeserializationException(parser, msg, node.asToken(), Entity.class);
81+
}
82+
83+
Set<EntityUID> parentEUIDs;
84+
if (node.has("parents")) {
85+
JsonNode parentsNode = node.get("parents");
86+
if (parentsNode.isArray()) {
87+
parentEUIDs = StreamSupport.stream(parentsNode.spliterator(), false).map(parentNode -> {
88+
try {
89+
return parseEntityUID(parser, parentNode);
90+
} catch (InvalidValueDeserializationException e) {
91+
throw new RuntimeException(e);
92+
}
93+
}).collect(Collectors.toSet());
94+
} else {
95+
String msg = "\"parents\" field must be a JSON array";
96+
throw new InvalidValueDeserializationException(parser, msg, parentsNode.asToken(), Entity.class);
97+
}
98+
} else {
99+
String msg = "\"parents\" not found";
100+
throw new InvalidValueDeserializationException(parser, msg, node.asToken(), Entity.class);
101+
}
102+
103+
Map<String, Value> tags = new HashMap<>();
104+
if (node.has("tags")) {
105+
JsonNode tagsNode = node.get("tags");
106+
if (tagsNode.isObject()) {
107+
tags.putAll(parseValueMap(mapper, tagsNode));
108+
} else {
109+
String msg = "\"tags\" must be a JSON object";
110+
throw new InvalidValueDeserializationException(parser, msg, tagsNode.asToken(), Entity.class);
111+
}
112+
}
113+
return new Entity(euid, attrs, parentEUIDs, tags);
114+
}
115+
116+
/**
117+
* Parses a JSON node into an EntityUID object.
118+
*
119+
* @param parser The JsonParser used for error reporting
120+
* @param entityUIDJson The JsonNode containing the entity UID data to parse. Must have "type" and "id" fields.
121+
*
122+
* @return An EntityUID object constructed from the JSON data
123+
* @throws InvalidValueDeserializationException if the required fields are missing or invalid
124+
*/
125+
private EntityUID parseEntityUID(JsonParser parser, JsonNode entityUIDJson)
126+
throws InvalidValueDeserializationException {
127+
if (entityUIDJson.has("type") && entityUIDJson.has("id")) {
128+
JsonEUID jsonEuid = new JsonEUID(entityUIDJson.get("type").asText(), entityUIDJson.get("id").asText());
129+
return EntityUID.parseFromJson(jsonEuid).get();
130+
} else {
131+
String msg = "\"type\" or \"id\" not found";
132+
throw new InvalidValueDeserializationException(parser, msg, entityUIDJson.asToken(), Entity.class);
133+
}
134+
}
135+
136+
/**
137+
* Parses a JSON node containing key-value pairs into a Map of String to Value objects.
138+
*
139+
* @param mapper The ObjectMapper used to convert JSON nodes to Value objects
140+
* @param valueMapJson The JsonNode containing the key-value pairs to parse
141+
*
142+
* @return A Map where keys are Strings and values are Value objects
143+
* @throws RuntimeException if there is an error converting a JSON node to a Value
144+
*/
145+
private Map<String, Value> parseValueMap(ObjectMapper mapper, JsonNode valueMapJson) {
146+
Map<String, Value> valueMap = StreamSupport
147+
.stream(Spliterators.spliteratorUnknownSize(valueMapJson.fields(), Spliterator.ORDERED), false)
148+
.collect(Collectors.toMap(Map.Entry::getKey, entry -> {
149+
try {
150+
return mapper.treeToValue(entry.getValue(), Value.class);
151+
} catch (IOException e) {
152+
throw new RuntimeException(e);
153+
}
154+
}));
155+
return valueMap;
156+
}
157+
}

0 commit comments

Comments
 (0)