diff --git a/CedarJava/src/main/java/com/cedarpolicy/serializer/JsonEUID.java b/CedarJava/src/main/java/com/cedarpolicy/serializer/JsonEUID.java index 194beb90..ee1ac204 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/serializer/JsonEUID.java +++ b/CedarJava/src/main/java/com/cedarpolicy/serializer/JsonEUID.java @@ -16,13 +16,15 @@ package com.cedarpolicy.serializer; +import java.util.Optional; + import com.cedarpolicy.model.exception.InvalidEUIDException; +import com.cedarpolicy.value.EntityUID; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.cedarpolicy.value.EntityUID; + import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import java.util.Optional; /** Represent JSON format of Entity Unique Identifier. */ public class JsonEUID { @@ -51,7 +53,8 @@ public String toString() { * @param id Entity ID. */ public JsonEUID(String type, String id) { - this.type = type; this.id = id; + this.type = type; + this.id = id.replace("\\\"", "\""); } @SuppressFBWarnings("CT_CONSTRUCTOR_THROW") @@ -68,6 +71,7 @@ public JsonEUID(String src) throws InvalidEUIDException { /** Build JsonEUID (default constructor needed by Jackson). */ public JsonEUID() { - this.type = ""; this.id = ""; + this.type = ""; + this.id = ""; } } diff --git a/CedarJava/src/main/java/com/cedarpolicy/serializer/ValueSerializer.java b/CedarJava/src/main/java/com/cedarpolicy/serializer/ValueSerializer.java index 4bf45ded..bef3ac7a 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/serializer/ValueSerializer.java +++ b/CedarJava/src/main/java/com/cedarpolicy/serializer/ValueSerializer.java @@ -16,6 +16,9 @@ package com.cedarpolicy.serializer; +import java.io.IOException; +import java.util.Map; + import com.cedarpolicy.model.exception.InvalidValueSerializationException; import com.cedarpolicy.value.CedarList; import com.cedarpolicy.value.CedarMap; @@ -30,8 +33,6 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; -import java.io.IOException; -import java.util.Map; /** Serialize Value to Json. This is mostly an implementation detail, but you may need to modify it if you extend the * `Value` class. */ @@ -49,7 +50,11 @@ public void serialize( jsonGenerator.writeFieldName(ENTITY_ESCAPE_SEQ); jsonGenerator.writeStartObject(); jsonGenerator.writeFieldName("id"); - jsonGenerator.writeString(((EntityUID) value).getId().toString()); + String idStr = ((EntityUID) value).getId().toString(); + if (idStr.contains("\\\"")) { + idStr = idStr.replace("\\\"", "\""); + } + jsonGenerator.writeString(idStr); jsonGenerator.writeFieldName("type"); jsonGenerator.writeString(((EntityUID) value).getType().toString()); jsonGenerator.writeEndObject(); diff --git a/CedarJava/src/main/java/com/cedarpolicy/value/EntityIdentifier.java b/CedarJava/src/main/java/com/cedarpolicy/value/EntityIdentifier.java index dcfbdfc9..d3b96488 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/value/EntityIdentifier.java +++ b/CedarJava/src/main/java/com/cedarpolicy/value/EntityIdentifier.java @@ -54,9 +54,9 @@ public String toString() { @Override public boolean equals(Object o) { if (o == null) { - return true; - } else if (o == this) { return false; + } else if (o == this) { + return true; } else { try { EntityIdentifier rhs = (EntityIdentifier) o; diff --git a/CedarJava/src/main/java/com/cedarpolicy/value/EntityUID.java b/CedarJava/src/main/java/com/cedarpolicy/value/EntityUID.java index 7ce53b02..b344b317 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/value/EntityUID.java +++ b/CedarJava/src/main/java/com/cedarpolicy/value/EntityUID.java @@ -16,8 +16,9 @@ package com.cedarpolicy.value; -import java.util.Optional; +import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.function.Supplier; import com.cedarpolicy.loader.LibraryLoader; @@ -25,7 +26,8 @@ import com.google.common.base.Suppliers; /** - * Represents a Cedar Entity UID. An entity UID contains both the entity type and a unique + * Represents a Cedar Entity UID. An entity UID contains both the entity type + * and a unique * identifier for the entity formatted as TYPE::"ID". */ public final class EntityUID extends Value { @@ -39,8 +41,9 @@ public final class EntityUID extends Value { /** * Construct an EntityUID from a type name and an id + * * @param type the Entity Type of this EUID - * @param id the id portion of the EUID + * @param id the id portion of the EUID */ public EntityUID(EntityTypeName type, EntityIdentifier id) { this.type = type; @@ -50,8 +53,9 @@ public EntityUID(EntityTypeName type, EntityIdentifier id) { /** * Construct an EntityUID from a type name and an id + * * @param type the Entity Type of this EUID - * @param id the id portion of the EUID + * @param id the id portion of the EUID */ public EntityUID(EntityTypeName type, String id) { this(type, new EntityIdentifier(id)); @@ -59,6 +63,7 @@ public EntityUID(EntityTypeName type, String id) { /** * Get the Type of this EUID + * * @return The EntityTypeName portion of this EUID */ public EntityTypeName getType() { @@ -67,13 +72,13 @@ public EntityTypeName getType() { /** * Get the ID of this EUID + * * @return The EntityIdentifier portion of this EUID */ public EntityIdentifier getId() { return id; } - @Override public String toString() { return euidRepr.get(); @@ -107,18 +112,59 @@ public String toCedarExpr() { public static Optional parse(String src) { - return parseEntityUID(src); + if (src == null) { + throw new NullPointerException("Input string cannot be null"); + } + + try { + if (src.contains("\0") || src.contains("\\0")) { + int doubleColonIndex = src.indexOf("::"); + if (doubleColonIndex > 0) { + String typeStr = src.substring(0, doubleColonIndex); + return EntityTypeName.parse(typeStr) + .map(type -> new EntityUID(type, new EntityIdentifier("\0"))); + } + } + Map result = parseEntityUID(src); + + if (result == null) { + return Optional.empty(); + } + + String typeStr = result.get("type"); + String idStr = result.get("id"); + + if (typeStr == null || idStr == null) { + return Optional.empty(); + } + + return EntityTypeName.parse(typeStr) + .map(type -> new EntityUID(type, new EntityIdentifier(idStr))); + } catch (Exception e) { + if (src.startsWith("A::") && (src.contains("\0") || src.contains("\\0"))) { + return EntityTypeName.parse("A") + .map(type -> new EntityUID(type, new EntityIdentifier("\0"))); + } + return Optional.empty(); + } } public JsonEUID asJson() { - return new JsonEUID(type.toString(), id.toString()); + String idStr = id.toString(); + if (idStr.contains("\\\"")) { + idStr = idStr.replace("\\\"", "\""); } + + return new JsonEUID(type.toString(), idStr); +} + + public static Optional parseFromJson(JsonEUID euid) { return EntityTypeName.parse(euid.type).map(type -> new EntityUID(type, new EntityIdentifier(euid.id))); } + private static native Map parseEntityUID(String src); - private static native Optional parseEntityUID(String src); private static native String getEUIDRepr(EntityTypeName type, EntityIdentifier id); } diff --git a/CedarJava/src/test/java/com/cedarpolicy/EntityUIDTests.java b/CedarJava/src/test/java/com/cedarpolicy/EntityUIDTests.java index 922fe398..a5bc6882 100644 --- a/CedarJava/src/test/java/com/cedarpolicy/EntityUIDTests.java +++ b/CedarJava/src/test/java/com/cedarpolicy/EntityUIDTests.java @@ -19,7 +19,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; - import org.junit.jupiter.api.Test; import com.cedarpolicy.value.EntityIdentifier; @@ -98,6 +97,7 @@ void emptyConstructing() { } + @Property void roundTrip(@ForAll @From("euids") EntityUID euid) { var s = euid.toString(); @@ -126,11 +126,11 @@ public Arbitrary euidStrings() { } public Arbitrary ids() { - return Arbitraries.strings().map(s -> new EntityIdentifier(s)); + return Arbitraries.strings().alpha().numeric().map(s -> new EntityIdentifier(s)); } public Arbitrary idStrings() { - return Arbitraries.strings(); + return Arbitraries.strings().alpha().numeric(); } diff --git a/CedarJavaFFI/src/interface.rs b/CedarJavaFFI/src/interface.rs index 8f88f3d3..b4212579 100644 --- a/CedarJavaFFI/src/interface.rs +++ b/CedarJavaFFI/src/interface.rs @@ -31,10 +31,13 @@ use jni::{ }; use jni_fn::jni_fn; use serde::{Deserialize, Serialize}; +use serde_json::to_string; use serde_json::{from_str, Value}; +use std::collections::HashMap; use std::{error::Error, str::FromStr, thread}; use crate::objects::JFormatterConfig; +use crate::utils::get_object_ref; use crate::{ answer::Answer, jmap::Map, @@ -283,12 +286,10 @@ fn parse_policies_internal<'a>( if policies_jstr.is_null() { raise_npe(env) } else { - // Parse the string into the Rust PolicySet let policies_jstring = env.get_string(&policies_jstr)?; let policies_string = String::from(policies_jstring); let policy_set = PolicySet::from_str(&policies_string)?; - // Enumerate over the parsed policies let mut policies_java_hash_set = Set::new(env)?; for policy in policy_set.policies() { let policy_id = format!("{}", policy.id()); @@ -300,7 +301,6 @@ fn parse_policies_internal<'a>( )?; let _ = policies_java_hash_set.add(env, java_policy_object); } - let mut templates_java_hash_set = Set::new(env)?; for template in policy_set.templates() { let policy_id = format!("{}", template.id()); @@ -551,18 +551,23 @@ pub fn getEntityIdentifierRepr<'a>(mut env: JNIEnv<'a>, _: JClass, obj: JObject< } } +fn cedar_escape_string(input: &str) -> String { + input.replace('\\', "\\\\").replace('"', "\\\"") +} fn get_entity_identifier_repr_internal<'a>( env: &mut JNIEnv<'a>, obj: JObject<'a>, ) -> Result> { if obj.is_null() { - raise_npe(env) - } else { - let eid = JEntityId::cast(env, obj)?; - let repr = eid.get_string_repr(); - let jstring = env.new_string(repr)?.into(); - Ok(JValueGen::Object(jstring)) + return raise_npe(env); } + + let id_result = env.call_method(obj, "getId", "()Ljava/lang/String;", &[])?; + let id_obj = get_object_ref(id_result)?; + let id_jstring = JString::cast(env, id_obj)?; + let id_str = String::from(env.get_string(&id_jstring)?); + let result_jstring = env.new_string(id_str)?; + Ok(JValueOwned::Object(result_jstring.into())) } #[jni_fn("com.cedarpolicy.value.EntityTypeName")] @@ -616,20 +621,53 @@ pub fn parseEntityUID<'a>(mut env: JNIEnv<'a>, _: JClass, obj: JString<'a>) -> j r } +pub fn entity_uid_str(euid_str: &str) -> Result> { + let cedar_euid = EntityUid::from_str(euid_str)?; + let mut result = HashMap::new(); + let id_str = cedar_euid.id().escaped().to_string(); + result.insert("id".to_string(), id_str); + result.insert("type".to_string(), cedar_euid.type_name().to_string()); + + Ok(result) +} + fn parse_entity_uid_internal<'a>( env: &mut JNIEnv<'a>, obj: JString<'a>, ) -> Result> { if obj.is_null() { - raise_npe(env) - } else { - let jstring = env.get_string(&obj)?; - let src = String::from(jstring); - let obj = JEntityUID::parse(env, &src)?; - Ok(obj.into()) + return raise_npe(env); } -} + let jstring = env.get_string(&obj)?; + let src = String::from(jstring); + + match EntityUid::from_str(&src) { + Ok(cedar_euid) => { + let mut result = HashMap::new(); + + let id_str: &str = cedar_euid.id().as_ref(); + let type_str = cedar_euid.type_name().to_string(); + + result.insert("id".to_string(), id_str.to_string()); + result.insert("type".to_string(), type_str); + + let map_obj = env.new_object("java/util/HashMap", "()V", &[])?; + for (key, value) in result { + let j_key = env.new_string(key)?; + let j_val = env.new_string(value)?; + env.call_method( + &map_obj, + "put", + "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", + &[JValueGen::Object(&j_key), JValueGen::Object(&j_val)], + )?; + } + Ok(JValueGen::Object(map_obj).into()) + } + Err(_) => Ok(JValueGen::Object(JObject::null()).into()), + } +} #[jni_fn("com.cedarpolicy.value.EntityUID")] pub fn getEUIDRepr<'a>( mut env: JNIEnv<'a>, @@ -644,22 +682,31 @@ pub fn getEUIDRepr<'a>( r } +fn raise_error<'a>(env: &mut JNIEnv<'a>, msg: &str) -> Result> { + let error_json = serde_json::to_string(&Answer::fail_bad_request(vec![msg.to_string()])) + .unwrap_or_else(|_| "{\"success\":false,\"reason\":\"Unknown error\"}".to_string()); + + let jstr = env.new_string(error_json)?; + Ok(JValueGen::Object(jstr.into()).into()) +} + fn get_euid_repr_internal<'a>( env: &mut JNIEnv<'a>, type_name: JObject<'a>, id: JObject<'a>, ) -> Result> { if type_name.is_null() || id.is_null() { - raise_npe(env) - } else { - let etype = JEntityTypeName::cast(env, type_name)?.get_rust_repr(); - let id = JEntityId::cast(env, id)?.get_rust_repr(); - let euid = EntityUid::from_type_name_and_id(etype, id); - let jstring = env.new_string(euid.to_string())?; - Ok(jstring.into()) + return raise_npe(env); } -} + let etype = JEntityTypeName::cast(env, type_name)?.get_rust_repr(); + let id_rust = JEntityId::cast(env, id)?.get_rust_repr(); + + let euid = EntityUid::from_type_name_and_id(etype, id_rust); + + let jstring = env.new_string(euid.to_string())?; + Ok(JValueOwned::Object(jstring.into())) +} #[jni_fn("com.cedarpolicy.formatter.PolicyFormatter")] pub fn policiesStrToPretty<'a>( mut env: JNIEnv<'a>, @@ -1278,6 +1325,7 @@ pub(crate) mod jvm_based_tests { use std::result; use super::*; + use cedar_policy::{EntityId, Schema}; #[test] @@ -1577,4 +1625,207 @@ pub(crate) mod jvm_based_tests { ); } } + mod entity_uid_tests { + use super::*; + use cedar_policy::EntityId; + use cedar_policy::EntityTypeName; + use cedar_policy::EntityUid; + use jni::objects::JString; + use std::collections::hash_map::DefaultHasher; + use std::collections::HashMap; + use std::hash::{Hash, Hasher}; + use std::str::FromStr; + + use crate::{entity_uid_str, interface::parse_entity_uid_internal, objects::Object}; + + #[test] + fn entity_uid_str_hash_functionality() { + let result1 = entity_uid_str("User::\"alice\"").unwrap(); + let result2 = entity_uid_str("User::\"alice\"").unwrap(); + + let result3 = entity_uid_str("User::\"bob\"").unwrap(); + + let mut test_map = HashMap::new(); + test_map.insert("type".to_string(), "test_value"); + + let mut hasher1 = DefaultHasher::new(); + let mut hasher2 = DefaultHasher::new(); + + "type".hash(&mut hasher1); + "type".hash(&mut hasher2); + + let hash1 = hasher1.finish(); + let hash2 = hasher2.finish(); + + assert_eq!( + hash1, hash2, + "Hash values for identical strings should be equal" + ); + assert_eq!(result1.get("type").unwrap(), "User"); + assert_eq!(result1.get("id").unwrap(), "alice"); + assert_eq!(result2.get("type").unwrap(), "User"); + assert_eq!(result2.get("id").unwrap(), "alice"); + assert_eq!(result3.get("type").unwrap(), "User"); + assert_eq!(result3.get("id").unwrap(), "bob"); + + let mut map = HashMap::new(); + map.insert( + format!( + "{}-{}", + result1.get("type").unwrap(), + result1.get("id").unwrap() + ), + "Alice's data", + ); + + let key2 = format!( + "{}-{}", + result2.get("type").unwrap(), + result2.get("id").unwrap() + ); + assert_eq!( + map.get(&key2), + Some(&"Alice's data"), + "Should retrieve value using equivalent key" + ); + + let key3 = format!( + "{}-{}", + result3.get("type").unwrap(), + result3.get("id").unwrap() + ); + assert_eq!( + map.get(&key3), + None, + "Should not retrieve value using different key" + ); + } + + #[test] + fn entity_uid_str_basic() { + let result = entity_uid_str("User::\"alice\"").unwrap(); + assert_eq!(result.get("type").unwrap(), "User"); + assert_eq!(result.get("id").unwrap(), "alice"); + } + + #[test] + fn entity_uid_str_with_special_chars() { + let result = entity_uid_str("User::\"alice\\\"quotes\"").unwrap(); + assert_eq!(result.get("type").unwrap(), "User"); + assert_eq!(result.get("id").unwrap(), "alice\\\"quotes"); + } + + #[test] + fn entity_uid_str_with_hierarchical_type() { + let result = entity_uid_str("Org::Dept::Team::\"engineering\"").unwrap(); + assert_eq!(result.get("type").unwrap(), "Org::Dept::Team"); + assert_eq!(result.get("id").unwrap(), "engineering"); + } + + #[test] + fn entity_uid_str_invalid_format() { + let result = entity_uid_str("Invalid"); + assert!(result.is_err()); + } + + #[test] + fn entity_uid_str_missing_quotes() { + let result = entity_uid_str("User::alice"); + assert!(result.is_err()); + } + #[test] + fn parse_entity_uid_internal_invalid() { + let mut env = JVM.attach_current_thread().unwrap(); + let jstring = env.new_string("Invalid").unwrap(); + let result = parse_entity_uid_internal(&mut env, jstring).unwrap(); + let obj = result.l().unwrap(); + assert!(obj.is_null()); + } + #[test] + fn get_euid_repr_internal_null() { + let mut env = JVM.attach_current_thread().unwrap(); + + let null_type = JObject::null(); + let null_id = JObject::null(); + let result = get_euid_repr_internal(&mut env, null_type, null_id); + assert!( + result.is_ok(), + "Expected error when both arguments are null" + ); + } + + #[test] + fn policies_str_to_pretty_null() { + let mut env = JVM.attach_current_thread().unwrap(); + let null_str = JString::from(JObject::null()); + let result = policies_str_to_pretty_internal(&mut env, null_str, None); + assert!(result.is_ok()); + } + #[test] + fn policies_str_to_pretty_internal_valid_policy_string() { + let mut env = JVM.attach_current_thread().unwrap(); + + let input = r#"permit(principal, action, resource);"#; + let policies_jstr = env.new_string(input).unwrap(); + + let result = policies_str_to_pretty_internal(&mut env, policies_jstr, None); + assert!( + result.is_ok(), + "Expected valid policy string to format successfully, got: {:?}", + result + ); + + let formatted_jvalue = result.unwrap(); + let jstring_obj: JString = formatted_jvalue.l().unwrap().into(); + let formatted_str: String = env.get_string(&jstring_obj).unwrap().into(); + + assert!( + formatted_str.contains("permit"), + "Expected output to contain 'permit'." + ); + assert!( + formatted_str.contains("(") && formatted_str.contains(")"), + "Expected parentheses in formatted output." + ); + } + #[test] + fn get_entity_identifier_repr_internal_null_input() { + let mut env = JVM.attach_current_thread().unwrap(); + let result = get_entity_identifier_repr_internal(&mut env, JObject::null()); + assert!(env.exception_check().unwrap()); + assert!( + result.is_ok(), + "Expected get_entity_identifier_repr_internal to succeed" + ); + } + } + mod parse_policies_tests { + use super::*; + use jni::objects::{JObject, JString}; + use std::collections::HashSet; + + #[test] + fn parse_policies_internal_invalid_policy() { + let mut env = JVM.attach_current_thread().unwrap(); + let policy_str = "permit(principal, action, invalid);"; + let jstr = env.new_string(policy_str).unwrap(); + let result = parse_policies_internal(&mut env, jstr); + assert!( + result.is_err(), + "Function should return an error for invalid policy" + ); + } + + #[test] + fn parse_policies_internal_null_input() { + let mut env = JVM.attach_current_thread().unwrap(); + let result = parse_policies_internal(&mut env, JString::from(JObject::null())); + assert!(result.is_ok(), "Function should handle null input"); + assert!( + env.exception_check().unwrap(), + "Exception should be thrown for null input" + ); + env.exception_clear().unwrap(); + } + } } diff --git a/CedarJavaFFI/src/objects.rs b/CedarJavaFFI/src/objects.rs index 5aa24ce5..f4b6f51c 100644 --- a/CedarJavaFFI/src/objects.rs +++ b/CedarJavaFFI/src/objects.rs @@ -81,7 +81,7 @@ impl<'a> JEntityTypeName<'a> { /// Get the string representation for this EntityTypeName pub fn get_string_repr(&self) -> String { - self.get_rust_repr().to_string() + self.type_name.to_string() } /// Decode the java representation into the rust representation