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