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 77ba8b33..c980d2aa 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/model/policy/PolicySet.java +++ b/CedarJava/src/main/java/com/cedarpolicy/model/policy/PolicySet.java @@ -16,8 +16,10 @@ package com.cedarpolicy.model.policy; +import static com.cedarpolicy.CedarJson.objectWriter; import com.cedarpolicy.loader.LibraryLoader; import com.cedarpolicy.model.exception.InternalException; +import com.fasterxml.jackson.core.JsonProcessingException; import java.util.Collections; import java.util.List; @@ -87,22 +89,33 @@ public Map getTemplates() { /** * Gets number of static policies in the Policy Set. - * + * * @return number of static policies, returns 0 if policies set is null */ public int getNumPolicies() { return policies != null ? policies.size() : 0; - } + } /** * Gets number of templates in the Policy Set. - * + * * @return number of templates, returns 0 if templates set is null */ public int getNumTemplates() { return templates != null ? templates.size() : 0; } + /** + * Converts the PolicySet object to a Cedar JSON string representation. + * + * @return Cedar JSON string representation of the PolicySet + * @throws InternalException if there is an error during JSON conversion in the Rust native code + * @throws JsonProcessingException if there is an error serializing the object to JSON + */ + public String toJson() throws InternalException, JsonProcessingException { + return policySetToJson(objectWriter().writeValueAsString(this)); + } + /** * Parse multiple policies and templates from a file into a PolicySet. * @param filePath the path to the file containing the policies @@ -130,4 +143,5 @@ public static PolicySet parsePolicies(String policiesString) throws InternalExce } private static native PolicySet parsePoliciesJni(String policiesStr) throws InternalException, NullPointerException; + private static native String policySetToJson(String policySetStr) throws InternalException, NullPointerException; } diff --git a/CedarJava/src/test/java/com/cedarpolicy/PolicySetTests.java b/CedarJava/src/test/java/com/cedarpolicy/PolicySetTests.java index b06e7bdd..86490c56 100644 --- a/CedarJava/src/test/java/com/cedarpolicy/PolicySetTests.java +++ b/CedarJava/src/test/java/com/cedarpolicy/PolicySetTests.java @@ -23,18 +23,22 @@ import java.io.IOException; import java.nio.file.Path; +import com.fasterxml.jackson.core.JsonProcessingException; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertEquals; +import static com.cedarpolicy.TestUtil.buildValidPolicySet; +import static com.cedarpolicy.TestUtil.buildInvalidPolicySet; public class PolicySetTests { private static final String TEST_RESOURCES_DIR = "src/test/resources/"; @Test public void parsePoliciesTests() throws InternalException, IOException { - PolicySet policySet = PolicySet.parsePolicies(Path.of(TEST_RESOURCES_DIR + "policies.cedar")); - for (Policy p: policySet.policies) { + PolicySet policySet = + PolicySet.parsePolicies(Path.of(TEST_RESOURCES_DIR + "policies.cedar")); + for (Policy p : policySet.policies) { assertNotNull(p.policySrc); } // Make sure the policy IDs are unique as Policies are made @@ -46,13 +50,14 @@ public void parsePoliciesTests() throws InternalException, IOException { @Test public void parsePoliciesStringTests() throws InternalException { PolicySet policySet = PolicySet.parsePolicies("permit(principal, action, resource);"); - PolicySet policySet2 = PolicySet.parsePolicies("permit(principal, action, resource) when { principal has x && principal.x == 5};"); - for (Policy p: policySet.policies) { + PolicySet policySet2 = PolicySet.parsePolicies( + "permit(principal, action, resource) when { principal has x && principal.x == 5};"); + for (Policy p : policySet.policies) { assertNotNull(p.policySrc); } assertEquals(1, policySet.policies.size()); assertEquals(0, policySet.templates.size()); - for (Policy p: policySet2.policies) { + for (Policy p : policySet2.policies) { assertNotNull(p.policySrc); } assertEquals(1, policySet2.policies.size()); @@ -61,13 +66,14 @@ public void parsePoliciesStringTests() throws InternalException { @Test public void parseTemplatesTests() throws InternalException, IOException { - PolicySet policySet = PolicySet.parsePolicies(Path.of(TEST_RESOURCES_DIR + "template.cedar")); - for (Policy p: policySet.policies) { + PolicySet policySet = + PolicySet.parsePolicies(Path.of(TEST_RESOURCES_DIR + "template.cedar")); + for (Policy p : policySet.policies) { assertNotNull(p.policySrc); } assertEquals(2, policySet.policies.size()); - for (Policy p: policySet.templates) { + for (Policy p : policySet.templates) { assertNotNull(p.policySrc); } assertEquals(1, policySet.templates.size()); @@ -96,8 +102,33 @@ public void getNumTests() throws InternalException, IOException { assertEquals(0, emptyPolicySet.getNumTemplates()); // Non-empty policy set - PolicySet policySet = PolicySet.parsePolicies(Path.of(TEST_RESOURCES_DIR + "template.cedar")); + PolicySet policySet = + PolicySet.parsePolicies(Path.of(TEST_RESOURCES_DIR + "template.cedar")); assertEquals(2, policySet.getNumPolicies()); assertEquals(1, policySet.getNumTemplates()); } + + @Test + public void policySetToJsonTests() + throws JsonProcessingException, IOException, InternalException { + // Tests valid PolicySet + PolicySet validPolicySet = buildValidPolicySet(); + String validJson = + "{\"templates\":{\"t0\":{\"effect\":\"permit\",\"principal\":{\"op\":\"==\",\"slot\":\"?principal\"}," + + "\"action\":{\"op\":\"==\",\"entity\":{\"type\":\"Action\",\"id\":\"View_Photo\"}}," + + "\"resource\":{\"op\":\"in\",\"entity\":{\"type\":\"Album\",\"id\":\"Vacation\"}},\"conditions\":[]}}," + + "\"staticPolicies\":{\"p1\":{\"effect\":\"permit\",\"principal\":{\"op\":\"==\"," + + "\"entity\":{\"type\":\"User\",\"id\":\"Bob\"}}," + + "\"action\":{\"op\":\"==\",\"entity\":{\"type\":\"Action\",\"id\":\"View_Photo\"}}," + + "\"resource\":{\"op\":\"in\",\"entity\":{\"type\":\"Album\",\"id\":\"Vacation\"}},\"conditions\":[]}}," + + "\"templateLinks\":[{\"templateId\":\"t0\",\"newId\":\"tl0\",\"values\":{\"?principal\":" + + "{\"__entity\":{\"type\":\"User\",\"id\":\"Alice\"}}}}]}"; + assertEquals(validJson, validPolicySet.toJson()); + + // Tests invalid PolicySet + PolicySet invalidPolicySet = buildInvalidPolicySet(); + assertThrows(InternalException.class, () -> { + invalidPolicySet.toJson(); + }); + } } diff --git a/CedarJava/src/test/java/com/cedarpolicy/TestUtil.java b/CedarJava/src/test/java/com/cedarpolicy/TestUtil.java index 939b11d1..28bc3ec3 100644 --- a/CedarJava/src/test/java/com/cedarpolicy/TestUtil.java +++ b/CedarJava/src/test/java/com/cedarpolicy/TestUtil.java @@ -18,11 +18,21 @@ import com.cedarpolicy.model.schema.Schema; import com.cedarpolicy.model.schema.Schema.JsonOrCedar; +import com.cedarpolicy.model.policy.TemplateLink; +import com.cedarpolicy.model.policy.PolicySet; +import com.cedarpolicy.model.policy.LinkValue; +import com.cedarpolicy.model.policy.Policy; +import com.cedarpolicy.model.entity.Entity; +import com.cedarpolicy.value.EntityTypeName; +import java.util.HashSet; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Optional; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Set; /** Utils to help with tests. */ public final class TestUtil { @@ -45,4 +55,57 @@ public static Schema loadSchemaResource(String schemaFile) { throw new RuntimeException("Failed to load test schema file " + schemaFile, e); } } + + public static PolicySet buildValidPolicySet() { + EntityTypeName principalType = EntityTypeName.parse("User").get(); + Set policies = new HashSet<>(); + Set templates = new HashSet<>(); + ArrayList templateLinks = new ArrayList(); + ArrayList linkValueList = new ArrayList<>(); + + String fullPolicy = + "permit(principal == User::\"Bob\", action == Action::\"View_Photo\", resource in Album::\"Vacation\");"; + Policy newPolicy = new Policy(fullPolicy, "p1"); + policies.add(newPolicy); + + String template = "permit(principal == ?principal, action == Action::\"View_Photo\", resource in Album::\"Vacation\");"; + Policy policyTemplate = new Policy(template, "t0"); + templates.add(policyTemplate); + + Entity principal = new Entity(principalType.of("Alice"), new HashMap<>(), new HashSet<>()); + LinkValue principalLinkValue = new LinkValue("?principal", principal.getEUID()); + linkValueList.add(principalLinkValue); + + TemplateLink templateLink = new TemplateLink("t0", "tl0", linkValueList); + templateLinks.add(templateLink); + + return new PolicySet(policies, templates, templateLinks); + } + + public static PolicySet buildInvalidPolicySet() { + EntityTypeName principalType = EntityTypeName.parse("User").get(); + Set policies = new HashSet<>(); + Set templates = new HashSet<>(); + ArrayList templateLinks = new ArrayList(); + ArrayList linkValueList = new ArrayList<>(); + + String fullPolicy = + "permit(prinipal == User::\"Bob\", action == Action::\"View_Photo\", resource in Album::\"Vacation\");"; + Policy newPolicy = new Policy(fullPolicy, "p1"); + policies.add(newPolicy); + + String template = "permit(principal, action == Action::\"View_Photo\", resource in Album::\"Vacation\");"; + Policy policyTemplate = new Policy(template, "t0"); + templates.add(policyTemplate); + + Entity principal = new Entity(principalType.of("Alice"), new HashMap<>(), new HashSet<>()); + LinkValue principalLinkValue = new LinkValue("?principal", principal.getEUID()); + linkValueList.add(principalLinkValue); + + TemplateLink templateLink = new TemplateLink("t0", "tl0", linkValueList); + templateLinks.add(templateLink); + + return new PolicySet(policies, templates, templateLinks); + } + } diff --git a/CedarJavaFFI/src/interface.rs b/CedarJavaFFI/src/interface.rs index bab88455..3b8e1c0c 100644 --- a/CedarJavaFFI/src/interface.rs +++ b/CedarJavaFFI/src/interface.rs @@ -17,7 +17,8 @@ use cedar_policy::entities_errors::EntitiesError; #[cfg(feature = "partial-eval")] use cedar_policy::ffi::is_authorized_partial_json_str; use cedar_policy::ffi::{ - schema_to_json, schema_to_text, Schema as FFISchema, SchemaToJsonAnswer, SchemaToTextAnswer, + schema_to_json, schema_to_text, PolicySet as PolicySetFFI, Schema as FFISchema, + SchemaToJsonAnswer, SchemaToTextAnswer, }; use cedar_policy::{ ffi::{is_authorized_json_str, validate_json_str}, @@ -270,6 +271,32 @@ fn parse_policy_internal<'a>( } } +#[jni_fn("com.cedarpolicy.model.policy.PolicySet")] +pub fn policySetToJson<'a>(mut env: JNIEnv<'a>, _: JClass, policies_jstr: JString<'a>) -> jvalue { + match policy_set_to_json_internal(&mut env, policies_jstr) { + Err(e) => jni_failed(&mut env, e.as_ref()), + Ok(policies_set) => policies_set.as_jni(), + } +} + +fn policy_set_to_json_internal<'a>( + env: &mut JNIEnv<'a>, + policy_set_jstr: JString<'a>, +) -> Result> { + if policy_set_jstr.is_null() { + raise_npe(env) + } else { + let policy_set_jstring = env.get_string(&policy_set_jstr)?; + let policy_set_string = String::from(policy_set_jstring); + let policy_set_ffi: PolicySetFFI = serde_json::from_str(&policy_set_string)?; + let policy_set = policy_set_ffi + .parse() + .map_err(|err| format!("Error parsing policy set: {:?}", err))?; + let policy_set_json = serde_json::to_string(&policy_set.to_json().unwrap())?; + Ok(JValueGen::Object(env.new_string(&policy_set_json)?.into())) + } +} + #[jni_fn("com.cedarpolicy.model.policy.PolicySet")] pub fn parsePoliciesJni<'a>(mut env: JNIEnv<'a>, _: JClass, policies_jstr: JString<'a>) -> jvalue { match parse_policies_internal(&mut env, policies_jstr) { @@ -812,6 +839,7 @@ pub(crate) mod jvm_based_tests { env.exception_check().unwrap(), "Expected Java exception due to a null input" ); + env.exception_clear().unwrap(); } #[track_caller] @@ -893,6 +921,7 @@ pub(crate) mod jvm_based_tests { env.exception_check().unwrap(), "Expected java exception due to a null input" ); + env.exception_clear().unwrap(); } #[test] fn parse_policy_internal_success_basic() { @@ -942,6 +971,7 @@ pub(crate) mod jvm_based_tests { env.exception_check().unwrap(), "Expected java exception due to a null input" ); + env.exception_clear().unwrap(); } #[test] fn parse_policy_internal_null() { @@ -953,6 +983,7 @@ pub(crate) mod jvm_based_tests { env.exception_check().unwrap(), "Expected Java exception due to a null input" ); + env.exception_clear().unwrap(); } #[test] fn parse_policy_template_valid_test() { @@ -993,6 +1024,7 @@ pub(crate) mod jvm_based_tests { env.exception_check().unwrap(), "Expected Java exception due to a null input" ); + env.exception_clear().unwrap(); } #[test] fn from_json_test_valid() { @@ -1063,6 +1095,7 @@ pub(crate) mod jvm_based_tests { env.exception_check().unwrap(), "Expected Java exception due to a null input" ); + env.exception_clear().unwrap(); } #[test] @@ -1161,6 +1194,7 @@ pub(crate) mod jvm_based_tests { env.exception_check().unwrap(), "Expected Java exception due to a null input" ); + env.exception_clear().unwrap(); } #[test] fn template_effect_jni_internal_permit_test() { @@ -1203,6 +1237,7 @@ pub(crate) mod jvm_based_tests { env.exception_check().unwrap(), "Expected Java exception due to a null input" ); + env.exception_clear().unwrap(); } } mod map_tests { @@ -1352,6 +1387,7 @@ pub(crate) mod jvm_based_tests { env.exception_check().unwrap(), "Expected Java exception due to a null input" ); + env.exception_clear().unwrap(); } #[test] fn parse_cedar_schema_internal_invalid() { @@ -1404,6 +1440,7 @@ pub(crate) mod jvm_based_tests { env.exception_check().unwrap(), "Expected Java exception due to a null input" ); + env.exception_clear().unwrap(); } #[test] fn test_get_template_annotations_internal_invalid_template() { @@ -1464,7 +1501,7 @@ pub(crate) mod jvm_based_tests { fn get_cedar_schema_internal_invalid() { let mut env = JVM.attach_current_thread().unwrap(); let json_input = r#" - + entity User = { name: String, age?: Long, @@ -1575,4 +1612,142 @@ pub(crate) mod jvm_based_tests { ); } } + + mod policyset_tests { + use super::*; + #[test] + fn policyset_to_json_valid_test() { + let mut env = JVM.attach_current_thread().unwrap(); + let policyset_str = r#"{ + "staticPolicies": { + "p1": "permit(principal == User::\"Bob\", action == Action::\"View_Photo\", resource in Album::\"Vacation\");" + }, + "templates": { + "t0": "permit(principal == ?principal, action == Action::\"View_Photo\", resource in Album::\"Vacation\");" + }, + "templateLinks": [{ + "templateId": "t0", + "newId": "tl0", + "values": { + "?principal": {"type": "User", "id": "Alice"} + } + }] + }"#; + + let policyset_jstr = env.new_string(policyset_str).unwrap(); + let policyset_json_result = policy_set_to_json_internal(&mut env, policyset_jstr); + + assert!(policyset_json_result.is_ok()); + + let policyset_json_jstr = + JString::cast(&mut env, policyset_json_result.unwrap().l().unwrap()).unwrap(); + let actual_json_str = String::from(env.get_string(&policyset_json_jstr).unwrap()); + + let expected_json = serde_json::json!({ + "templates": { + "t0": { + "effect": "permit", + "principal": { + "op": "==", + "slot": "?principal" + }, + "action": { + "op": "==", + "entity": { + "type": "Action", + "id": "View_Photo" + } + }, + "resource": { + "op": "in", + "entity": { + "type": "Album", + "id": "Vacation" + } + }, + "conditions": [] + } + }, + "staticPolicies": { + "p1": { + "effect": "permit", + "principal": { + "op": "==", + "entity": { + "type": "User", + "id": "Bob" + } + }, + "action": { + "op": "==", + "entity": { + "type": "Action", + "id": "View_Photo" + } + }, + "resource": { + "op": "in", + "entity": { + "type": "Album", + "id": "Vacation" + } + }, + "conditions": [] + } + }, + "templateLinks": [{ + "templateId": "t0", + "newId": "tl0", + "values": { + "?principal": { + "__entity": { + "type": "User", + "id": "Alice" + } + } + } + }] + }); + + let actual_json: serde_json::Value = serde_json::from_str(&actual_json_str) + .expect("Failed to parse actual JSON response"); + + assert_eq!( + expected_json, actual_json, + "PolicySet JSON output does not match expected structure" + ); + } + + #[test] + fn policyset_to_json_invalid_test() { + let mut env = JVM.attach_current_thread().unwrap(); + let policyset_str = r#"{ + "staticPolicies": { + "p1": "permit(principal == User::\"Bob\", act == Action::\"View_Photo\", resrce in Album::\"Vacation\");" + }, + "templates": { + "t0": "permit(principal == ? + } + } + }"#; + + let policyset_jstr = env.new_string(policyset_str).unwrap(); + let policyset_json_result = policy_set_to_json_internal(&mut env, policyset_jstr); + + assert!( + policyset_json_result.is_err(), + "Expected error when converting a malformed policy set" + ); + + let null_str = JString::from(JObject::null()); + let null_result = policy_set_to_json_internal(&mut env, null_str); + + assert!(null_result.is_ok(), "Expected Ok on null input"); + assert!( + env.exception_check().unwrap(), + "Expected Java exception due to a null input" + ); + env.exception_clear().unwrap(); + } + } } diff --git a/CedarJavaFFI/src/jlist.rs b/CedarJavaFFI/src/jlist.rs index f1300ddd..49a34130 100644 --- a/CedarJavaFFI/src/jlist.rs +++ b/CedarJavaFFI/src/jlist.rs @@ -168,6 +168,7 @@ mod jlist_tests { let result = list.get(&mut env, 1); assert!(result.is_err()); + env.exception_clear().unwrap(); } #[test] @@ -247,6 +248,7 @@ mod jlist_tests { Ok(_) => panic!("Expected error, but got Ok"), }; assert!(result.is_err()); + env.exception_clear().unwrap(); } #[test] fn size_method_works() {