From 649c1fba0584ed15531467c402da3830429f53d3 Mon Sep 17 00:00:00 2001 From: Mark Creamer Date: Tue, 28 Jul 2026 13:20:57 +0000 Subject: [PATCH 1/2] Adding PolicySet parsePoliciesJson method --- .../cedarpolicy/model/policy/PolicySet.java | 42 +++++++++++++ .../java/com/cedarpolicy/PolicySetTests.java | 51 ++++++++++++++++ CedarJava/src/test/resources/policies.json | 37 ++++++++++++ CedarJava/src/test/resources/template.json | 53 ++++++++++++++++ CedarJavaFFI/src/interface.rs | 60 +++++++++++++++++++ 5 files changed, 243 insertions(+) create mode 100644 CedarJava/src/test/resources/policies.json create mode 100644 CedarJava/src/test/resources/template.json diff --git a/CedarJava/src/main/java/com/cedarpolicy/model/policy/PolicySet.java b/CedarJava/src/main/java/com/cedarpolicy/model/policy/PolicySet.java index 8dd1c98c..371b03d5 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/model/policy/PolicySet.java +++ b/CedarJava/src/main/java/com/cedarpolicy/model/policy/PolicySet.java @@ -151,6 +151,46 @@ public static PolicySet parsePolicies(String policiesString) throws InternalExce return policySet; } + /** + * Parse multiple policies and templates from a JSON file into a PolicySet. + * + *

The file is expected to contain the JSON (EST) representation of a policy set, e.g. + *

{@code
+     * {
+     *   "staticPolicies": { "policy0": { ... } },
+     *   "templates": { "template0": { ... } },
+     *   "templateLinks": [ ... ]
+     * }
+     * }
+ * + * @param filePath the path to the JSON file containing the policies + * @return a PolicySet containing the parsed policies + * @throws InternalException + * @throws IOException + * @throws NullPointerException + */ + public static PolicySet parsePoliciesJson(Path filePath) throws InternalException, IOException { + // Read the file contents into a String + String policiesJsonString = Files.readString(filePath); + return parsePoliciesJson(policiesJsonString); + } + + /** + * Parse a JSON string containing multiple policies and templates into a PolicySet. + * + *

The string is expected to be the JSON (EST) representation of a policy set. See + * {@link #parsePoliciesJson(Path)} for the expected shape. + * + * @param policiesJsonString the JSON string containing the policies + * @return a PolicySet containing the parsed policies + * @throws InternalException + * @throws NullPointerException + */ + public static PolicySet parsePoliciesJson(String policiesJsonString) throws InternalException { + PolicySet policySet = parsePoliciesJsonJni(policiesJsonString); + return policySet; + } + // --- Caching support --- private volatile String cacheId; @@ -219,6 +259,8 @@ public void run() { } private static native PolicySet parsePoliciesJni(String policiesStr) throws InternalException, NullPointerException; + private static native PolicySet parsePoliciesJsonJni(String policiesJsonStr) + throws InternalException, NullPointerException; private static native String policySetToJson(String policySetStr) throws InternalException, NullPointerException; private static native void preparsePolicySetJni(String id, String policiesJson) throws InternalException; private static native void removeCachedPolicySetJni(String id); diff --git a/CedarJava/src/test/java/com/cedarpolicy/PolicySetTests.java b/CedarJava/src/test/java/com/cedarpolicy/PolicySetTests.java index 86490c56..9abc46d5 100644 --- a/CedarJava/src/test/java/com/cedarpolicy/PolicySetTests.java +++ b/CedarJava/src/test/java/com/cedarpolicy/PolicySetTests.java @@ -79,6 +79,57 @@ public void parseTemplatesTests() throws InternalException, IOException { assertEquals(1, policySet.templates.size()); } + @Test + public void parsePoliciesJsonTests() throws InternalException, IOException { + PolicySet policySet = PolicySet.parsePoliciesJson(Path.of(TEST_RESOURCES_DIR + "policies.json")); + for (Policy p: policySet.policies) { + assertNotNull(p.policySrc); + } + // Make sure the policy IDs are unique as Policies are made + assertEquals(2, policySet.policies.stream().map(p -> p.policyID).distinct().count()); + assertEquals(2, policySet.policies.size()); + assertEquals(0, policySet.templates.size()); + } + + @Test + public void parsePoliciesJsonStringTests() throws InternalException { + String json = "{\"staticPolicies\":{\"policy0\":{\"effect\":\"permit\"," + + "\"principal\":{\"op\":\"All\"},\"action\":{\"op\":\"All\"}," + + "\"resource\":{\"op\":\"All\"},\"conditions\":[]}}," + + "\"templates\":{},\"templateLinks\":[]}"; + PolicySet policySet = PolicySet.parsePoliciesJson(json); + for (Policy p: policySet.policies) { + assertNotNull(p.policySrc); + } + assertEquals(1, policySet.policies.size()); + assertEquals(0, policySet.templates.size()); + } + + @Test + public void parseTemplatesJsonTests() throws InternalException, IOException { + PolicySet policySet = PolicySet.parsePoliciesJson(Path.of(TEST_RESOURCES_DIR + "template.json")); + for (Policy p: policySet.policies) { + assertNotNull(p.policySrc); + } + assertEquals(2, policySet.policies.size()); + + for (Policy p: policySet.templates) { + assertNotNull(p.policySrc); + } + assertEquals(1, policySet.templates.size()); + } + + @Test + public void parsePoliciesJsonExceptionTests() throws InternalException, IOException { + assertThrows(IOException.class, () -> { + PolicySet.parsePoliciesJson(Path.of("nonExistentFilePath.json")); + }); + // Cedar policy text, not JSON, should fail to parse as JSON + assertThrows(InternalException.class, () -> { + PolicySet.parsePoliciesJson("permit(principal, action, resource);"); + }); + } + @Test public void parsePoliciesExceptionTests() throws InternalException, IOException { assertThrows(IOException.class, () -> { diff --git a/CedarJava/src/test/resources/policies.json b/CedarJava/src/test/resources/policies.json new file mode 100644 index 00000000..98c39d3c --- /dev/null +++ b/CedarJava/src/test/resources/policies.json @@ -0,0 +1,37 @@ +{ + "staticPolicies": { + "Policy #1": { + "effect": "permit", + "principal": { + "op": "in", + "entity": { "type": "UserGroup", "id": "friends" } + }, + "action": { + "op": "==", + "entity": { "type": "Action", "id": "view" } + }, + "resource": { + "op": "==", + "entity": { "type": "Photo", "id": "Husky.jpg" } + }, + "conditions": [] + }, + "Policy #2": { + "effect": "forbid", + "principal": { + "op": "==", + "entity": { "type": "User", "id": "Matt" } + }, + "action": { + "op": "All" + }, + "resource": { + "op": "==", + "entity": { "type": "Photo", "id": "Husky.jpg" } + }, + "conditions": [] + } + }, + "templates": {}, + "templateLinks": [] +} diff --git a/CedarJava/src/test/resources/template.json b/CedarJava/src/test/resources/template.json new file mode 100644 index 00000000..108fba01 --- /dev/null +++ b/CedarJava/src/test/resources/template.json @@ -0,0 +1,53 @@ +{ + "staticPolicies": { + "Policy #1": { + "effect": "permit", + "principal": { + "op": "in", + "entity": { "type": "UserGroup", "id": "friends" } + }, + "action": { + "op": "==", + "entity": { "type": "Action", "id": "view" } + }, + "resource": { + "op": "==", + "entity": { "type": "Photo", "id": "Husky.jpg" } + }, + "conditions": [] + }, + "Policy #2": { + "effect": "forbid", + "principal": { + "op": "==", + "entity": { "type": "User", "id": "Matt" } + }, + "action": { + "op": "All" + }, + "resource": { + "op": "==", + "entity": { "type": "Photo", "id": "Husky.jpg" } + }, + "conditions": [] + } + }, + "templates": { + "Template #1": { + "effect": "permit", + "principal": { + "op": "==", + "slot": "?principal" + }, + "action": { + "op": "All" + }, + "resource": { + "op": "in", + "slot": "?resource" + }, + "conditions": [] + } + }, + "templateLinks": [] +} diff --git a/CedarJavaFFI/src/interface.rs b/CedarJavaFFI/src/interface.rs index 2bdbaa18..46ff780a 100644 --- a/CedarJavaFFI/src/interface.rs +++ b/CedarJavaFFI/src/interface.rs @@ -512,6 +512,7 @@ pub fn validate_entities(input: &str) -> serde_json::Result { EntitiesError::Duplicate(err) => err.to_string(), EntitiesError::TransitiveClosureError(err) => err.to_string(), EntitiesError::InvalidEntity(err) => err.to_string(), + EntitiesError::InvalidEntityStructure(err) => err.to_string(), }; Ok(Answer::fail_bad_request(vec![err_message])) } @@ -700,6 +701,65 @@ fn parse_policies_internal<'a>( } } +#[jni_fn("com.cedarpolicy.model.policy.PolicySet")] +pub fn parsePoliciesJsonJni<'a>( + mut env: JNIEnv<'a>, + _: JClass, + policies_json_jstr: JString<'a>, +) -> jvalue { + match parse_policies_json_internal(&mut env, policies_json_jstr) { + Err(e) => jni_failed(&mut env, e.as_ref()), + Ok(policies_set) => policies_set.as_jni(), + } +} + +fn parse_policies_json_internal<'a>( + env: &mut JNIEnv<'a>, + policies_json_jstr: JString<'a>, +) -> Result> { + if policies_json_jstr.is_null() { + raise_npe(env) + } else { + // Parse the JSON string into the Rust PolicySet + let policies_json_jstring = env.get_string(&policies_json_jstr)?; + let policies_json_string = String::from(policies_json_jstring); + let policy_set = PolicySet::from_json_str(&policies_json_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()); + let policy_text = format!("{}", policy); + let java_policy_object = JPolicy::new( + env, + &env.new_string(&policy_text)?, + &env.new_string(&policy_id)?, + )?; + 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()); + let policy_text = format!("{}", template); + let java_policy_object = JPolicy::new( + env, + &env.new_string(&policy_text)?, + &env.new_string(&policy_id)?, + )?; + let _ = templates_java_hash_set.add(env, java_policy_object); + } + + let java_policy_set = create_java_policy_set( + env, + policies_java_hash_set.as_ref(), + templates_java_hash_set.as_ref(), + ); + + Ok(JValueGen::Object(java_policy_set)) + } +} + fn create_java_policy_set<'a>( env: &mut JNIEnv<'a>, policies_java_hash_set: &JObject<'a>, From ce0a935b96f544bcca5a15049006192aafc3e5a9 Mon Sep 17 00:00:00 2001 From: Mark Creamer Date: Tue, 28 Jul 2026 18:40:00 +0000 Subject: [PATCH 2/2] Supporting template linked policies extraction in parsePoliciesJson --- .../java/com/cedarpolicy/PolicySetTests.java | 16 ++ CedarJava/src/test/resources/template.json | 11 +- CedarJavaFFI/src/interface.rs | 174 +++++++++++------- CedarJavaFFI/src/objects.rs | 72 ++++++++ 4 files changed, 206 insertions(+), 67 deletions(-) diff --git a/CedarJava/src/test/java/com/cedarpolicy/PolicySetTests.java b/CedarJava/src/test/java/com/cedarpolicy/PolicySetTests.java index 9abc46d5..5d4ed623 100644 --- a/CedarJava/src/test/java/com/cedarpolicy/PolicySetTests.java +++ b/CedarJava/src/test/java/com/cedarpolicy/PolicySetTests.java @@ -19,10 +19,13 @@ import com.cedarpolicy.model.exception.InternalException; import com.cedarpolicy.model.policy.Policy; import com.cedarpolicy.model.policy.PolicySet; +import com.cedarpolicy.model.policy.TemplateLink; +import com.cedarpolicy.value.EntityUID; import org.junit.jupiter.api.Test; import java.io.IOException; import java.nio.file.Path; +import java.util.Map; import com.fasterxml.jackson.core.JsonProcessingException; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -111,12 +114,25 @@ public void parseTemplatesJsonTests() throws InternalException, IOException { for (Policy p: policySet.policies) { assertNotNull(p.policySrc); } + // The two static policies only; the template-linked policy is reported + // via templateLinks rather than as a static policy. assertEquals(2, policySet.policies.size()); for (Policy p: policySet.templates) { assertNotNull(p.policySrc); } assertEquals(1, policySet.templates.size()); + + // The template instantiation is preserved as a TemplateLink + assertEquals(1, policySet.templateLinks.size()); + TemplateLink link = policySet.templateLinks.get(0); + assertEquals("Template #1", link.getTemplateId()); + assertEquals("Link #1", link.getResultPolicyId()); + + Map linkValues = link.getLinkValues(); + assertEquals(2, linkValues.size()); + assertEquals("User::\"Matt\"", linkValues.get("?principal").toString()); + assertEquals("Album::\"Vacation\"", linkValues.get("?resource").toString()); } @Test diff --git a/CedarJava/src/test/resources/template.json b/CedarJava/src/test/resources/template.json index 108fba01..090b315f 100644 --- a/CedarJava/src/test/resources/template.json +++ b/CedarJava/src/test/resources/template.json @@ -49,5 +49,14 @@ "conditions": [] } }, - "templateLinks": [] + "templateLinks": [ + { + "newId": "Link #1", + "templateId": "Template #1", + "values": { + "?principal": { "type": "User", "id": "Matt" }, + "?resource": { "type": "Album", "id": "Vacation" } + } + } + ] } diff --git a/CedarJavaFFI/src/interface.rs b/CedarJavaFFI/src/interface.rs index 46ff780a..3c506a5c 100644 --- a/CedarJavaFFI/src/interface.rs +++ b/CedarJavaFFI/src/interface.rs @@ -22,7 +22,8 @@ use cedar_policy::ffi::{ }; use cedar_policy::{ ffi::{is_authorized_json_str, validate_json_str}, - Authorizer, Entities as CedarEntities, EntityUid, Policy, PolicySet, Request, Schema, Template, + Authorizer, Entities as CedarEntities, EntityUid, Policy, PolicySet, Request, Schema, SlotId, + Template, }; use cedar_policy_formatter::{policies_str_to_pretty, Config}; use dashmap::DashMap; @@ -34,15 +35,17 @@ use jni::{ use jni_fn::jni_fn; use serde::{Deserialize, Serialize}; use serde_json::{from_str, Value}; +use std::collections::HashMap; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::LazyLock; use std::{error::Error, panic, str::FromStr}; use crate::{ answer::Answer, + jlist::List, jmap::Map, jset::Set, - objects::{JEntityId, JEntityTypeName, JEntityUID, JPolicy, Object}, + objects::{JEntityId, JEntityTypeName, JEntityUID, JLinkValue, JPolicy, JTemplateLink, Object}, utils::raise_npe, }; use crate::{helpers::validate_with_level_json_str, objects::JFormatterConfig}; @@ -666,38 +669,7 @@ fn parse_policies_internal<'a>( 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()); - let policy_text = format!("{}", policy); - let java_policy_object = JPolicy::new( - env, - &env.new_string(&policy_text)?, - &env.new_string(&policy_id)?, - )?; - 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()); - let policy_text = format!("{}", template); - let java_policy_object = JPolicy::new( - env, - &env.new_string(&policy_text)?, - &env.new_string(&policy_id)?, - )?; - let _ = templates_java_hash_set.add(env, java_policy_object); - } - - let java_policy_set = create_java_policy_set( - env, - policies_java_hash_set.as_ref(), - templates_java_hash_set.as_ref(), - ); - - Ok(JValueGen::Object(java_policy_set)) + policy_set_to_java(env, &policy_set) } } @@ -725,52 +697,122 @@ fn parse_policies_json_internal<'a>( let policies_json_string = String::from(policies_json_jstring); let policy_set = PolicySet::from_json_str(&policies_json_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()); - let policy_text = format!("{}", policy); - let java_policy_object = JPolicy::new( - env, - &env.new_string(&policy_text)?, - &env.new_string(&policy_id)?, - )?; - 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()); - let policy_text = format!("{}", template); - let java_policy_object = JPolicy::new( - env, - &env.new_string(&policy_text)?, - &env.new_string(&policy_id)?, - )?; - let _ = templates_java_hash_set.add(env, java_policy_object); - } - - let java_policy_set = create_java_policy_set( - env, - policies_java_hash_set.as_ref(), - templates_java_hash_set.as_ref(), - ); + policy_set_to_java(env, &policy_set) + } +} + +/// Build a Java `LinkValue` from a slot id and the entity UID linked to it. +fn create_java_link_value<'a>( + env: &mut JNIEnv<'a>, + slot_id: &SlotId, + euid: &EntityUid, +) -> Result> { + let slot_jstring = env.new_string(format!("{}", slot_id))?; + let entity_type = JEntityTypeName::try_from(env, euid.type_name())?; + let entity_id = JEntityId::try_from(env, euid.id())?; + let java_euid = JEntityUID::new(env, entity_type, entity_id)?; + + JLinkValue::new(env, slot_jstring, java_euid) +} - Ok(JValueGen::Object(java_policy_set)) +/// Build a Java `TemplateLink` describing a template-linked policy. +fn create_java_template_link<'a>( + env: &mut JNIEnv<'a>, + template_id: &str, + result_policy_id: &str, + link_values: &HashMap, +) -> Result> { + let mut link_values_list: List<'a, JLinkValue<'a>> = List::new(env)?; + // Sort by slot id so the resulting list has a deterministic order + let mut sorted_values: Vec<(&SlotId, &EntityUid)> = link_values.iter().collect(); + sorted_values.sort_by_key(|(slot_id, _)| format!("{}", slot_id)); + for (slot_id, euid) in sorted_values { + let link_value = create_java_link_value(env, slot_id, euid)?; + link_values_list.add(env, link_value)?; } + + let template_id_jstring = env.new_string(template_id)?; + let result_policy_id_jstring = env.new_string(result_policy_id)?; + JTemplateLink::new( + env, + template_id_jstring, + result_policy_id_jstring, + link_values_list, + ) +} + +/// Convert a Rust `PolicySet` into the equivalent Java `PolicySet` object. +/// +/// Static policies land in `policies`, templates in `templates`, and each +/// template-linked policy contributes a `TemplateLink` to `templateLinks`. +/// Note that `PolicySet::policies()` yields both static and template-linked +/// policies, so linked policies are separated out by `template_id()`. +fn policy_set_to_java<'a>(env: &mut JNIEnv<'a>, policy_set: &PolicySet) -> Result> { + let mut policies_java_hash_set = Set::new(env)?; + let mut template_links_java_list: List<'a, JTemplateLink<'a>> = List::new(env)?; + + for policy in policy_set.policies() { + let policy_id = format!("{}", policy.id()); + match (policy.template_id(), policy.template_links()) { + // A template-linked policy: record the link instead of treating it + // as a static policy. + (Some(template_id), Some(link_values)) => { + let java_template_link = create_java_template_link( + env, + &format!("{}", template_id), + &policy_id, + &link_values, + )?; + template_links_java_list.add(env, java_template_link)?; + } + // A static policy. + _ => { + let policy_text = format!("{}", policy); + let java_policy_object = JPolicy::new( + env, + &env.new_string(&policy_text)?, + &env.new_string(&policy_id)?, + )?; + 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()); + let policy_text = format!("{}", template); + let java_policy_object = JPolicy::new( + env, + &env.new_string(&policy_text)?, + &env.new_string(&policy_id)?, + )?; + let _ = templates_java_hash_set.add(env, java_policy_object); + } + + let java_policy_set = create_java_policy_set( + env, + policies_java_hash_set.as_ref(), + templates_java_hash_set.as_ref(), + template_links_java_list.as_ref(), + ); + + Ok(JValueGen::Object(java_policy_set)) } fn create_java_policy_set<'a>( env: &mut JNIEnv<'a>, policies_java_hash_set: &JObject<'a>, templates_java_hash_set: &JObject<'a>, + template_links_java_list: &JObject<'a>, ) -> JObject<'a> { env.new_object( "com/cedarpolicy/model/policy/PolicySet", - "(Ljava/util/Set;Ljava/util/Set;)V", + "(Ljava/util/Set;Ljava/util/Set;Ljava/util/List;)V", &[ JValueGen::Object(policies_java_hash_set), JValueGen::Object(templates_java_hash_set), + JValueGen::Object(template_links_java_list), ], ) .expect("Failed to create new PolicySet object") diff --git a/CedarJavaFFI/src/objects.rs b/CedarJavaFFI/src/objects.rs index 5aa24ce5..0507e0dd 100644 --- a/CedarJavaFFI/src/objects.rs +++ b/CedarJavaFFI/src/objects.rs @@ -370,6 +370,78 @@ impl<'a> AsRef> for JPolicy<'a> { } } +/// Typed wrapper for LinkValue objects +/// (com.cedarpolicy.model.policy.LinkValue) +pub struct JLinkValue<'a> { + obj: JObject<'a>, +} + +impl<'a> JLinkValue<'a> { + /// Construct a new LinkValue object from a slot name and the entity UID + /// filling that slot + pub fn new(env: &mut JNIEnv<'a>, slot: JString<'a>, value: JEntityUID<'a>) -> Result { + let obj = env.new_object( + "com/cedarpolicy/model/policy/LinkValue", + "(Ljava/lang/String;Lcom/cedarpolicy/value/EntityUID;)V", + &[JValueGen::Object(&slot), JValueGen::Object(value.as_ref())], + )?; + Ok(Self { obj }) + } +} + +impl<'a> Object<'a> for JLinkValue<'a> { + fn cast(env: &mut JNIEnv<'a>, obj: JObject<'a>) -> Result { + assert_is_class(env, &obj, "com/cedarpolicy/model/policy/LinkValue")?; + Ok(Self { obj }) + } +} + +impl<'a> AsRef> for JLinkValue<'a> { + fn as_ref(&self) -> &JObject<'a> { + &self.obj + } +} + +/// Typed wrapper for TemplateLink objects +/// (com.cedarpolicy.model.policy.TemplateLink) +pub struct JTemplateLink<'a> { + obj: JObject<'a>, +} + +impl<'a> JTemplateLink<'a> { + /// Construct a new TemplateLink object + pub fn new( + env: &mut JNIEnv<'a>, + template_id: JString<'a>, + result_policy_id: JString<'a>, + link_values: List<'a, JLinkValue<'a>>, + ) -> Result { + let obj = env.new_object( + "com/cedarpolicy/model/policy/TemplateLink", + "(Ljava/lang/String;Ljava/lang/String;Ljava/util/List;)V", + &[ + JValueGen::Object(&template_id), + JValueGen::Object(&result_policy_id), + JValueGen::Object(link_values.as_ref()), + ], + )?; + Ok(Self { obj }) + } +} + +impl<'a> Object<'a> for JTemplateLink<'a> { + fn cast(env: &mut JNIEnv<'a>, obj: JObject<'a>) -> Result { + assert_is_class(env, &obj, "com/cedarpolicy/model/policy/TemplateLink")?; + Ok(Self { obj }) + } +} + +impl<'a> AsRef> for JTemplateLink<'a> { + fn as_ref(&self) -> &JObject<'a> { + &self.obj + } +} + pub struct JFormatterConfig<'a> { obj: JObject<'a>, formatter_config: Config,