diff --git a/CedarJava/src/main/java/com/cedarpolicy/model/slice/BasicSlice.java b/CedarJava/src/main/java/com/cedarpolicy/model/slice/BasicSlice.java index 7d126cc9..58cd7ed5 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/model/slice/BasicSlice.java +++ b/CedarJava/src/main/java/com/cedarpolicy/model/slice/BasicSlice.java @@ -90,7 +90,16 @@ public BasicSlice(Set policies, Set entities) { this(policies, entities, Collections.emptySet(), Collections.emptyList()); } - + /** + * Construct a Slice from Entity and PolicySet objects. + * + * @param policySet Set of policies. + * @param entities Set of entities. + */ + @SuppressFBWarnings + public BasicSlice(PolicySet policySet, Set entities) { + this(policySet.policies, entities, policySet.templates, policySet.templateInstantiations); + } @Override @SuppressFBWarnings diff --git a/CedarJava/src/main/java/com/cedarpolicy/model/slice/Policy.java b/CedarJava/src/main/java/com/cedarpolicy/model/slice/Policy.java index 7c9e21d2..985e758c 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/model/slice/Policy.java +++ b/CedarJava/src/main/java/com/cedarpolicy/model/slice/Policy.java @@ -69,6 +69,10 @@ public String toString() { return "// Policy ID: " + policyID + "\n" + policySrc; } + public String toJson() throws InternalException, NullPointerException { + return toJsonJni(policySrc); + } + public static Policy parseStaticPolicy(String policyStr) throws InternalException, NullPointerException { var policyText = parsePolicyJni(policyStr); return new Policy(policyText, null); @@ -97,4 +101,6 @@ public static boolean validateTemplateLinkedPolicy(Policy p, EntityUID principal private static native String parsePolicyJni(String policyStr) throws InternalException, NullPointerException; private static native String parsePolicyTemplateJni(String policyTemplateStr) throws InternalException, NullPointerException; private static native boolean validateTemplateLinkedPolicyJni(String templateText, EntityUID principal, EntityUID resource) throws InternalException, NullPointerException; + + private native String toJsonJni(String policyStr) throws InternalException, NullPointerException; } diff --git a/CedarJava/src/main/java/com/cedarpolicy/model/slice/PolicySet.java b/CedarJava/src/main/java/com/cedarpolicy/model/slice/PolicySet.java new file mode 100644 index 00000000..c22bbde3 --- /dev/null +++ b/CedarJava/src/main/java/com/cedarpolicy/model/slice/PolicySet.java @@ -0,0 +1,97 @@ +/* + * Copyright 2022-2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.cedarpolicy.model.slice; + +import com.cedarpolicy.loader.LibraryLoader; + +import com.cedarpolicy.model.exception.InternalException; + +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +/** Policy Set containing policies in the Cedar language. */ +public class PolicySet { + static { + LibraryLoader.loadLibrary(); + } + + /** Policy set. */ + public Set policies; + + /** Template Instantiations. */ + public List templateInstantiations; + + /** Templates. */ + public Set templates; + + public PolicySet() { + this.policies = Collections.emptySet(); + this.templates = Collections.emptySet(); + this.templateInstantiations = Collections.emptyList(); + } + + public PolicySet(Set policies) { + this.policies = policies; + this.templates = Collections.emptySet(); + this.templateInstantiations = Collections.emptyList(); + } + + public PolicySet(Set policies, Set templates) { + this.policies = policies; + this.templates = templates; + this.templateInstantiations = Collections.emptyList(); + } + + public PolicySet(Set policies, Set templates, List templateInstantiations) { + this.policies = policies; + this.templates = templates; + this.templateInstantiations = templateInstantiations; + } + + /** + * Parse multiple policies and templates from a file into a PolicySet. + * @param filePath the path to the file containing the policies + * @return a PolicySet containing the parsed policies + * @throws InternalException + * @throws IOException + * @throws NullPointerException + */ + public static PolicySet parsePolicies(Path filePath) throws InternalException, IOException { + // Read the file contents into a String + String policiesString = Files.readString(filePath); + return parsePolicies(policiesString); + } + + /** + * Parse a string containing multiple policies and templates into a PolicySet. + * @param policiesString the string containing the policies + * @return a PolicySet containing the parsed policies + * @throws InternalException + * @throws NullPointerException + */ + public static PolicySet parsePolicies(String policiesString) throws InternalException { + PolicySet policySet = parsePoliciesJni(policiesString); + return policySet; + } + + private static native PolicySet parsePoliciesJni(String policiesStr) throws InternalException, NullPointerException; +} diff --git a/CedarJava/src/test/java/com/cedarpolicy/PolicyFormatterTests.java b/CedarJava/src/test/java/com/cedarpolicy/PolicyFormatterTests.java index 2e26e19b..abac4d1e 100644 --- a/CedarJava/src/test/java/com/cedarpolicy/PolicyFormatterTests.java +++ b/CedarJava/src/test/java/com/cedarpolicy/PolicyFormatterTests.java @@ -24,6 +24,15 @@ public void testPoliciesStrToPretty() throws Exception { assertEquals(formattedCedarPolicy, PolicyFormatter.policiesStrToPretty(unformattedCedarPolicy)); } + @Test + public void testPoliciesStrToPrettyMalformedCedarPolicy() throws Exception { + String malformedCedarPolicy = Files.readString( + Path.of(TEST_RESOURCES_DIR + "malformed_policy_set.cedar")); + + assertThrows(InternalException.class, + () -> PolicyFormatter.policiesStrToPretty(malformedCedarPolicy)); + } + @Test public void testPoliciesStrToPrettyNullSafety() { assertThrows(NullPointerException.class, () -> PolicyFormatter.policiesStrToPretty(null)); diff --git a/CedarJava/src/test/java/com/cedarpolicy/PolicySetTests.java b/CedarJava/src/test/java/com/cedarpolicy/PolicySetTests.java new file mode 100644 index 00000000..597448bf --- /dev/null +++ b/CedarJava/src/test/java/com/cedarpolicy/PolicySetTests.java @@ -0,0 +1,69 @@ +package com.cedarpolicy; + +import com.cedarpolicy.model.exception.InternalException; +import com.cedarpolicy.model.slice.Policy; +import com.cedarpolicy.model.slice.PolicySet; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertEquals; + +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) { + 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 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) { + assertNotNull(p.policySrc); + } + assertEquals(1, policySet.policies.size()); + assertEquals(0, policySet.templates.size()); + for (Policy p: policySet2.policies) { + assertNotNull(p.policySrc); + } + assertEquals(1, policySet2.policies.size()); + assertEquals(0, policySet2.templates.size()); + } + + @Test + public void parseTemplatesTests() throws InternalException, IOException { + 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) { + assertNotNull(p.policySrc); + } + assertEquals(1, policySet.templates.size()); + } + + @Test + public void parsePoliciesExceptionTests() throws InternalException, IOException { + assertThrows(IOException.class, () -> { + PolicySet.parsePolicies(Path.of("nonExistentFilePath.cedar")); + }); + assertThrows(InternalException.class, () -> { + PolicySet.parsePolicies(Path.of(TEST_RESOURCES_DIR + "malformed_policy_set.cedar")); + }); + } +} diff --git a/CedarJava/src/test/java/com/cedarpolicy/PolicyTests.java b/CedarJava/src/test/java/com/cedarpolicy/PolicyTests.java index 407285da..6bc55e61 100644 --- a/CedarJava/src/test/java/com/cedarpolicy/PolicyTests.java +++ b/CedarJava/src/test/java/com/cedarpolicy/PolicyTests.java @@ -6,9 +6,11 @@ import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; public class PolicyTests { @Test @@ -16,7 +18,7 @@ public void parseStaticPolicyTests() { assertDoesNotThrow(() -> { var policy1 = Policy.parseStaticPolicy("permit(principal, action, resource);"); var policy2 = Policy.parseStaticPolicy("permit(principal, action, resource) when { principal has x && principal.x == 5};"); - assertNotEquals(policy1.policyID.equals(policy2.policyID), true); + assertNotEquals(policy1.policyID, policy2.policyID); }); assertThrows(InternalException.class, () -> { Policy.parseStaticPolicy("permit();"); @@ -32,7 +34,7 @@ public void parsePolicyTemplateTests() { assertDoesNotThrow(() -> { String tbody = "permit(principal == ?principal, action, resource in ?resource);"; var template = Policy.parsePolicyTemplate(tbody); - assertTrue(template.policySrc.equals(tbody)); + assertEquals(tbody, template.policySrc); }); // ?resource slot shouldn't be used in the principal scope assertThrows(InternalException.class, () -> { @@ -84,4 +86,34 @@ public void validateTemplateLinkedPolicyFailsWhenExpected() { Policy.validateTemplateLinkedPolicy(p2, principal, resource); }); } + + @Test + public void staticPolicyToJsonTests() throws InternalException { + assertThrows(NullPointerException.class, () -> { + Policy p = new Policy(null, null); + p.toJson(); + }); + assertThrows(InternalException.class, () -> { + Policy p = new Policy("permit();", null); + p.toJson(); + }); + + Policy p = Policy.parseStaticPolicy("permit(principal, action, resource);"); + String actualJson = p.toJson(); + String expectedJson = "{\"effect\":\"permit\",\"principal\":{\"op\":\"All\"},\"action\":{\"op\":\"All\"}," + + "\"resource\":{\"op\":\"All\"},\"conditions\":[]}"; + assertEquals(expectedJson, actualJson); + } + + @Test + public void policyTemplateToJsonFailureTests() throws InternalException { + try { + String tbody = "permit(principal == ?principal, action, resource in ?resource);"; + Policy template = Policy.parsePolicyTemplate(tbody); + template.toJson(); + fail("Expected InternalException"); + } catch (InternalException e) { + assertTrue(e.getMessage().contains("expected a static policy, got a template containing the slot ?resource")); + } + } } diff --git a/CedarJava/src/test/java/com/cedarpolicy/SharedIntegrationTests.java b/CedarJava/src/test/java/com/cedarpolicy/SharedIntegrationTests.java index a4e5e817..a672ee4a 100644 --- a/CedarJava/src/test/java/com/cedarpolicy/SharedIntegrationTests.java +++ b/CedarJava/src/test/java/com/cedarpolicy/SharedIntegrationTests.java @@ -27,10 +27,12 @@ import com.cedarpolicy.model.AuthorizationResponse.Decision; import com.cedarpolicy.model.exception.AuthException; import com.cedarpolicy.model.exception.BadRequestException; +import com.cedarpolicy.model.exception.InternalException; import com.cedarpolicy.model.schema.Schema; import com.cedarpolicy.model.slice.BasicSlice; import com.cedarpolicy.model.slice.Entity; import com.cedarpolicy.model.slice.Policy; +import com.cedarpolicy.model.slice.PolicySet; import com.cedarpolicy.model.slice.Slice; import com.cedarpolicy.value.EntityUID; import com.cedarpolicy.serializer.JsonEUID; @@ -58,7 +60,6 @@ import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.junit.jupiter.api.DynamicContainer; import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.TestFactory; @@ -211,10 +212,10 @@ private static class JsonEntity { /** * This method is the main entry point for JUnit. It returns a list of containers, which contain - * tests for junit to run. JUnit will run all the test returned from this method. + * tests for junit to run. JUnit will run all the tests returned from this method. */ @TestFactory - public List integrationTestsFromJson() throws IOException { + public List integrationTestsFromJson() throws InternalException, IOException { List tests = new ArrayList<>(); //If we can't find the `cedar` package, don't try to load integration tests. if (Files.notExists(resolveIntegrationTestPath("corpus_tests"))) { @@ -228,25 +229,23 @@ public List integrationTestsFromJson() throws IOException { try (Stream stream = Files.list(resolveIntegrationTestPath("corpus_tests"))) { stream - // ignore non-JSON files - .filter(path -> path.getFileName().toString().endsWith(".json")) - // ignore files that start with policies_, entities_, or schema_ - .filter( - path -> - !path.getFileName().toString().startsWith("policies_") - && !path.getFileName().toString().startsWith("entities_") - && !path.getFileName().toString().startsWith("schema_")) - // add the test - .forEach( - path -> { - try { - tests.add(loadJsonTests(path.toAbsolutePath().toString())); - } catch (final IOException e) { - // inside the forEach we can't throw checked exceptions, but we - // can throw this unchecked exception - throw new UncheckedIOException(e); - } - }); + // ignore non-JSON files + .filter(path -> path.getFileName().toString().endsWith(".json")) + // ignore files that end with `.entities.json` + .filter(path -> !path.getFileName().toString().endsWith(".entities.json")) + // add the test + .forEach( + path -> { + try { + tests.add(loadJsonTests(path.toAbsolutePath().toString())); + } catch (final IOException e) { + // inside the forEach we can't throw checked exceptions, but we + // can throw this unchecked exception + throw new UncheckedIOException(e); + } catch (final InternalException e) { + throw new RuntimeException(e); + } + }); } return tests; } @@ -256,14 +255,14 @@ public List integrationTestsFromJson() throws IOException { * test, and all the test in the json file are grouped into the returned container. */ @SuppressFBWarnings("NP_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD") - private DynamicContainer loadJsonTests(String jsonFile) throws IOException { + private DynamicContainer loadJsonTests(String jsonFile) throws InternalException, IOException { JsonTest test; try (InputStream jsonIn = new FileInputStream(resolveIntegrationTestPath(jsonFile).toFile())) { test = OBJECT_MAPPER.reader().readValue(jsonIn, JsonTest.class); } Set entities = loadEntities(test.entities); - Set policies = loadPolicies(test.policies); + PolicySet policySet = PolicySet.parsePolicies(resolveIntegrationTestPath(test.policies)); Schema schema = loadSchema(test.schema); return DynamicContainer.dynamicContainer( @@ -272,7 +271,7 @@ private DynamicContainer loadJsonTests(String jsonFile) throws IOException { Stream.of(DynamicTest.dynamicTest( jsonFile + ": validate", () -> - executeJsonValidationTest(policies, schema, test.shouldValidate))), + executeJsonValidationTest(policySet.policies, schema, test.shouldValidate))), test.queries.stream() .map( request -> @@ -280,61 +279,10 @@ private DynamicContainer loadJsonTests(String jsonFile) throws IOException { jsonFile + ": " + request.desc, () -> executeJsonRequestTest( - entities, policies, request, + entities, policySet, request, schema))))); } - /** - * Load all policies from the policy file. The policy file path must be relative to the shared - * integration test root. This should be the case if the path was obtained from a JsonTest - * object. Extra processing is required because the test format does not include policy ids, and - * does not explicit separate policies in a file other than by semicolons. - */ - private Set loadPolicies(String policiesFile) throws IOException { - String policiesSrc = String.join("\n", Files.readAllLines(resolveIntegrationTestPath(policiesFile))); - - // Get a list of the policy sources for the individual policies in the - // file by splitting the full policy source on semicolons. This will - // break if a semicolon shows up in a string, eid, or comment. - String[] policyStrings = policiesSrc.split(";"); - // Some of the corpus tests contain semicolons in strings and/or eids. - // A simple way to check if the code above did the wrong thing in this case - // is to check for unmatched, unescaped quotes in the resulting policies. - for (String policyString : policyStrings) { - if (hasUnmatchedQuote(policyString)) { - policyStrings = null; - } - } - - Set policies = new HashSet<>(); - if (policyStrings == null) { - // This case will only be reached for corpus tests. - // The corpus tests all consist of a single policy, so it is fine to use - // the full policy source as a single policy. - policies.add(new Policy(policiesSrc, "policy0")); - } else { - for (int i = 0; i < policyStrings.length; i++) { - // The policy source doesn't include an explicit policy id, but the expected output - // implicitly assumes policies are numbered by their position in file. - String policyId = "policy" + i; - String policySrc = policyStrings[i]; - if (!policySrc.trim().isEmpty()) { - policies.add(new Policy(policySrc + ";", policyId)); - } - } - } - return policies; - } - - /** Check for unmatched quotes. */ - private Boolean hasUnmatchedQuote(String s) { - // Ignore escaped quotes, i.e. \" - // Note that backslashes in the regular expression have to be double escaped. - String new_s = s.replaceAll("\\\\\"", ""); - long count = new_s.chars().filter(ch -> ch == '\"').count(); - return (count % 2 == 1); - } - /** Load the schema file. */ private Schema loadSchema(String schemaFile) throws IOException { try (InputStream schemaIn = @@ -401,7 +349,7 @@ private void executeJsonValidationTest(Set policies, Schema schema, Bool * that the result is equal to the expected result. */ private void executeJsonRequestTest( - Set entities, Set policies, JsonRequest request, Schema schema) throws AuthException { + Set entities, PolicySet policySet, JsonRequest request, Schema schema) throws AuthException { AuthorizationEngine auth = new BasicAuthorizationEngine(); AuthorizationRequest authRequest = new AuthorizationRequest( @@ -411,7 +359,7 @@ private void executeJsonRequestTest( Optional.of(request.context), Optional.of(schema), request.enable_request_validation); - Slice slice = new BasicSlice(policies, entities); + Slice slice = new BasicSlice(policySet, entities); try { AuthorizationResponse response = auth.isAuthorized(authRequest, slice); diff --git a/CedarJava/src/test/resources/malformed_policy_set.cedar b/CedarJava/src/test/resources/malformed_policy_set.cedar new file mode 100644 index 00000000..80972386 --- /dev/null +++ b/CedarJava/src/test/resources/malformed_policy_set.cedar @@ -0,0 +1,13 @@ +@id("Proper Policy") +permit ( + principal == User::"Matt", + action == Action::"view", + resource == Photo::"Husky.jpg" +); + +@id("Malformed Policy") +forbid ( + principal == User::"Liam", + action, + resource = Photo::"Husky.jpg" +); \ No newline at end of file diff --git a/CedarJava/src/test/resources/policies.cedar b/CedarJava/src/test/resources/policies.cedar new file mode 100644 index 00000000..b0ada812 --- /dev/null +++ b/CedarJava/src/test/resources/policies.cedar @@ -0,0 +1,13 @@ +@id("Policy #1") +permit ( + principal in UserGroup::"friends", + action == Action::"view", + resource == Photo::"Husky.jpg" +); + +@id("Policy #2") +forbid ( + principal == User::"Matt", + action, + resource == Photo::"Husky.jpg" +); \ No newline at end of file diff --git a/CedarJava/src/test/resources/template.cedar b/CedarJava/src/test/resources/template.cedar new file mode 100644 index 00000000..52d785d7 --- /dev/null +++ b/CedarJava/src/test/resources/template.cedar @@ -0,0 +1,19 @@ +@id("Policy #1") +permit ( + principal == User::"Aaron", + action == Action::"view", + resource == Photo::"SomePhoto.jpg" +); + +@id("Policy #2") +permit (principal == User::"Josh", action == Action::"comment", resource == Photo::"SomePhoto.jpg"); + +@id("Template #1") +permit ( + principal in ?principal, + action in [Action::"view", Action::"comment"], + resource in ?resource +) +unless { + resource.tag == "private" +}; \ No newline at end of file diff --git a/CedarJavaFFI/src/interface.rs b/CedarJavaFFI/src/interface.rs index c115c012..a1aab64d 100644 --- a/CedarJavaFFI/src/interface.rs +++ b/CedarJavaFFI/src/interface.rs @@ -33,7 +33,8 @@ use serde::{Deserialize, Serialize}; use std::{collections::HashMap, error::Error, str::FromStr, thread}; use crate::{ - objects::{JEntityId, JEntityTypeName, JEntityUID, Object}, + jset::Set, + objects::{JEntityId, JEntityTypeName, JEntityUID, JPolicy, Object}, utils::raise_npe, }; @@ -230,6 +231,77 @@ fn parse_policy_internal<'a>( } } +#[jni_fn("com.cedarpolicy.model.slice.PolicySet")] +pub fn parsePoliciesJni<'a>(mut env: JNIEnv<'a>, _: JClass, policies_jstr: JString<'a>) -> jvalue { + match parse_policies_internal(&mut env, policies_jstr) { + Err(e) => jni_failed(&mut env, e.as_ref()), + Ok(policies_set) => policies_set.as_jni(), + } +} + +fn parse_policies_internal<'a>( + env: &mut JNIEnv<'a>, + policies_jstr: JString<'a>, +) -> Result> { + 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()); + let policy_text = format!("{}", policy); + let java_policy_object = JPolicy::new( + env, + &env.new_string(&policy_text)?.into(), + &env.new_string(&policy_id)?.into(), + )?; + 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)?.into(), + &env.new_string(&policy_id)?.into(), + )?; + 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.into())) + } +} + +fn create_java_policy_set<'a>( + env: &mut JNIEnv<'a>, + policies_java_hash_set: &JObject<'a>, + templates_java_hash_set: &JObject<'a>, +) -> JObject<'a> { + env.new_object( + "com/cedarpolicy/model/slice/PolicySet", + &"(Ljava/util/Set;Ljava/util/Set;)V", + &[ + JValueGen::Object(&policies_java_hash_set), + JValueGen::Object(&templates_java_hash_set), + ], + ) + .expect("Failed to create new PolicySet object") +} + #[jni_fn("com.cedarpolicy.model.slice.Policy")] pub fn parsePolicyTemplateJni<'a>( mut env: JNIEnv<'a>, @@ -317,6 +389,26 @@ fn validate_template_linked_policy_internal<'a>( } } +#[jni_fn("com.cedarpolicy.model.slice.Policy")] +pub fn toJsonJni<'a>(mut env: JNIEnv<'a>, _: JClass, policy_jstr: JString<'a>) -> jvalue { + match to_json_internal(&mut env, policy_jstr) { + Err(e) => jni_failed(&mut env, e.as_ref()), + Ok(policy_json) => policy_json.as_jni(), + } +} + +fn to_json_internal<'a>(env: &mut JNIEnv<'a>, policy_jstr: JString<'a>) -> Result> { + if policy_jstr.is_null() { + raise_npe(env) + } else { + let policy_jstring = env.get_string(&policy_jstr)?; + let policy_string = String::from(policy_jstring); + let policy = Policy::from_str(&policy_string)?; + let policy_json = serde_json::to_string(&policy.to_json().unwrap())?; + Ok(JValueGen::Object(env.new_string(&policy_json)?.into())) + } +} + #[jni_fn("com.cedarpolicy.value.EntityIdentifier")] pub fn getEntityIdentifierRepr<'a>(mut env: JNIEnv<'a>, _: JClass, obj: JObject<'a>) -> jvalue { match get_entity_identifier_repr_internal(&mut env, obj) { diff --git a/CedarJavaFFI/src/jset.rs b/CedarJavaFFI/src/jset.rs new file mode 100644 index 00000000..9b707ac8 --- /dev/null +++ b/CedarJavaFFI/src/jset.rs @@ -0,0 +1,46 @@ +use std::marker::PhantomData; + +use crate::{objects::Object, utils::Result}; +use jni::{ + objects::{JObject, JValueGen}, + JNIEnv, +}; + +/// Typed wrapper for Java sets +/// (java.util.Set) +#[derive(Debug)] +pub struct Set<'a, T> { + /// Underlying Java object + obj: JObject<'a>, + /// ZST for tracking type info + marker: PhantomData, + /// The size of this set + size: i32, +} + +impl<'a, T: Object<'a>> Set<'a, T> { + /// Construct an empty hash set, which will serve as a set + pub fn new(env: &mut JNIEnv<'a>) -> Result { + let obj = env.new_object("java/util/HashSet", "()V", &[])?; + + Ok(Self { + obj, + marker: PhantomData, + size: 0, + }) + } + + /// Add an item to the set + pub fn add(&mut self, env: &mut JNIEnv<'a>, v: T) -> Result<()> { + let value = JValueGen::Object(v.as_ref()); + env.call_method(&self.obj, "add", "(Ljava/lang/Object;)Z", &[value])?; + self.size += 1; + Ok(()) + } +} + +impl<'a, T> AsRef> for Set<'a, T> { + fn as_ref(&self) -> &JObject<'a> { + &self.obj + } +} diff --git a/CedarJavaFFI/src/lib.rs b/CedarJavaFFI/src/lib.rs index 4abf7c8b..58cc3aa5 100644 --- a/CedarJavaFFI/src/lib.rs +++ b/CedarJavaFFI/src/lib.rs @@ -17,6 +17,7 @@ #![forbid(unsafe_code)] mod interface; mod jlist; +mod jset; mod objects; mod utils; diff --git a/CedarJavaFFI/src/objects.rs b/CedarJavaFFI/src/objects.rs index 917e8d19..49842a25 100644 --- a/CedarJavaFFI/src/objects.rs +++ b/CedarJavaFFI/src/objects.rs @@ -339,3 +339,44 @@ impl<'a> AsRef> for JEntityUID<'a> { &self.obj } } + +/// Typed wrapper for Policy objects +/// (com.cedarpolicy.model.slice.Policy) +pub struct JPolicy<'a> { + obj: JObject<'a>, +} + +impl<'a> JPolicy<'a> { + /// Construct a new Policy object + pub fn new( + env: &mut JNIEnv<'a>, + policy_string: &JString, + policy_id_string: &JString, + ) -> Result { + let obj = env + .new_object( + "com/cedarpolicy/model/slice/Policy", + &"(Ljava/lang/String;Ljava/lang/String;)V", + &[ + JValueGen::Object(&policy_string), + JValueGen::Object(&policy_id_string), + ], + ) + .expect("Failed to create new Policy object"); + + Ok(Self { obj }) + } +} + +impl<'a> Object<'a> for JPolicy<'a> { + fn cast(env: &mut JNIEnv<'a>, obj: JObject<'a>) -> Result { + assert_is_class(env, &obj, "com/cedarpolicy/model/slice/Policy")?; + Ok(Self { obj }) + } +} + +impl<'a> AsRef> for JPolicy<'a> { + fn as_ref(&self) -> &JObject<'a> { + &self.obj + } +}