From fe04e5d789754f96b930580bf576eeb558854c18 Mon Sep 17 00:00:00 2001 From: John Brain Date: Tue, 28 Jan 2025 19:44:06 +0000 Subject: [PATCH 01/10] Add getJson methods to Entity Signed-off-by: John Brain --- .../com/cedarpolicy/model/entity/Entity.java | 27 +++ .../java/com/cedarpolicy/EntityTests.java | 207 +++++++++++++++++- CedarJavaFFI/src/interface.rs | 29 ++- CedarJavaFFI/src/jmap.rs | 83 +++++++ CedarJavaFFI/src/lib.rs | 1 + CedarJavaFFI/src/objects.rs | 158 ++++++++++++- 6 files changed, 500 insertions(+), 5 deletions(-) create mode 100644 CedarJavaFFI/src/jmap.rs 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..2c0b31f2 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/model/entity/Entity.java +++ b/CedarJava/src/main/java/com/cedarpolicy/model/entity/Entity.java @@ -18,6 +18,11 @@ import com.cedarpolicy.value.EntityUID; import com.cedarpolicy.value.Value; +import com.cedarpolicy.model.exception.InternalException; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.core.JsonProcessingException; import java.util.*; import java.util.stream.Collectors; @@ -29,6 +34,8 @@ * entities, and zero or more tags. */ public class Entity { + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private final EntityUID euid; /** Key/Value attribute map. */ @@ -66,6 +73,16 @@ public Entity(EntityUID uid, Map attributes, Set paren this.tags = new HashMap<>(tags); } + public String toJsonString() throws InternalException, NullPointerException { + String entityJsonStr = toJsonEntityJni(this); + return entityJsonStr; + } + + public JsonNode toJsonValue() throws InternalException, NullPointerException, JsonProcessingException { + String entityJsonStr = this.toJsonString(); + return OBJECT_MAPPER.readTree(entityJsonStr); + } + /** * Get the value for the given attribute, or null if not present. * @@ -116,6 +133,14 @@ public EntityUID getEUID() { return euid; } + /** + * Get the Entity's attributes + * @return the map of attributes + */ + public Map getAttributes() { + return attrs; + } + /** * Get this Entity's parents * @return the set of parent EntityUIDs @@ -131,4 +156,6 @@ public Set getParents() { public Map getTags() { return tags; } + + private static native String toJsonEntityJni(Entity entity) throws InternalException, NullPointerException; } diff --git a/CedarJava/src/test/java/com/cedarpolicy/EntityTests.java b/CedarJava/src/test/java/com/cedarpolicy/EntityTests.java index 94c78379..fd329acb 100644 --- a/CedarJava/src/test/java/com/cedarpolicy/EntityTests.java +++ b/CedarJava/src/test/java/com/cedarpolicy/EntityTests.java @@ -14,19 +14,23 @@ * limitations under the License. */ - package com.cedarpolicy; +package com.cedarpolicy; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertThrows; +import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; import com.cedarpolicy.value.*; import com.cedarpolicy.model.entity.Entity; +import com.fasterxml.jackson.databind.JsonNode; public class EntityTests { @@ -49,4 +53,205 @@ public void getAttrTests() { // Test key not found assertEquals(principal.getAttr("decimalAttr"), null); } + + @Test + public void getAttributesTests() { + PrimString stringAttr = new PrimString("stringAttrValue"); + HashMap attrs = new HashMap<>(); + attrs.put("stringAttr", stringAttr); + EntityTypeName principalType = EntityTypeName.parse("User").get(); + Entity principal = new Entity(principalType.of("Alice"), attrs, new HashSet<>()); + + // Test getting attribute + assertEquals(principal.getAttributes(), attrs); + } + + @Test + public void toJsonTests() { + PrimString stringAttr = new PrimString("stringAttrValue"); + HashMap attrs = new HashMap<>(); + attrs.put("stringAttr", stringAttr); + + EntityTypeName principalType = EntityTypeName.parse("User").get(); + + HashSet parents = new HashSet(); + parents.add(principalType.of("Bob")); + Entity principal = new Entity(principalType.of("Alice"), attrs, parents); + JsonNode entityJson = Assertions.assertDoesNotThrow(() -> { + return principal.toJsonValue(); + }); + + assertEquals(entityJson.toString(), + "{\"uid\":{\"type\":\"User\",\"id\":\"Alice\"}," + + "\"attrs\":{\"stringAttr\":\"stringAttrValue\"}," + + "\"parents\":[{\"type\":\"User\",\"id\":\"Bob\"}]}"); + } + + @Test + public void toJsonAllTypesTests() { + EntityTypeName principalType = EntityTypeName.parse("User").get(); + EntityUID aliceType = principalType.of("Alice"); + + HashMap attrs = new HashMap<>(); + PrimBool boolAttr = new PrimBool(false); + attrs.put("boolAttr", boolAttr); + + final Entity principalWithBool = new Entity(aliceType, attrs, new HashSet<>()); + JsonNode entityJson = Assertions.assertDoesNotThrow(() -> { + return principalWithBool.toJsonValue(); + }); + + assertEquals(entityJson.toString(), + "{\"uid\":{\"type\":\"User\",\"id\":\"Alice\"}," + + "\"attrs\":{\"boolAttr\":false}," + + "\"parents\":[]}"); + + attrs = new HashMap<>(); + PrimLong longAttr = new PrimLong(5); + attrs.put("longAttr", longAttr); + + final Entity principalWithLong = new Entity(aliceType, attrs, new HashSet<>()); + entityJson = Assertions.assertDoesNotThrow(() -> { + return principalWithLong.toJsonValue(); + }); + + assertEquals(entityJson.toString(), + "{\"uid\":{\"type\":\"User\",\"id\":\"Alice\"}," + + "\"attrs\":{\"longAttr\":5}," + + "\"parents\":[]}"); + + attrs = new HashMap<>(); + IpAddress ipAttr = new IpAddress("0.1.2.3"); + attrs.put("ipAttr", ipAttr); + + final Entity principalWithIpAddress = new Entity(aliceType, attrs, new HashSet<>()); + entityJson = Assertions.assertDoesNotThrow(() -> { + return principalWithIpAddress.toJsonValue(); + }); + + assertEquals(entityJson.toString(), + "{\"uid\":{\"type\":\"User\",\"id\":\"Alice\"}," + + "\"attrs\":{\"ipAttr\":{\"__extn\":{\"fn\":\"ip\",\"arg\":\"0.1.2.3\"}}}," + + "\"parents\":[]}"); + + attrs = new HashMap<>(); + EntityTypeName typeName = EntityTypeName.parse("User").get(); + EntityIdentifier id = new EntityIdentifier("testId"); + + EntityUID entityAttr = new EntityUID(typeName, id); + attrs.put("entityAttr", entityAttr); + + final Entity principalWithEntity = new Entity(aliceType, attrs, new HashSet<>()); + entityJson = Assertions.assertDoesNotThrow(() -> { + return principalWithEntity.toJsonValue(); + }); + + assertEquals(entityJson.toString(), + "{\"uid\":{\"type\":\"User\",\"id\":\"Alice\"}," + + "\"attrs\":{\"entityAttr\":{\"__entity\":{\"type\":\"User\",\"id\":\"testId\"}}}," + + "\"parents\":[]}"); + + attrs = new HashMap<>(); + Decimal decimalAttr = new Decimal("1.234"); + attrs.put("decimalAttr", decimalAttr); + + final Entity principalWithDecimal = new Entity(aliceType, attrs, + new HashSet<>()); + entityJson = Assertions.assertDoesNotThrow(() -> { + return principalWithDecimal.toJsonValue(); + }); + + assertEquals(entityJson.toString(), + "{\"uid\":{\"type\":\"User\",\"id\":\"Alice\"}," + + "\"attrs\":{\"decimalAttr\":{\"__extn\":{\"fn\":\"decimal\",\"arg\":\"1.234\"}}}," + + "\"parents\":[]}"); + + attrs = new HashMap<>(); + List valueList = new ArrayList(); + valueList.add(boolAttr); + CedarList listAttr = new CedarList(valueList); + attrs.put("listAttr", listAttr); + + final Entity principalWithList = new Entity(aliceType, attrs, new HashSet<>()); + entityJson = Assertions.assertDoesNotThrow(() -> { + return principalWithList.toJsonValue(); + }); + + assertEquals(entityJson.toString(), + "{\"uid\":{\"type\":\"User\",\"id\":\"Alice\"}," + + "\"attrs\":{\"listAttr\":[false]}," + + "\"parents\":[]}"); + + attrs = new HashMap<>(); + HashMap valueMap = new HashMap(); + valueMap.put("boolAttr", boolAttr); + CedarMap mapAttr = new CedarMap(valueMap); + attrs.put("mapAttr", mapAttr); + + final Entity principalWithMap = new Entity(aliceType, attrs, new HashSet<>()); + entityJson = Assertions.assertDoesNotThrow(() -> { + return principalWithMap.toJsonValue(); + }); + + assertEquals(entityJson.toString(), + "{\"uid\":{\"type\":\"User\",\"id\":\"Alice\"}," + + "\"attrs\":{\"mapAttr\":{\"boolAttr\":false}}," + + "\"parents\":[]}"); + } + + @Test + public void toJsonWithTagsTests() { + PrimString stringAttr = new PrimString("stringAttrValue"); + HashMap attrs = new HashMap<>(); + attrs.put("stringAttr", stringAttr); + + EntityTypeName principalType = EntityTypeName.parse("User").get(); + + HashSet parents = new HashSet(); + parents.add(principalType.of("Bob")); + + PrimString longTag = new PrimString("longTagValue"); + HashMap tags = new HashMap<>(); + tags.put("tag", longTag); + + Entity principal = new Entity(principalType.of("Alice"), attrs, parents, tags); + + JsonNode entityJson = Assertions.assertDoesNotThrow(() -> { + return principal.toJsonValue(); + }); + + assertEquals(entityJson.toString(), + "{\"uid\":{\"type\":\"User\",\"id\":\"Alice\"}," + + "\"attrs\":{\"stringAttr\":\"stringAttrValue\"}," + + "\"parents\":[{\"type\":\"User\",\"id\":\"Bob\"}]," + + "\"tags\":{\"tag\":\"longTagValue\"}}"); + } + + @Test + public void toJsonStringTests() { + PrimString stringAttr = new PrimString("stringAttrValue"); + HashMap attrs = new HashMap<>(); + attrs.put("stringAttr", stringAttr); + + EntityTypeName principalType = EntityTypeName.parse("User").get(); + + HashSet parents = new HashSet(); + parents.add(principalType.of("Bob")); + + PrimString longTag = new PrimString("longTagValue"); + HashMap tags = new HashMap<>(); + tags.put("tag", longTag); + + Entity principal = new Entity(principalType.of("Alice"), attrs, parents, tags); + + String entityJson = Assertions.assertDoesNotThrow(() -> { + return principal.toJsonString(); + }); + + assertEquals(entityJson, + "{\"uid\":{\"type\":\"User\",\"id\":\"Alice\"}," + + "\"attrs\":{\"stringAttr\":\"stringAttrValue\"}," + + "\"parents\":[{\"type\":\"User\",\"id\":\"Bob\"}]," + + "\"tags\":{\"tag\":\"longTagValue\"}}"); + } } diff --git a/CedarJavaFFI/src/interface.rs b/CedarJavaFFI/src/interface.rs index 94e30491..dc610fb0 100644 --- a/CedarJavaFFI/src/interface.rs +++ b/CedarJavaFFI/src/interface.rs @@ -32,7 +32,7 @@ use serde::{Deserialize, Serialize}; use serde_json::{from_str, Value}; use std::{error::Error, str::FromStr, thread}; -use crate::objects::JFormatterConfig; +use crate::objects::{JEntity, JFormatterConfig}; use crate::{ answer::Answer, jset::Set, @@ -461,6 +461,33 @@ fn from_json_internal<'a>( } } +#[jni_fn("com.cedarpolicy.model.entity.Entity")] +pub fn toJsonEntityJni<'a>( + mut env: JNIEnv<'a>, + _: JClass, + obj: JObject<'a> +) -> jvalue { + let entity = match JEntity::cast(&mut env, obj) { + Ok(v) => v, + Err(e) => return jni_failed(&mut env, e.as_ref()), + }; + + match to_json_entity_internal(&mut env, entity) { + Ok(v) => v.as_jni(), + Err(e) => jni_failed(&mut env, e.as_ref()), + } +} + +fn to_json_entity_internal<'a>( + env: &mut JNIEnv<'a>, + java_entity: JEntity<'a>, +) -> Result> { + let entity = java_entity.to_entity(env)?; + let entity_json = serde_json::to_string(&entity.to_json_value().unwrap())?; + Ok(JValueGen::Object(env.new_string(&entity_json)?.into())) +} + + #[jni_fn("com.cedarpolicy.value.EntityIdentifier")] pub fn getEntityIdentifierRepr<'a>(mut env: JNIEnv<'a>, _: JClass, obj: JObject<'a>) -> jvalue { match get_entity_identifier_repr_internal(&mut env, obj) { diff --git a/CedarJavaFFI/src/jmap.rs b/CedarJavaFFI/src/jmap.rs new file mode 100644 index 00000000..ffc050a6 --- /dev/null +++ b/CedarJavaFFI/src/jmap.rs @@ -0,0 +1,83 @@ +/* + * 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. + */ + +use std::{marker::PhantomData}; + +use crate::{objects::Object, utils::Result}; +use jni::{ + objects::{JObject, JValueGen}, + JNIEnv, +}; + +/// Typed wrapper for Java maps +/// (java.util.Map) +#[derive(Debug)] +pub struct Map<'a, T, U> { + /// Underlying Java object + obj: JObject<'a>, + /// ZST for tracking key type info + key_marker: PhantomData, + /// ZST for tracking value type info + value_marker: PhantomData, +} + +impl<'a, T: Object<'a>, U: Object<'a>> Map<'a, T, U> { + /// Construct an empty hash map, which will serve as a semap + pub fn new(env: &mut JNIEnv<'a>) -> Result { + let obj = env.new_object("java/util/HashMap", "()V", &[])?; + + Ok(Self { + obj, + key_marker: PhantomData, + value_marker: PhantomData, + }) + } + + /// Get a value mapped to a key + pub fn get(&mut self, env: &mut JNIEnv<'a>, k: T) -> Result> { + let key = JValueGen::Object(k.as_ref()); + let key = env + .call_method( + &self.obj, + "get", + "(Ljava/lang/Object;)Ljava/lang/Object;", + &[key], + )? + .l()?; + Ok(key) + } + + /// Put a key-value pair to the map + pub fn put(&mut self, env: &mut JNIEnv<'a>, k: T, v: U) -> Result> { + let key = JValueGen::Object(k.as_ref()); + let value = JValueGen::Object(v.as_ref()); + let value = env + .call_method( + &self.obj, + "put", + "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", + &[key, value], + )? + .l()?; + Ok(value) + } +} + +impl<'a, T, U> AsRef> for Map<'a, T, U> { + fn as_ref(&self) -> &JObject<'a> { + &self.obj + } +} diff --git a/CedarJavaFFI/src/lib.rs b/CedarJavaFFI/src/lib.rs index 11f0b6f8..52f9a1f5 100644 --- a/CedarJavaFFI/src/lib.rs +++ b/CedarJavaFFI/src/lib.rs @@ -18,6 +18,7 @@ mod answer; mod interface; mod jlist; +mod jmap; mod jset; mod objects; mod tests; diff --git a/CedarJavaFFI/src/objects.rs b/CedarJavaFFI/src/objects.rs index 5aa24ce5..ad62e49d 100644 --- a/CedarJavaFFI/src/objects.rs +++ b/CedarJavaFFI/src/objects.rs @@ -18,12 +18,12 @@ use crate::{ jlist::{jstr_list_to_rust_vec, List}, utils::{assert_is_class, get_object_ref, Result}, }; -use std::{marker::PhantomData, str::FromStr}; +use std::{collections::{HashMap, HashSet}, marker::PhantomData, str::FromStr}; -use cedar_policy::{EntityId, EntityTypeName, EntityUid}; +use cedar_policy::{Entity, EntityId, EntityTypeName, EntityUid, RestrictedExpression}; use cedar_policy_formatter::Config; use jni::{ - objects::{JObject, JString, JValueGen, JValueOwned}, + objects::{JObject, JObjectArray, JString, JValueGen, JValueOwned}, sys::jvalue, JNIEnv, }; @@ -42,6 +42,121 @@ impl<'a> Object<'a> for JString<'a> { } } +/// Typed wrapper for Entity objects +/// (com.cedarpolicy.model.entity.Entity) +pub struct JEntity<'a> { + obj: JObject<'a>, +} + +impl<'a> JEntity<'a> { + pub fn to_entity(&self, env: &mut JNIEnv<'a>) -> Result { + let euid = self.entity_uid(env)?; + let parents = self.parents(env)?; + let attrs = self.restricted_expr_map(env, "getAttributes")?; + let tags = self.restricted_expr_map(env, "getTags")?; + + Ok(Entity::new_with_tags(euid, attrs, parents, tags)?) + } + + fn entity_uid(&self, env: &mut JNIEnv<'a>) -> Result { + let java_euid: JObject<'a> = env + .call_method( + &self.obj, + "getEUID", + "()Lcom/cedarpolicy/value/EntityUID;", + &[], + )? + .l()?; + let euid_java = JEntityUID::cast(env, java_euid)?; + euid_java.to_entity_uid(env) + } + + fn parents(&self, env: &mut JNIEnv<'a>) -> Result> { + let java_parents: JObject<'a> = env + .call_method(&self.obj, "getParents", "()Ljava/util/Set;", &[])? + .l()?; + + let mut parents = HashSet::new(); + + let java_parents_array: JObjectArray = env + .call_method(java_parents, "toArray", "()[Ljava/lang/Object;", &[])? + .l()? + .into(); + + let length = env.get_array_length(&java_parents_array)?; + for i in 0..length { + let java_parent = env.get_object_array_element(&java_parents_array, i)?; + let parent_euid = JEntityUID::cast(env, java_parent)?; + parents.insert(parent_euid.to_entity_uid(env)?); + } + Ok(parents) + } + + fn restricted_expr_map( + &self, + env: &mut JNIEnv<'a>, + method_name: &str, + ) -> Result> { + let java_obj_map: JObject<'a> = env + .call_method(&self.obj, method_name, "()Ljava/util/Map;", &[])? + .l()?; + + let mut map = HashMap::new(); + + let java_entry_set: JObjectArray = env + .call_method(&java_obj_map, "entrySet", "()Ljava/util/Set;", &[])? + .l()? + .into(); + + let java_entry_array: JObjectArray = env + .call_method(java_entry_set, "toArray", "()[Ljava/lang/Object;", &[])? + .l()? + .into(); + + let length = env.get_array_length(&java_entry_array)?; + + for i in 0..length { + let java_map_entry = env.get_object_array_element(&java_entry_array, i)?; + + let java_key: JObject = env + .call_method(&java_map_entry, "getKey", "()Ljava/lang/Object;", &[])? + .l()?; + + let java_value: JObject = env + .call_method(&java_map_entry, "getValue", "()Ljava/lang/Object;", &[])? + .l()?; + + let cedar_expr: JObject = env + .call_method(java_value, "toCedarExpr", "()Ljava/lang/String;", &[])? + .l()?; + + let cedar_expr_jstr = JString::cast(env, cedar_expr)?; + let cedar_expr_str: String = env.get_string(&cedar_expr_jstr)?.into(); + let restircted_expr = RestrictedExpression::from_str(cedar_expr_str.as_str())?; + + let key_jobject = JString::cast(env, java_key)?; + let key: String = env.get_string(&key_jobject)?.into(); + + map.insert(key, restircted_expr); + } + + Ok(map) + } +} + +impl<'a> Object<'a> for JEntity<'a> { + fn cast(env: &mut JNIEnv<'a>, obj: JObject<'a>) -> Result { + assert_is_class(env, &obj, "com/cedarpolicy/model/entity/Entity")?; + Ok(Self { obj }) + } +} + +impl<'a> AsRef> for JEntity<'a> { + fn as_ref(&self) -> &JObject<'a> { + &self.obj + } +} + /// Typed wrapper around EntityTypeNames /// (com.cedarpolicy.value.EntityTypeName) pub struct JEntityTypeName<'a> { @@ -314,6 +429,43 @@ impl<'a> JEntityUID<'a> { Err(_) => JOptional::empty(env), } } + + pub fn to_entity_uid(&self, env: &mut JNIEnv<'a>) -> Result { + let java_eid = env + .call_method( + &self.obj, + "getId", + "()Lcom/cedarpolicy/value/EntityIdentifier;", + &[], + )? + .l()?; + + let eid_id_jstr = env + .call_method(java_eid, "toString", "()Ljava/lang/String;", &[])? + .l()?; + + let eid_id_str: String = env.get_string(&JString::from(eid_id_jstr)).unwrap().into(); + let entity_id = EntityId::new(eid_id_str); + + let java_entity_type_name = env + .call_method( + &self.obj, + "getType", + "()Lcom/cedarpolicy/value/EntityTypeName;", + &[], + )? + .l()?; + + let entity_type_name_jstr = env + .call_method(java_entity_type_name, "toString", "()Ljava/lang/String;", &[])? + .l()?; + + let entity_type_name_str: String = env.get_string(&JString::from(entity_type_name_jstr))?.into(); + let entity_type_name = EntityTypeName::from_str(entity_type_name_str.as_str())?; + + let entity_uid = EntityUid::from_type_name_and_id(entity_type_name, entity_id); + Ok(entity_uid) + } } impl<'a> Object<'a> for JEntityUID<'a> { From 6448f7acb28090e7a9ab882020f74b1880f7d4fe Mon Sep 17 00:00:00 2001 From: John Brain Date: Tue, 28 Jan 2025 19:51:03 +0000 Subject: [PATCH 02/10] make getAttributes method private Signed-off-by: John Brain --- .../java/com/cedarpolicy/model/entity/Entity.java | 2 +- .../src/test/java/com/cedarpolicy/EntityTests.java | 12 ------------ 2 files changed, 1 insertion(+), 13 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 2c0b31f2..f963dab1 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/model/entity/Entity.java +++ b/CedarJava/src/main/java/com/cedarpolicy/model/entity/Entity.java @@ -137,7 +137,7 @@ public EntityUID getEUID() { * Get the Entity's attributes * @return the map of attributes */ - public Map getAttributes() { + private Map getAttributes() { return attrs; } diff --git a/CedarJava/src/test/java/com/cedarpolicy/EntityTests.java b/CedarJava/src/test/java/com/cedarpolicy/EntityTests.java index fd329acb..2680dcbb 100644 --- a/CedarJava/src/test/java/com/cedarpolicy/EntityTests.java +++ b/CedarJava/src/test/java/com/cedarpolicy/EntityTests.java @@ -54,18 +54,6 @@ public void getAttrTests() { assertEquals(principal.getAttr("decimalAttr"), null); } - @Test - public void getAttributesTests() { - PrimString stringAttr = new PrimString("stringAttrValue"); - HashMap attrs = new HashMap<>(); - attrs.put("stringAttr", stringAttr); - EntityTypeName principalType = EntityTypeName.parse("User").get(); - Entity principal = new Entity(principalType.of("Alice"), attrs, new HashSet<>()); - - // Test getting attribute - assertEquals(principal.getAttributes(), attrs); - } - @Test public void toJsonTests() { PrimString stringAttr = new PrimString("stringAttrValue"); From b39981f7c771bb0c418516c9ea9390d9d2fb4ed0 Mon Sep 17 00:00:00 2001 From: John Brain Date: Tue, 28 Jan 2025 20:13:54 +0000 Subject: [PATCH 03/10] Add documentation Signed-off-by: John Brain --- .../com/cedarpolicy/model/entity/Entity.java | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 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 f963dab1..9940486e 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/model/entity/Entity.java +++ b/CedarJava/src/main/java/com/cedarpolicy/model/entity/Entity.java @@ -73,12 +73,25 @@ public Entity(EntityUID uid, Map attributes, Set paren this.tags = new HashMap<>(tags); } - public String toJsonString() throws InternalException, NullPointerException { + /** + * Get the Entity's JSON string value. + * + * @return the Entity's JSON string value + * @throws InternalException if the Entity is unable to be parsed + */ + public String toJsonString() throws InternalException { String entityJsonStr = toJsonEntityJni(this); return entityJsonStr; } - public JsonNode toJsonValue() throws InternalException, NullPointerException, JsonProcessingException { + /** + * Get the Entity's JSON string value. + * + * @return the Entity's JSON string value + * @throws InternalException if the Entity is unable to be parsed + * @throws JsonProcessingException if the Entity JSON is unable to be processed + */ + public JsonNode toJsonValue() throws InternalException, JsonProcessingException { String entityJsonStr = this.toJsonString(); return OBJECT_MAPPER.readTree(entityJsonStr); } From 0c4d603bcac3bfdada0b8f94cba1c4c413eb7d3f Mon Sep 17 00:00:00 2001 From: John Brain Date: Tue, 28 Jan 2025 21:13:42 +0000 Subject: [PATCH 04/10] Improve Entity tests Signed-off-by: John Brain --- .../java/com/cedarpolicy/EntityTests.java | 148 ++++++++++++++---- 1 file changed, 118 insertions(+), 30 deletions(-) diff --git a/CedarJava/src/test/java/com/cedarpolicy/EntityTests.java b/CedarJava/src/test/java/com/cedarpolicy/EntityTests.java index 2680dcbb..800fca52 100644 --- a/CedarJava/src/test/java/com/cedarpolicy/EntityTests.java +++ b/CedarJava/src/test/java/com/cedarpolicy/EntityTests.java @@ -18,15 +18,15 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; -import static org.junit.jupiter.api.Assertions.assertEquals; - import com.cedarpolicy.value.*; import com.cedarpolicy.model.entity.Entity; @@ -75,6 +75,122 @@ public void toJsonTests() { + "\"parents\":[{\"type\":\"User\",\"id\":\"Bob\"}]}"); } + @Test + public void toJsonWithTagsTests() { + PrimString stringAttr = new PrimString("stringAttrValue"); + HashMap attrs = new HashMap<>(); + attrs.put("stringAttr", stringAttr); + + EntityTypeName principalType = EntityTypeName.parse("User").get(); + + HashSet parents = new HashSet(); + parents.add(principalType.of("Bob")); + + PrimString strTag = new PrimString("strTagValue"); + HashMap tags = new HashMap<>(); + tags.put("tag", strTag); + + Entity principal = new Entity(principalType.of("Alice"), attrs, parents, tags); + + JsonNode entityJson = Assertions.assertDoesNotThrow(() -> { + return principal.toJsonValue(); + }); + + assertEquals(entityJson.toString(), + "{\"uid\":{\"type\":\"User\",\"id\":\"Alice\"}," + + "\"attrs\":{\"stringAttr\":\"stringAttrValue\"}," + + "\"parents\":[{\"type\":\"User\",\"id\":\"Bob\"}]," + + "\"tags\":{\"tag\":\"strTagValue\"}}"); + } + + public void toJsonMultipleAttributesTests() { + HashMap attrs = new HashMap<>(); + PrimString stringAttr = new PrimString("stringAttrValue"); + attrs.put("stringAttr", stringAttr); + + PrimString stringAttr2 = new PrimString("stringAttrValue2"); + attrs.put("stringAttr2", stringAttr2); + + EntityTypeName principalType = EntityTypeName.parse("User").get(); + + HashSet parents = new HashSet(); + parents.add(principalType.of("Bob")); + Entity principal = new Entity(principalType.of("Alice"), attrs, parents); + JsonNode entityJson = Assertions.assertDoesNotThrow(() -> { + return principal.toJsonValue(); + }); + + String entityJsonStr = entityJson.toString(); + boolean entityJsonIsExpected = entityJsonStr.equals("{\"uid\":{\"type\":\"User\",\"id\":\"Alice\"}," + + "\"attrs\":{\"stringAttr\":\"stringAttrValue\",\"stringAttr2\":\"stringAttrValue2\"}," + + "\"parents\":[{\"type\":\"User\",\"id\":\"Bob\"}]}") + || entityJsonStr.equals("{\"uid\":{\"type\":\"User\",\"id\":\"Alice\"}," + + "\"attrs\":{\"stringAttr2\":\"stringAttrValue2\",\"stringAttr\":\"stringAttrValue\"}," + + "\"parents\":[{\"type\":\"User\",\"id\":\"Bob\"}]}"); + + assertTrue(entityJsonIsExpected, entityJsonStr); + } + + public void toJsonMultipleParentsTests() { + HashMap attrs = new HashMap<>(); + PrimString stringAttr = new PrimString("stringAttrValue"); + attrs.put("stringAttr", stringAttr); + + EntityTypeName principalType = EntityTypeName.parse("User").get(); + + HashSet parents = new HashSet(); + parents.add(principalType.of("Alice")); + parents.add(principalType.of("Bob")); + Entity principal = new Entity(principalType.of("Alice"), attrs, parents); + JsonNode entityJson = Assertions.assertDoesNotThrow(() -> { + return principal.toJsonValue(); + }); + + String entityJsonStr = entityJson.toString(); + boolean entityJsonIsExpected = entityJsonStr.equals("{\"uid\":{\"type\":\"User\",\"id\":\"Alice\"}," + + "\"attrs\":{\"stringAttr\":\"stringAttrValue\"}," + + "\"parents\":[{\"type\":\"User\",\"id\":\"Alice\"},{\"type\":\"User\",\"id\":\"Bob\"}]}") + || entityJsonStr.equals("{\"uid\":{\"type\":\"User\",\"id\":\"Alice\"}," + + "\"attrs\":{\"stringAttr\":\"stringAttrValue\"}," + + "\"parents\":[{\"type\":\"User\",\"id\":\"Bob\",{\"type\":\"User\",\"id\":\"Alice\"}}]}"); + + assertTrue(entityJsonIsExpected, entityJsonStr); + } + + public void toJsonMultipleTagsTests() { + HashMap attrs = new HashMap<>(); + PrimString stringAttr = new PrimString("stringAttrValue"); + attrs.put("stringAttr", stringAttr); + + EntityTypeName principalType = EntityTypeName.parse("User").get(); + + HashSet parents = new HashSet(); + parents.add(principalType.of("Alice")); + + HashMap tags = new HashMap<>(); + PrimString strTag = new PrimString("strTagValue"); + tags.put("tag", strTag); + PrimBool boolTag = new PrimBool(true); + tags.put("tag2", boolTag); + + Entity principal = new Entity(principalType.of("Alice"), attrs, parents, tags); + JsonNode entityJson = Assertions.assertDoesNotThrow(() -> { + return principal.toJsonValue(); + }); + + String entityJsonStr = entityJson.toString(); + boolean entityJsonIsExpected = entityJsonStr.equals("{\"uid\":{\"type\":\"User\",\"id\":\"Alice\"}," + + "\"attrs\":{\"stringAttr\":\"stringAttrValue\"}," + + "\"parents\":[{\"type\":\"User\",\"id\":\"Bob\"}]," + + "\"tags\":{\"tag\":\"strTagValue\",\"tag2\":\"true\"}}") + || entityJsonStr.equals("{\"uid\":{\"type\":\"User\",\"id\":\"Alice\"}," + + "\"attrs\":{\"stringAttr\":\"stringAttrValue\"}," + + "\"parents\":[{\"type\":\"User\",\"id\":\"Bob\"}]," + + "\"tags\":{\"tag2\":\"tue\",\"tag\":\"strTagValue\"}}"); + + assertTrue(entityJsonIsExpected, entityJsonStr); + } + @Test public void toJsonAllTypesTests() { EntityTypeName principalType = EntityTypeName.parse("User").get(); @@ -187,34 +303,6 @@ public void toJsonAllTypesTests() { + "\"parents\":[]}"); } - @Test - public void toJsonWithTagsTests() { - PrimString stringAttr = new PrimString("stringAttrValue"); - HashMap attrs = new HashMap<>(); - attrs.put("stringAttr", stringAttr); - - EntityTypeName principalType = EntityTypeName.parse("User").get(); - - HashSet parents = new HashSet(); - parents.add(principalType.of("Bob")); - - PrimString longTag = new PrimString("longTagValue"); - HashMap tags = new HashMap<>(); - tags.put("tag", longTag); - - Entity principal = new Entity(principalType.of("Alice"), attrs, parents, tags); - - JsonNode entityJson = Assertions.assertDoesNotThrow(() -> { - return principal.toJsonValue(); - }); - - assertEquals(entityJson.toString(), - "{\"uid\":{\"type\":\"User\",\"id\":\"Alice\"}," - + "\"attrs\":{\"stringAttr\":\"stringAttrValue\"}," - + "\"parents\":[{\"type\":\"User\",\"id\":\"Bob\"}]," - + "\"tags\":{\"tag\":\"longTagValue\"}}"); - } - @Test public void toJsonStringTests() { PrimString stringAttr = new PrimString("stringAttrValue"); From 9baff879bf0523272eefd1ae70ce9984de3d35e1 Mon Sep 17 00:00:00 2001 From: John Brain Date: Wed, 29 Jan 2025 15:40:02 +0000 Subject: [PATCH 05/10] Improve documentation and error handling Signed-off-by: John Brain --- .../com/cedarpolicy/model/entity/Entity.java | 17 ++-- CedarJavaFFI/src/interface.rs | 22 ++--- CedarJavaFFI/src/jmap.rs | 8 +- CedarJavaFFI/src/objects.rs | 82 +++++++++++++------ 4 files changed, 76 insertions(+), 53 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 9940486e..d96c0008 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/model/entity/Entity.java +++ b/CedarJava/src/main/java/com/cedarpolicy/model/entity/Entity.java @@ -77,21 +77,22 @@ public Entity(EntityUID uid, Map attributes, Set paren * Get the Entity's JSON string value. * * @return the Entity's JSON string value + * @throws NullPointerException if the Entity is null * @throws InternalException if the Entity is unable to be parsed */ - public String toJsonString() throws InternalException { - String entityJsonStr = toJsonEntityJni(this); - return entityJsonStr; + public String toJsonString() throws NullPointerException, InternalException { + return toJsonEntityJni(this); } /** - * Get the Entity's JSON string value. + * Get the Entity's JSON value. * - * @return the Entity's JSON string value + * @return the Entity's JSON value + * @throws NullPointerException if the Entity is null * @throws InternalException if the Entity is unable to be parsed * @throws JsonProcessingException if the Entity JSON is unable to be processed */ - public JsonNode toJsonValue() throws InternalException, JsonProcessingException { + public JsonNode toJsonValue() throws NullPointerException, InternalException, JsonProcessingException { String entityJsonStr = this.toJsonString(); return OBJECT_MAPPER.readTree(entityJsonStr); } @@ -148,7 +149,7 @@ public EntityUID getEUID() { /** * Get the Entity's attributes - * @return the map of attributes + * @return the attribute map */ private Map getAttributes() { return attrs; @@ -170,5 +171,5 @@ public Map getTags() { return tags; } - private static native String toJsonEntityJni(Entity entity) throws InternalException, NullPointerException; + private static native String toJsonEntityJni(Entity entity) throws NullPointerException, InternalException; } diff --git a/CedarJavaFFI/src/interface.rs b/CedarJavaFFI/src/interface.rs index dc610fb0..9c6176f9 100644 --- a/CedarJavaFFI/src/interface.rs +++ b/CedarJavaFFI/src/interface.rs @@ -462,16 +462,7 @@ fn from_json_internal<'a>( } #[jni_fn("com.cedarpolicy.model.entity.Entity")] -pub fn toJsonEntityJni<'a>( - mut env: JNIEnv<'a>, - _: JClass, - obj: JObject<'a> -) -> jvalue { - let entity = match JEntity::cast(&mut env, obj) { - Ok(v) => v, - Err(e) => return jni_failed(&mut env, e.as_ref()), - }; - +pub fn toJsonEntityJni<'a>(mut env: JNIEnv<'a>, _: JClass, entity: JEntity<'a>) -> jvalue { match to_json_entity_internal(&mut env, entity) { Ok(v) => v.as_jni(), Err(e) => jni_failed(&mut env, e.as_ref()), @@ -482,12 +473,15 @@ fn to_json_entity_internal<'a>( env: &mut JNIEnv<'a>, java_entity: JEntity<'a>, ) -> Result> { - let entity = java_entity.to_entity(env)?; - let entity_json = serde_json::to_string(&entity.to_json_value().unwrap())?; - Ok(JValueGen::Object(env.new_string(&entity_json)?.into())) + if java_entity.as_ref().is_null() { + raise_npe(env) + } else { + let entity = java_entity.to_entity(env)?; + let entity_json = serde_json::to_string(&entity.to_json_value()?)?; + Ok(JValueGen::Object(env.new_string(&entity_json)?.into())) + } } - #[jni_fn("com.cedarpolicy.value.EntityIdentifier")] pub fn getEntityIdentifierRepr<'a>(mut env: JNIEnv<'a>, _: JClass, obj: JObject<'a>) -> jvalue { match get_entity_identifier_repr_internal(&mut env, obj) { diff --git a/CedarJavaFFI/src/jmap.rs b/CedarJavaFFI/src/jmap.rs index ffc050a6..1672a66c 100644 --- a/CedarJavaFFI/src/jmap.rs +++ b/CedarJavaFFI/src/jmap.rs @@ -35,7 +35,7 @@ pub struct Map<'a, T, U> { } impl<'a, T: Object<'a>, U: Object<'a>> Map<'a, T, U> { - /// Construct an empty hash map, which will serve as a semap + /// Construct an empty hash map, which will serve as a map pub fn new(env: &mut JNIEnv<'a>) -> Result { let obj = env.new_object("java/util/HashMap", "()V", &[])?; @@ -49,7 +49,7 @@ impl<'a, T: Object<'a>, U: Object<'a>> Map<'a, T, U> { /// Get a value mapped to a key pub fn get(&mut self, env: &mut JNIEnv<'a>, k: T) -> Result> { let key = JValueGen::Object(k.as_ref()); - let key = env + let value = env .call_method( &self.obj, "get", @@ -57,10 +57,10 @@ impl<'a, T: Object<'a>, U: Object<'a>> Map<'a, T, U> { &[key], )? .l()?; - Ok(key) + Ok(value) } - /// Put a key-value pair to the map + /// Put a key-value pair into the map pub fn put(&mut self, env: &mut JNIEnv<'a>, k: T, v: U) -> Result> { let key = JValueGen::Object(k.as_ref()); let value = JValueGen::Object(v.as_ref()); diff --git a/CedarJavaFFI/src/objects.rs b/CedarJavaFFI/src/objects.rs index ad62e49d..edf284a7 100644 --- a/CedarJavaFFI/src/objects.rs +++ b/CedarJavaFFI/src/objects.rs @@ -18,7 +18,11 @@ use crate::{ jlist::{jstr_list_to_rust_vec, List}, utils::{assert_is_class, get_object_ref, Result}, }; -use std::{collections::{HashMap, HashSet}, marker::PhantomData, str::FromStr}; +use std::{ + collections::{HashMap, HashSet}, + marker::PhantomData, + str::FromStr, +}; use cedar_policy::{Entity, EntityId, EntityTypeName, EntityUid, RestrictedExpression}; use cedar_policy_formatter::Config; @@ -49,17 +53,19 @@ pub struct JEntity<'a> { } impl<'a> JEntity<'a> { + /// Converts the Java Entity into a Rust Entity pub fn to_entity(&self, env: &mut JNIEnv<'a>) -> Result { let euid = self.entity_uid(env)?; let parents = self.parents(env)?; - let attrs = self.restricted_expr_map(env, "getAttributes")?; - let tags = self.restricted_expr_map(env, "getTags")?; + let attrs = self.restricted_expr_map(env, JEntityMapType::Attributes)?; + let tags = self.restricted_expr_map(env, JEntityMapType::Tags)?; Ok(Entity::new_with_tags(euid, attrs, parents, tags)?) } + /// Get the Entity's uid fn entity_uid(&self, env: &mut JNIEnv<'a>) -> Result { - let java_euid: JObject<'a> = env + let euid_jobj: JObject<'a> = env .call_method( &self.obj, "getEUID", @@ -67,49 +73,55 @@ impl<'a> JEntity<'a> { &[], )? .l()?; - let euid_java = JEntityUID::cast(env, java_euid)?; - euid_java.to_entity_uid(env) + let java_euid = JEntityUID::cast(env, euid_jobj)?; + java_euid.to_entity_uid(env) } + /// Get the Entity's parents fn parents(&self, env: &mut JNIEnv<'a>) -> Result> { - let java_parents: JObject<'a> = env + let parents_jobj: JObject<'a> = env .call_method(&self.obj, "getParents", "()Ljava/util/Set;", &[])? .l()?; let mut parents = HashSet::new(); let java_parents_array: JObjectArray = env - .call_method(java_parents, "toArray", "()[Ljava/lang/Object;", &[])? + .call_method(parents_jobj, "toArray", "()[Ljava/lang/Object;", &[])? .l()? .into(); let length = env.get_array_length(&java_parents_array)?; for i in 0..length { - let java_parent = env.get_object_array_element(&java_parents_array, i)?; - let parent_euid = JEntityUID::cast(env, java_parent)?; - parents.insert(parent_euid.to_entity_uid(env)?); + let parent_jobj = env.get_object_array_element(&java_parents_array, i)?; + let java_parent_euid = JEntityUID::cast(env, parent_jobj)?; + parents.insert(java_parent_euid.to_entity_uid(env)?); } Ok(parents) } + // Get a map value from the Entity (attributes or tags) fn restricted_expr_map( &self, env: &mut JNIEnv<'a>, - method_name: &str, + map_type: JEntityMapType, ) -> Result> { - let java_obj_map: JObject<'a> = env + let method_name = match map_type { + JEntityMapType::Attributes => "getAttributes", + JEntityMapType::Tags => "getTags", + }; + + let jobj_map: JObject<'a> = env .call_method(&self.obj, method_name, "()Ljava/util/Map;", &[])? .l()?; let mut map = HashMap::new(); - let java_entry_set: JObjectArray = env - .call_method(&java_obj_map, "entrySet", "()Ljava/util/Set;", &[])? - .l()? - .into(); + let jobj_entry_set: JObject<'a> = env + .call_method(&jobj_map, "entrySet", "()Ljava/util/Set;", &[])? + .l()?; let java_entry_array: JObjectArray = env - .call_method(java_entry_set, "toArray", "()[Ljava/lang/Object;", &[])? + .call_method(jobj_entry_set, "toArray", "()[Ljava/lang/Object;", &[])? .l()? .into(); @@ -126,16 +138,16 @@ impl<'a> JEntity<'a> { .call_method(&java_map_entry, "getValue", "()Ljava/lang/Object;", &[])? .l()?; - let cedar_expr: JObject = env + let cedar_expr_jobj: JObject = env .call_method(java_value, "toCedarExpr", "()Ljava/lang/String;", &[])? .l()?; - let cedar_expr_jstr = JString::cast(env, cedar_expr)?; + let cedar_expr_jstr = JString::cast(env, cedar_expr_jobj)?; let cedar_expr_str: String = env.get_string(&cedar_expr_jstr)?.into(); let restircted_expr = RestrictedExpression::from_str(cedar_expr_str.as_str())?; - let key_jobject = JString::cast(env, java_key)?; - let key: String = env.get_string(&key_jobject)?.into(); + let key_jobj = JString::cast(env, java_key)?; + let key: String = env.get_string(&key_jobj)?.into(); map.insert(key, restircted_expr); } @@ -144,6 +156,11 @@ impl<'a> JEntity<'a> { } } +enum JEntityMapType { + Attributes, + Tags, +} + impl<'a> Object<'a> for JEntity<'a> { fn cast(env: &mut JNIEnv<'a>, obj: JObject<'a>) -> Result { assert_is_class(env, &obj, "com/cedarpolicy/model/entity/Entity")?; @@ -430,8 +447,10 @@ impl<'a> JEntityUID<'a> { } } + /// Convert the Java EntityUID into a rust EntityUid pub fn to_entity_uid(&self, env: &mut JNIEnv<'a>) -> Result { - let java_eid = env + // get the entity id from the JEntityUID + let eid_jobj = env .call_method( &self.obj, "getId", @@ -441,13 +460,14 @@ impl<'a> JEntityUID<'a> { .l()?; let eid_id_jstr = env - .call_method(java_eid, "toString", "()Ljava/lang/String;", &[])? + .call_method(eid_jobj, "toString", "()Ljava/lang/String;", &[])? .l()?; let eid_id_str: String = env.get_string(&JString::from(eid_id_jstr)).unwrap().into(); let entity_id = EntityId::new(eid_id_str); - let java_entity_type_name = env + // get the entity type name from the JEntityUID + let entity_type_name_jobj = env .call_method( &self.obj, "getType", @@ -457,12 +477,20 @@ impl<'a> JEntityUID<'a> { .l()?; let entity_type_name_jstr = env - .call_method(java_entity_type_name, "toString", "()Ljava/lang/String;", &[])? + .call_method( + entity_type_name_jobj, + "toString", + "()Ljava/lang/String;", + &[], + )? .l()?; - let entity_type_name_str: String = env.get_string(&JString::from(entity_type_name_jstr))?.into(); + let entity_type_name_str: String = env + .get_string(&JString::from(entity_type_name_jstr))? + .into(); let entity_type_name = EntityTypeName::from_str(entity_type_name_str.as_str())?; + // create the entity uid let entity_uid = EntityUid::from_type_name_and_id(entity_type_name, entity_id); Ok(entity_uid) } From 28f3bb654b8162a89cb65ee697583d2f3c1a0aaf Mon Sep 17 00:00:00 2001 From: John Brain Date: Wed, 29 Jan 2025 16:55:49 +0000 Subject: [PATCH 06/10] Refactor conversion helper methods Signed-off-by: John Brain --- CedarJavaFFI/src/objects.rs | 49 +++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/CedarJavaFFI/src/objects.rs b/CedarJavaFFI/src/objects.rs index edf284a7..1e1f76b0 100644 --- a/CedarJavaFFI/src/objects.rs +++ b/CedarJavaFFI/src/objects.rs @@ -255,6 +255,20 @@ impl<'a> JEntityTypeName<'a> { Err(_) => JOptional::empty(env), } } + + /// Decode the underlying EntityTypeName Java object into the Rust EntityTypeName struct + pub fn to_entity_type_name(&self, env: &mut JNIEnv<'a>) -> Result { + let entity_type_name_jstr = env + .call_method(&self.obj, "toString", "()Ljava/lang/String;", &[])? + .l()?; + + let entity_type_name_str: String = env + .get_string(&JString::from(entity_type_name_jstr))? + .into(); + let entity_type_name = EntityTypeName::from_str(entity_type_name_str.as_str())?; + + Ok(entity_type_name) + } } impl<'a> Object<'a> for JEntityTypeName<'a> { @@ -382,6 +396,18 @@ impl<'a> JEntityId<'a> { self.id.clone() } + /// Decode the underlying EntityId Java object into the Rust EntityId struct + pub fn to_entity_id(&self, env: &mut JNIEnv<'a>) -> Result { + let eid_id_jstr = env + .call_method(&self.obj, "toString", "()Ljava/lang/String;", &[])? + .l()?; + + let eid_id_str: String = env.get_string(&JString::from(eid_id_jstr)).unwrap().into(); + let entity_id = EntityId::new(eid_id_str); + + Ok(entity_id) + } + /// Decode the object into its string representation pub fn get_string_repr(&self) -> String { self.id.escaped().to_string() @@ -459,12 +485,8 @@ impl<'a> JEntityUID<'a> { )? .l()?; - let eid_id_jstr = env - .call_method(eid_jobj, "toString", "()Ljava/lang/String;", &[])? - .l()?; - - let eid_id_str: String = env.get_string(&JString::from(eid_id_jstr)).unwrap().into(); - let entity_id = EntityId::new(eid_id_str); + let java_eid = JEntityId::cast(env, eid_jobj)?; + let entity_id = java_eid.to_entity_id(env)?; // get the entity type name from the JEntityUID let entity_type_name_jobj = env @@ -476,19 +498,8 @@ impl<'a> JEntityUID<'a> { )? .l()?; - let entity_type_name_jstr = env - .call_method( - entity_type_name_jobj, - "toString", - "()Ljava/lang/String;", - &[], - )? - .l()?; - - let entity_type_name_str: String = env - .get_string(&JString::from(entity_type_name_jstr))? - .into(); - let entity_type_name = EntityTypeName::from_str(entity_type_name_str.as_str())?; + let java_entity_type_name = JEntityTypeName::cast(env, entity_type_name_jobj)?; + let entity_type_name = java_entity_type_name.to_entity_type_name(env)?; // create the entity uid let entity_uid = EntityUid::from_type_name_and_id(entity_type_name, entity_id); From 61c4ca3dee8cb597f403256aacac64e7da1d87e6 Mon Sep 17 00:00:00 2001 From: John Brain Date: Thu, 30 Jan 2025 15:00:27 +0000 Subject: [PATCH 07/10] Enhance tests Signed-off-by: John Brain --- .../java/com/cedarpolicy/EntityTests.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CedarJava/src/test/java/com/cedarpolicy/EntityTests.java b/CedarJava/src/test/java/com/cedarpolicy/EntityTests.java index 800fca52..24b267fe 100644 --- a/CedarJava/src/test/java/com/cedarpolicy/EntityTests.java +++ b/CedarJava/src/test/java/com/cedarpolicy/EntityTests.java @@ -73,6 +73,27 @@ public void toJsonTests() { "{\"uid\":{\"type\":\"User\",\"id\":\"Alice\"}," + "\"attrs\":{\"stringAttr\":\"stringAttrValue\"}," + "\"parents\":[{\"type\":\"User\",\"id\":\"Bob\"}]}"); + + Entity parentlessPrincipal = new Entity(principalType.of("Alice"), attrs, new HashSet<>()); + JsonNode parentlessEntityJson = Assertions.assertDoesNotThrow(() -> { + return parentlessPrincipal.toJsonValue(); + }); + + assertEquals(parentlessEntityJson.toString(), + "{\"uid\":{\"type\":\"User\",\"id\":\"Alice\"}," + + "\"attrs\":{\"stringAttr\":\"stringAttrValue\"}," + + "\"parents\":[]}"); + + Entity principalWithEuid = new Entity(principalType.of("Alice"), new HashMap<>(), new HashSet<>()); + JsonNode entityWithEuidJson = Assertions.assertDoesNotThrow(() -> { + return principalWithEuid.toJsonValue(); + }); + + assertEquals(entityWithEuidJson.toString(), + "{\"uid\":{\"type\":\"User\",\"id\":\"Alice\"}," + + "\"attrs\":{}," + + "\"parents\":[]}"); + } @Test From f3ca0fbeaee38f9ef2e363177bf3165825dfc965 Mon Sep 17 00:00:00 2001 From: John Brain Date: Fri, 31 Jan 2025 14:47:42 +0000 Subject: [PATCH 08/10] Add write_to_json functionality Signed-off-by: John Brain --- .../com/cedarpolicy/model/entity/Entity.java | 18 +++++++++++ .../java/com/cedarpolicy/EntityTests.java | 32 +++++++++++++++++++ 2 files changed, 50 insertions(+) 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 d96c0008..eac07e7b 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/model/entity/Entity.java +++ b/CedarJava/src/main/java/com/cedarpolicy/model/entity/Entity.java @@ -22,8 +22,11 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; import com.fasterxml.jackson.core.JsonProcessingException; +import java.io.File; +import java.io.IOException; import java.util.*; import java.util.stream.Collectors; @@ -97,6 +100,21 @@ public JsonNode toJsonValue() throws NullPointerException, InternalException, Js return OBJECT_MAPPER.readTree(entityJsonStr); } + /** + * Dump the Entity into an entity JSON file. + * + * @param file the file to dump the Entity into + * @throws NullPointerException if the Entity is null + * @throws InternalException if the Entity is unable to be parsed + * @throws JsonProcessingException if the Entity JSON is unable to be processed + * @throws IOException if the Entity is unable to be written to the file + */ + public void writeToJson(File file) throws NullPointerException, InternalException, JsonProcessingException, IOException { + ObjectWriter writer = OBJECT_MAPPER.writer(); + JsonNode entityJson = this.toJsonValue(); + writer.writeValue(file, entityJson); + } + /** * Get the value for the given attribute, or null if not present. * diff --git a/CedarJava/src/test/java/com/cedarpolicy/EntityTests.java b/CedarJava/src/test/java/com/cedarpolicy/EntityTests.java index 24b267fe..8c7f95b9 100644 --- a/CedarJava/src/test/java/com/cedarpolicy/EntityTests.java +++ b/CedarJava/src/test/java/com/cedarpolicy/EntityTests.java @@ -22,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.io.File; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -31,6 +32,7 @@ import com.cedarpolicy.model.entity.Entity; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; public class EntityTests { @@ -351,4 +353,34 @@ public void toJsonStringTests() { + "\"parents\":[{\"type\":\"User\",\"id\":\"Bob\"}]," + "\"tags\":{\"tag\":\"longTagValue\"}}"); } + + @Test + public void writeToFileTests() { + PrimString stringAttr = new PrimString("stringAttrValue"); + HashMap attrs = new HashMap<>(); + attrs.put("stringAttr", stringAttr); + + EntityTypeName principalType = EntityTypeName.parse("User").get(); + + HashSet parents = new HashSet(); + parents.add(principalType.of("Bob")); + + PrimString longTag = new PrimString("longTagValue"); + HashMap tags = new HashMap<>(); + tags.put("tag", longTag); + + Entity principal = new Entity(principalType.of("Alice"), attrs, parents, tags); + + Assertions.assertDoesNotThrow(() -> { + File writeFile = File.createTempFile("testEntity", "json"); + writeFile.deleteOnExit(); + principal.writeToJson(writeFile); + + ObjectMapper mapper = new ObjectMapper(); + JsonNode writeFileJson = mapper.readTree(writeFile); + + // Test the file is written correctly + assertEquals(principal.toJsonValue(), writeFileJson); + }); + } } From 6392f74de9258d281487448e7b6cc9105f22413b Mon Sep 17 00:00:00 2001 From: John Brain Date: Fri, 31 Jan 2025 16:09:19 +0000 Subject: [PATCH 09/10] Fix styling issues Signed-off-by: John Brain --- CedarJavaFFI/src/jmap.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CedarJavaFFI/src/jmap.rs b/CedarJavaFFI/src/jmap.rs index 1672a66c..c86a6b4f 100644 --- a/CedarJavaFFI/src/jmap.rs +++ b/CedarJavaFFI/src/jmap.rs @@ -14,7 +14,7 @@ * limitations under the License. */ -use std::{marker::PhantomData}; +use std::marker::PhantomData; use crate::{objects::Object, utils::Result}; use jni::{ From 163aa9654ba2731cce9fd578e463bcec101733ff Mon Sep 17 00:00:00 2001 From: John Brain Date: Fri, 31 Jan 2025 18:35:21 +0000 Subject: [PATCH 10/10] Change json logic Signed-off-by: John Brain --- .../com/cedarpolicy/model/entity/Entity.java | 10 +++++----- CedarJavaFFI/src/interface.rs | 16 +++++++--------- 2 files changed, 12 insertions(+), 14 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 eac07e7b..ece57f89 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/model/entity/Entity.java +++ b/CedarJava/src/main/java/com/cedarpolicy/model/entity/Entity.java @@ -109,11 +109,11 @@ public JsonNode toJsonValue() throws NullPointerException, InternalException, Js * @throws JsonProcessingException if the Entity JSON is unable to be processed * @throws IOException if the Entity is unable to be written to the file */ - public void writeToJson(File file) throws NullPointerException, InternalException, JsonProcessingException, IOException { - ObjectWriter writer = OBJECT_MAPPER.writer(); - JsonNode entityJson = this.toJsonValue(); - writer.writeValue(file, entityJson); - } + public void writeToJson(File file) throws NullPointerException, InternalException, JsonProcessingException, IOException { + ObjectWriter writer = OBJECT_MAPPER.writer(); + JsonNode entityJson = this.toJsonValue(); + writer.writeValue(file, entityJson); + } /** * Get the value for the given attribute, or null if not present. diff --git a/CedarJavaFFI/src/interface.rs b/CedarJavaFFI/src/interface.rs index 9c6176f9..a6c1fa01 100644 --- a/CedarJavaFFI/src/interface.rs +++ b/CedarJavaFFI/src/interface.rs @@ -462,23 +462,21 @@ fn from_json_internal<'a>( } #[jni_fn("com.cedarpolicy.model.entity.Entity")] -pub fn toJsonEntityJni<'a>(mut env: JNIEnv<'a>, _: JClass, entity: JEntity<'a>) -> jvalue { - match to_json_entity_internal(&mut env, entity) { +pub fn toJsonEntityJni<'a>(mut env: JNIEnv<'a>, _: JClass, obj: JObject<'a>) -> jvalue { + match to_json_entity_internal(&mut env, obj) { Ok(v) => v.as_jni(), Err(e) => jni_failed(&mut env, e.as_ref()), } } -fn to_json_entity_internal<'a>( - env: &mut JNIEnv<'a>, - java_entity: JEntity<'a>, -) -> Result> { - if java_entity.as_ref().is_null() { +fn to_json_entity_internal<'a>(env: &mut JNIEnv<'a>, obj: JObject<'a>) -> Result> { + if obj.is_null() { raise_npe(env) } else { + let java_entity = JEntity::cast(env, obj)?; let entity = java_entity.to_entity(env)?; - let entity_json = serde_json::to_string(&entity.to_json_value()?)?; - Ok(JValueGen::Object(env.new_string(&entity_json)?.into())) + let entity_json = &entity.to_json_string()?; + Ok(JValueGen::Object(env.new_string(entity_json)?.into())) } }