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..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;
@@ -79,6 +82,70 @@ 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);
+ }
+ // 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
+ 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..090b315f
--- /dev/null
+++ b/CedarJava/src/test/resources/template.json
@@ -0,0 +1,62 @@
+{
+ "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": [
+ {
+ "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 2bdbaa18..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};
@@ -512,6 +515,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]))
}
@@ -665,52 +669,150 @@ 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(),
- );
+ policy_set_to_java(env, &policy_set)
+ }
+}
+
+#[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)?;
+
+ 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,