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..ece57f89 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/model/entity/Entity.java +++ b/CedarJava/src/main/java/com/cedarpolicy/model/entity/Entity.java @@ -18,7 +18,15 @@ 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.databind.ObjectWriter; +import com.fasterxml.jackson.core.JsonProcessingException; + +import java.io.File; +import java.io.IOException; import java.util.*; import java.util.stream.Collectors; @@ -29,6 +37,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 +76,45 @@ public Entity(EntityUID uid, Map attributes, Set paren this.tags = new HashMap<>(tags); } + /** + * 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 NullPointerException, InternalException { + return toJsonEntityJni(this); + } + + /** + * Get the Entity's JSON 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 NullPointerException, InternalException, JsonProcessingException { + String entityJsonStr = this.toJsonString(); + 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. * @@ -116,6 +165,14 @@ public EntityUID getEUID() { return euid; } + /** + * Get the Entity's attributes + * @return the attribute map + */ + private Map getAttributes() { + return attrs; + } + /** * Get this Entity's parents * @return the set of parent EntityUIDs @@ -131,4 +188,6 @@ public Set getParents() { public Map getTags() { return tags; } + + private static native String toJsonEntityJni(Entity entity) throws NullPointerException, InternalException; } diff --git a/CedarJava/src/test/java/com/cedarpolicy/EntityTests.java b/CedarJava/src/test/java/com/cedarpolicy/EntityTests.java index 94c78379..8c7f95b9 100644 --- a/CedarJava/src/test/java/com/cedarpolicy/EntityTests.java +++ b/CedarJava/src/test/java/com/cedarpolicy/EntityTests.java @@ -14,19 +14,25 @@ * 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.assertEquals; 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; - -import static org.junit.jupiter.api.Assertions.assertEquals; +import java.util.List; import com.cedarpolicy.value.*; import com.cedarpolicy.model.entity.Entity; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; public class EntityTests { @@ -49,4 +55,332 @@ public void getAttrTests() { // Test key not found assertEquals(principal.getAttr("decimalAttr"), null); } + + @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\"}]}"); + + 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 + 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(); + 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 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\"}}"); + } + + @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); + }); + } } diff --git a/CedarJavaFFI/src/interface.rs b/CedarJavaFFI/src/interface.rs index 94e30491..a6c1fa01 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,25 @@ 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 { + 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>, 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 = &entity.to_json_string()?; + 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..c86a6b4f --- /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 map + 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 value = env + .call_method( + &self.obj, + "get", + "(Ljava/lang/Object;)Ljava/lang/Object;", + &[key], + )? + .l()?; + Ok(value) + } + + /// 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()); + 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..1e1f76b0 100644 --- a/CedarJavaFFI/src/objects.rs +++ b/CedarJavaFFI/src/objects.rs @@ -18,12 +18,16 @@ 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 +46,134 @@ 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> { + /// 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, 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 euid_jobj: JObject<'a> = env + .call_method( + &self.obj, + "getEUID", + "()Lcom/cedarpolicy/value/EntityUID;", + &[], + )? + .l()?; + 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 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(parents_jobj, "toArray", "()[Ljava/lang/Object;", &[])? + .l()? + .into(); + + let length = env.get_array_length(&java_parents_array)?; + for i in 0..length { + 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>, + map_type: JEntityMapType, + ) -> Result> { + 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 jobj_entry_set: JObject<'a> = env + .call_method(&jobj_map, "entrySet", "()Ljava/util/Set;", &[])? + .l()?; + + let java_entry_array: JObjectArray = env + .call_method(jobj_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_jobj: JObject = env + .call_method(java_value, "toCedarExpr", "()Ljava/lang/String;", &[])? + .l()?; + + 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_jobj = JString::cast(env, java_key)?; + let key: String = env.get_string(&key_jobj)?.into(); + + map.insert(key, restircted_expr); + } + + Ok(map) + } +} + +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")?; + 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> { @@ -123,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> { @@ -250,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() @@ -314,6 +472,39 @@ impl<'a> JEntityUID<'a> { Err(_) => JOptional::empty(env), } } + + /// Convert the Java EntityUID into a rust EntityUid + pub fn to_entity_uid(&self, env: &mut JNIEnv<'a>) -> Result { + // get the entity id from the JEntityUID + let eid_jobj = env + .call_method( + &self.obj, + "getId", + "()Lcom/cedarpolicy/value/EntityIdentifier;", + &[], + )? + .l()?; + + 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 + .call_method( + &self.obj, + "getType", + "()Lcom/cedarpolicy/value/EntityTypeName;", + &[], + )? + .l()?; + + 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); + Ok(entity_uid) + } } impl<'a> Object<'a> for JEntityUID<'a> {