diff --git a/CedarJava/build.gradle b/CedarJava/build.gradle index 27ac90cc..6f80f99a 100644 --- a/CedarJava/build.gradle +++ b/CedarJava/build.gradle @@ -183,8 +183,8 @@ tasks.register('downloadIntegrationTests', Download) { group 'Build' description 'Downloads Cedar repository with integration tests.' - src 'https://codeload.github.com/cedar-policy/cedar/zip/main' - dest layout.buildDirectory.file('cedar-main.zip') + src 'https://codeload.github.com/cedar-policy/cedar-integration-tests/zip/main' + dest layout.buildDirectory.file('cedar-integration-tests-main.zip') overwrite false } @@ -193,19 +193,28 @@ tasks.register('extractIntegrationTests', Copy) { description 'Extracts Cedar integration tests.' dependsOn('downloadIntegrationTests') - from zipTree(layout.buildDirectory.file('cedar-main.zip')) + from zipTree(layout.buildDirectory.file('cedar-integration-tests-main.zip')) into layout.buildDirectory.dir('resources/test') } +tasks.register('extractCorpusTests', Copy) { + group 'Build' + description 'Extracts Cedar corpus tests.' + + dependsOn('extractIntegrationTests') + dependsOn('processTestResources') + from tarTree(layout.buildDirectory.file('resources/test/cedar-integration-tests-main/corpus-tests.tar.gz')) + into layout.buildDirectory.dir('resources/test/cedar-integration-tests-main') +} + tasks.named('test') { useJUnitPlatform() dependsOn('compileFFI') - dependsOn('extractIntegrationTests') + dependsOn('extractCorpusTests') classpath += files(layout.buildDirectory.dir(compiledLibDir)) } test { - //environment "CEDAR_INTEGRATION_TESTS_ROOT", ''set to absolute path of `cedar-integration-tests`' testLogging { events "skipped", "failed", "standardOut", "standardError" showStandardStreams false diff --git a/CedarJava/src/main/java/com/cedarpolicy/AuthorizationEngine.java b/CedarJava/src/main/java/com/cedarpolicy/AuthorizationEngine.java index 22394173..0ec0c444 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/AuthorizationEngine.java +++ b/CedarJava/src/main/java/com/cedarpolicy/AuthorizationEngine.java @@ -81,6 +81,6 @@ public interface AuthorizationEngine { * @return The Cedar language major version supported */ static String getCedarLangVersion() { - return "3.0"; + return "4.0"; } } diff --git a/CedarJava/src/main/java/com/cedarpolicy/model/AuthorizationSuccessResponse.java b/CedarJava/src/main/java/com/cedarpolicy/model/AuthorizationSuccessResponse.java index 86a18040..ad2a1796 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/model/AuthorizationSuccessResponse.java +++ b/CedarJava/src/main/java/com/cedarpolicy/model/AuthorizationSuccessResponse.java @@ -138,7 +138,7 @@ public Decision getDecision() { * * @return list with the policy ids that contributed to the decision */ - public Set getReasons() { + public Set getReason() { return diagnostics.reason; } diff --git a/CedarJava/src/main/java/com/cedarpolicy/model/schema/Schema.java b/CedarJava/src/main/java/com/cedarpolicy/model/schema/Schema.java index 30efbdb7..4c4d2c75 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/model/schema/Schema.java +++ b/CedarJava/src/main/java/com/cedarpolicy/model/schema/Schema.java @@ -21,11 +21,13 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import java.io.IOException; -import java.util.Objects; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; + +import java.util.Optional; /** Represents a schema. */ +@JsonInclude(JsonInclude.Include.NON_EMPTY) public final class Schema { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); @@ -33,74 +35,113 @@ public final class Schema { LibraryLoader.loadLibrary(); } - // The schema after being parsed as a JSON object. - // (Using "json" as the JsonProperty ensures the FFI knows we are using the JSON format. - // The other option is "human".) - @JsonProperty("json") private final JsonNode schemaJson; + /** Is this schema in the JSON or human format */ + @JsonIgnore + public final JsonOrHuman type; + /** This will be present if and only if `type` is `Json`. */ + @JsonProperty("json") + private final Optional schemaJson; + /** This will be present if and only if `type` is `Human`. */ + @JsonProperty("human") + public final Optional schemaText; /** - * Build a Schema from a string containing the JSON source for the model. This constructor will - * fail with an exception if the string does not parse as JSON, but it does not check that the - * parsed JSON object represents a valid schema. - * - * @param schemaJson List of EntityTypes. - * @throws java.io.IOException When any errors are encountered while parsing the authorization - * model json string into json node. + * If `type` is `Json`, `schemaJson` should be present and `schemaText` empty. + * If `type` is `Human`, `schemaText` should be present and `schemaJson` empty. + * This constructor does not check that the input text represents a valid JSON + * or Cedar schema. Use the `parse` function to ensure schema validity. + * + * @param type The schema format used. + * @param schemaJson Optional schema in Cedar's JSON schema format. + * @param schemaText Optional schema in Cedar's human-readable schema format. */ - @SuppressFBWarnings - public Schema(String schemaJson) throws IOException { - if (schemaJson == null) { - throw new NullPointerException("schemaJson"); - } - - this.schemaJson = OBJECT_MAPPER.readTree(schemaJson); + public Schema(JsonOrHuman type, Optional schemaJson, Optional schemaText) { + this.type = type; + this.schemaJson = schemaJson.map(jsonStr -> { + try { + return OBJECT_MAPPER.readTree(jsonStr); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + }); + this.schemaText = schemaText; } /** - * Build a Schema from a json node. This does not check that the parsed JSON object represents a - * valid schema. + * Build a Schema from a json node. This does not check that the parsed JSON + * object represents a valid schema. Use `parse` to check validity. * * @param schemaJson Schema in Cedar's JSON schema format. */ - @SuppressFBWarnings public Schema(JsonNode schemaJson) { if (schemaJson == null) { throw new NullPointerException("schemaJson"); } - - this.schemaJson = schemaJson; + this.type = JsonOrHuman.Json; + this.schemaJson = Optional.of(schemaJson); + this.schemaText = Optional.empty(); } - /** Equals. */ - @Override - public boolean equals(final Object o) { - if (!(o instanceof Schema)) { - return false; + /** + * Build a Schema from a string. This does not check that the string represents + * a valid schema. Use `parse` to check validity. + * + * @param schemaText Schema in Cedar's human-readable schema format. + */ + public Schema(String schemaText) { + if (schemaText == null) { + throw new NullPointerException("schemaText"); } - - final Schema other = (Schema) o; - return schemaJson.equals(other.schemaJson); + this.type = JsonOrHuman.Human; + this.schemaJson = Optional.empty(); + this.schemaText = Optional.of(schemaText); } - /** Hash. */ - @Override - public int hashCode() { - return Objects.hash(schemaJson); - } - - /** Readable string representation. */ public String toString() { - return "Schema(schemaJson=" + schemaJson + ")"; + if (type == JsonOrHuman.Json) { + return "Schema(schemaJson=" + schemaJson.get() + ")"; + } else { + return "Schema(schemaText=" + schemaText.get() + ")"; + } } - public static Schema parse(String schemaStr) throws IOException, InternalException { - var success = parseSchema(schemaStr).equals("Success"); - if (success) { - return new Schema(schemaStr); + /** + * Try to parse a string representing a JSON or Cedar schema. If parsing + * succeeds, return a `Schema`, otherwise raise an exception. + * + * @param type The schema format used. + * @param str Schema text to parse. + * @throws InternalException If parsing fails. + * @throws NullPointerException If the input text is null. + * @return A {@link Schema} that is guaranteed to be valid. + */ + public static Schema parse(JsonOrHuman type, String str) throws InternalException, NullPointerException { + if (type == JsonOrHuman.Json) { + parseJsonSchemaJni(str); + return new Schema(JsonOrHuman.Json, Optional.of(str), Optional.empty()); } else { - throw new IOException("Unable to parse schema"); + parseHumanSchemaJni(str); + return new Schema(JsonOrHuman.Human, Optional.empty(), Optional.of(str)); } + } - private static native String parseSchema(String schemaStr) throws InternalException; + /** Specifies the schema format used. */ + public enum JsonOrHuman { + /** + * Cedar JSON schema format. See https://docs.cedarpolicy.com/schema/json-schema.html + */ + Json, + /** + * Cedar schema format. See https://docs.cedarpolicy.com/schema/human-readable-schema.html + */ + Human + } + + private static native String parseJsonSchemaJni(String schemaJson) throws InternalException, NullPointerException; + + private static native String parseHumanSchemaJni(String schemaText) throws InternalException, NullPointerException; } diff --git a/CedarJava/src/main/java/com/cedarpolicy/value/EntityUID.java b/CedarJava/src/main/java/com/cedarpolicy/value/EntityUID.java index b8e7773f..df5e48bb 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/value/EntityUID.java +++ b/CedarJava/src/main/java/com/cedarpolicy/value/EntityUID.java @@ -38,7 +38,7 @@ public final class EntityUID extends Value { } /** - * Construct an EntityUID from a tyep name and an 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 */ diff --git a/CedarJava/src/test/java/com/cedarpolicy/SchemaTests.java b/CedarJava/src/test/java/com/cedarpolicy/SchemaTests.java index e63b0a96..faac98ac 100644 --- a/CedarJava/src/test/java/com/cedarpolicy/SchemaTests.java +++ b/CedarJava/src/test/java/com/cedarpolicy/SchemaTests.java @@ -1,6 +1,8 @@ package com.cedarpolicy; import com.cedarpolicy.model.schema.Schema; +import com.cedarpolicy.model.schema.Schema.JsonOrHuman; + import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; @@ -8,13 +10,85 @@ public class SchemaTests { @Test - public void parseSchema() { + public void parseJsonSchema() { + assertDoesNotThrow(() -> { + Schema.parse(JsonOrHuman.Json, "{}"); + Schema.parse(JsonOrHuman.Json, """ + { + "Foo::Bar": { + "entityTypes": {}, + "actions": {} + } + } + """); + Schema.parse(JsonOrHuman.Json, """ + { + "": { + "entityTypes": { + "User": { + "shape": { + "type": "Record", + "attributes": { + "name": { + "type": "String", + "required": true + }, + "age": { + "type": "Long", + "required": false + } + } + } + }, + "Photo": { + "memberOfTypes": [ "Album" ] + }, + "Album": {} + }, + "actions": { + "view": { + "appliesTo": { + "principalTypes": ["User"], + "resourceTypes": ["Photo", "Album"] + } + } + } + } + } + """); + }); + assertThrows(Exception.class, () -> { + Schema.parse(JsonOrHuman.Json, "{\"foo\": \"bar\"}"); + Schema.parse(JsonOrHuman.Json, "namespace Foo::Bar;"); + }); + } + + @Test + public void parseHumanSchema() { assertDoesNotThrow(() -> { - Schema.parse("{\"ns1\": {\"entityTypes\": {}, \"actions\": {}}}"); - Schema.parse("{}"); + Schema.parse(JsonOrHuman.Human, ""); + Schema.parse(JsonOrHuman.Human, "namespace Foo::Bar {}"); + Schema.parse(JsonOrHuman.Human, """ + entity User = { + name: String, + age?: Long, + }; + entity Photo in Album; + entity Album; + action view + appliesTo { principal: [User], resource: [Album, Photo] }; + """); }); assertThrows(Exception.class, () -> { - Schema.parse("{\"foo\": \"bar\"}"); + Schema.parse(JsonOrHuman.Human, """ + { + "Foo::Bar": { + "entityTypes" {}, + "actions": {} + } + } + """); + Schema.parse(JsonOrHuman.Human, "namspace Foo::Bar;"); }); } } diff --git a/CedarJava/src/test/java/com/cedarpolicy/SharedIntegrationTests.java b/CedarJava/src/test/java/com/cedarpolicy/SharedIntegrationTests.java index 5fe915a6..d6e0a1c4 100644 --- a/CedarJava/src/test/java/com/cedarpolicy/SharedIntegrationTests.java +++ b/CedarJava/src/test/java/com/cedarpolicy/SharedIntegrationTests.java @@ -37,8 +37,8 @@ import com.cedarpolicy.value.EntityUID; import com.cedarpolicy.serializer.JsonEUID; import com.cedarpolicy.value.Value; +import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; @@ -51,6 +51,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; @@ -82,7 +83,7 @@ private Path resolveIntegrationTestPath(String path) { if (resolved.isAbsolute()) { return resolved; } else { - final URL integrationTestsLocation = getClass().getResource("/cedar-main/cedar-integration-tests"); + final URL integrationTestsLocation = getClass().getResource("/cedar-integration-tests-main"); return integrationTestsLocation == null ? resolved : Paths.get(integrationTestsLocation.getPath(), path); } } @@ -120,7 +121,8 @@ private static class JsonTest { public boolean shouldValidate; /** List of requests with their expected result. */ - public List queries; + @JsonAlias("queries") + public List requests; } /** Directly corresponds to the structure of a request in the JSON formatted tests files. */ @@ -149,7 +151,8 @@ private static class JsonRequest { public Decision decision; /** The expected reason list that should be returned by the authorization engine. */ - public List reasons; + @JsonAlias("reasons") + public List reason; /** The expected error list that should be returned by the authorization engine. */ public List errors; @@ -187,19 +190,20 @@ private static class JsonEntity { * files in this array will be executed as integration tests. */ private static final String[] JSON_TEST_FILES = { - "tests/example_use_cases_doc/1a.json", - "tests/example_use_cases_doc/2a.json", - "tests/example_use_cases_doc/2b.json", - "tests/example_use_cases_doc/2c.json", - "tests/example_use_cases_doc/3a.json", - "tests/example_use_cases_doc/3b.json", - "tests/example_use_cases_doc/3c.json", - "tests/example_use_cases_doc/4a.json", - // "tests/example_use_cases_doc/4c.json", // currently disabled because it uses action attributes - "tests/example_use_cases_doc/4d.json", - "tests/example_use_cases_doc/4e.json", - "tests/example_use_cases_doc/4f.json", - "tests/example_use_cases_doc/5b.json", + "tests/decimal/1.json", + "tests/decimal/2.json", + "tests/example_use_cases/1a.json", + "tests/example_use_cases/2a.json", + "tests/example_use_cases/2b.json", + "tests/example_use_cases/2c.json", + "tests/example_use_cases/3a.json", + "tests/example_use_cases/3b.json", + "tests/example_use_cases/3c.json", + "tests/example_use_cases/4a.json", + "tests/example_use_cases/4d.json", + "tests/example_use_cases/4e.json", + "tests/example_use_cases/4f.json", + "tests/example_use_cases/5b.json", "tests/ip/1.json", "tests/ip/2.json", "tests/ip/3.json", @@ -218,25 +222,17 @@ private static class JsonEntity { @TestFactory public List integrationTestsFromJson() throws 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"))) { - return tests; - } - // tests other than corpus tests + // handwritten integration tests for (String testFile : JSON_TEST_FILES) { tests.add(loadJsonTests(testFile)); } - // corpus tests - try (Stream stream = Files.list(resolveIntegrationTestPath("corpus_tests"))) { + // autogenerated corpus tests + 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_")) + // ignore files that end with `.entities.json` + .filter(path -> !path.getFileName().toString().endsWith(".entities.json")) // add the test .forEach( path -> { @@ -274,7 +270,7 @@ private DynamicContainer loadJsonTests(String jsonFile) throws IOException { jsonFile + ": validate", () -> executeJsonValidationTest(policies, schema, test.shouldValidate))), - test.queries.stream() + test.requests.stream() .map( request -> DynamicTest.dynamicTest( @@ -338,9 +334,10 @@ private Boolean hasUnmatchedQuote(String s) { /** Load the schema file. */ private Schema loadSchema(String schemaFile) throws IOException { - try (InputStream schemaIn = + try (InputStream schemaStream = new FileInputStream(resolveIntegrationTestPath(schemaFile).toFile())) { - return new Schema(OBJECT_MAPPER.reader().readValue(schemaIn, JsonNode.class)); + String schemaText = new String(schemaStream.readAllBytes(), StandardCharsets.UTF_8); + return new Schema(schemaText); } } @@ -421,7 +418,7 @@ private void executeJsonRequestTest( final var success = assertDoesNotThrow(() -> response.success.get()); assertEquals(request.decision, success.getDecision()); // convert to a HashSet to allow reordering - assertEquals(new HashSet<>(request.reasons), success.getReasons()); + assertEquals(new HashSet<>(request.reason), success.getReason()); // The integration tests only record the id of the erroring policy, // not the full error message. So only check that the list lengths match. assertEquals(request.errors.size(), success.getErrors().size()); diff --git a/CedarJava/src/test/java/com/cedarpolicy/TestUtil.java b/CedarJava/src/test/java/com/cedarpolicy/TestUtil.java index d22a03e7..445d70c7 100644 --- a/CedarJava/src/test/java/com/cedarpolicy/TestUtil.java +++ b/CedarJava/src/test/java/com/cedarpolicy/TestUtil.java @@ -17,13 +17,17 @@ package com.cedarpolicy; import com.cedarpolicy.model.schema.Schema; +import com.cedarpolicy.model.schema.Schema.JsonOrHuman; + import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; +import java.util.Optional; /** Utils to help with tests. */ public final class TestUtil { - private TestUtil() {} + private TestUtil() { + } /** * Load schema file. @@ -32,12 +36,11 @@ private TestUtil() {} */ public static Schema loadSchemaResource(String schemaFile) { try { - return new Schema( - new String( - Files.readAllBytes( - Paths.get( - ValidationTests.class.getResource(schemaFile).toURI())), - StandardCharsets.UTF_8)); + String text = new String(Files.readAllBytes( + Paths.get( + ValidationTests.class.getResource(schemaFile).toURI())), + StandardCharsets.UTF_8); + return new Schema(JsonOrHuman.Json, Optional.of(text), Optional.empty()); } catch (Exception e) { throw new RuntimeException("Failed to load test schema file " + schemaFile, e); } diff --git a/CedarJavaFFI/src/answer.rs b/CedarJavaFFI/src/answer.rs index 3ab37b2e..2a9e62b0 100644 --- a/CedarJavaFFI/src/answer.rs +++ b/CedarJavaFFI/src/answer.rs @@ -47,14 +47,6 @@ pub enum Answer { } impl Answer { - /// A successful result - pub fn succeed(value: T) -> Self { - serde_json::to_string(&value).map_or_else( - |e| Self::fail_internally(format!("error serializing result: {e:}")), - |result| Self::Success { result }, - ) - } - /// An "internal failure" result; see docs on [`Answer::Failure`] pub fn fail_internally(message: String) -> Self { Self::Failure { diff --git a/CedarJavaFFI/src/interface.rs b/CedarJavaFFI/src/interface.rs index 29e8901a..039fadd4 100644 --- a/CedarJavaFFI/src/interface.rs +++ b/CedarJavaFFI/src/interface.rs @@ -15,7 +15,7 @@ */ #[cfg(feature = "partial-eval")] -use cedar_policy::ffi::{is_authorized_partial_json_str, PartialAuthorizationAnswer}; +use cedar_policy::ffi::is_authorized_partial_json_str; use cedar_policy::{ ffi::{is_authorized_json_str, validate_json_str}, EntityUid, Policy, PolicyId, PolicySet, Schema, SlotId, Template, @@ -41,7 +41,6 @@ const V0_AUTH_OP: &str = "AuthorizationOperation"; #[cfg(feature = "partial-eval")] const V0_AUTH_PARTIAL_OP: &str = "AuthorizationPartialOperation"; const V0_VALIDATE_OP: &str = "ValidateOperation"; -const V0_PARSE_EUID_OP: &str = "ParseEntityUidOperation"; fn build_err_obj(env: &JNIEnv<'_>, err: &str) -> jstring { env.new_string( @@ -59,7 +58,7 @@ fn call_cedar_in_thread(call_str: String, input_str: String) -> String { call_cedar(&call_str, &input_str) } -/// The main JNI entry point +/// JNI entry point for authorization and validation requests #[jni_fn("com.cedarpolicy.BasicAuthorizationEngine")] pub fn callCedarJNI( mut env: JNIEnv<'_>, @@ -100,24 +99,20 @@ pub fn callCedarJNI( } } -/// The main JNI entry point +/// JNI entry point to get the Cedar version #[jni_fn("com.cedarpolicy.BasicAuthorizationEngine")] pub fn getCedarJNIVersion(env: JNIEnv<'_>) -> jstring { - env.new_string("3.0") + env.new_string("4.0") .expect("error creating Java string") .into_raw() } -fn call_cedar(call: &str, input: &str) -> String { +pub(crate) fn call_cedar(call: &str, input: &str) -> String { let result = match call { V0_AUTH_OP => is_authorized_json_str(input), #[cfg(feature = "partial-eval")] - V0_AUTH_PARTIAL_OP => is_authorized_partial_json_str(&input), + V0_AUTH_PARTIAL_OP => is_authorized_partial_json_str(input), V0_VALIDATE_OP => validate_json_str(input), - V0_PARSE_EUID_OP => { - let ires = json_parse_entity_uid(&input); - serde_json::to_string(&ires) - } _ => { let ires = Answer::fail_internally(format!("unsupported operation: {}", call)); serde_json::to_string(&ires) @@ -147,45 +142,25 @@ fn jni_failed(env: &mut JNIEnv<'_>, e: &dyn Error) -> jvalue { JValueOwned::Object(JObject::null()).as_jni() } -#[derive(Debug, Serialize, Deserialize)] -struct ParseEUIDCall { - euid: String, -} - -#[derive(Debug, Serialize, Deserialize)] -struct ParseEUIDOutput { - ty: String, - id: String, -} - -/// public string-based JSON interface to be invoked by FFIs. Takes in a `ParseEUIDCall`, parses it and (if successful) -/// returns a serialized `ParseEUIDOutput` -pub fn json_parse_entity_uid(input: &str) -> Answer { - match serde_json::from_str::(input) { - Err(e) => Answer::fail_internally(format!("error parsing call to parse EntityUID: {e:}")), - Ok(euid_call) => match cedar_policy::EntityUid::from_str(euid_call.euid.as_str()) { - Ok(euid) => match serde_json::to_string(&ParseEUIDOutput { - ty: euid.type_name().to_string(), - id: euid.id().to_string(), - }) { - Ok(s) => Answer::succeed(s), - Err(e) => Answer::fail_internally(format!("error serializing EntityUID: {e:}")), - }, - Err(e) => Answer::fail_internally(format!("error parsing EntityUID: {e:}")), - }, +/// Public string-based JSON interface to parse a schema in Cedar's JSON format +#[jni_fn("com.cedarpolicy.model.schema.Schema")] +pub fn parseJsonSchemaJni<'a>(mut env: JNIEnv<'a>, _: JClass, schema_jstr: JString<'a>) -> jvalue { + match parse_json_schema_internal(&mut env, schema_jstr) { + Ok(v) => v.as_jni(), + Err(e) => jni_failed(&mut env, e.as_ref()), } } -/// public string-based JSON interface to parse a schema +/// public string-based JSON interface to parse a schema in Cedar's human-readable format #[jni_fn("com.cedarpolicy.model.schema.Schema")] -pub fn parseSchema<'a>(mut env: JNIEnv<'a>, _: JClass, schema_jstr: JString<'a>) -> jvalue { - match parse_schema_internal(&mut env, schema_jstr) { +pub fn parseHumanSchemaJni<'a>(mut env: JNIEnv<'a>, _: JClass, schema_jstr: JString<'a>) -> jvalue { + match parse_human_schema_internal(&mut env, schema_jstr) { Ok(v) => v.as_jni(), Err(e) => jni_failed(&mut env, e.as_ref()), } } -fn parse_schema_internal<'a>( +fn parse_json_schema_internal<'a>( env: &mut JNIEnv<'a>, schema_jstr: JString<'a>, ) -> Result> { @@ -196,7 +171,23 @@ fn parse_schema_internal<'a>( let schema_string = String::from(schema_jstring); match Schema::from_str(&schema_string) { Err(e) => Err(Box::new(e)), - Ok(_) => Ok(JValueGen::Object(env.new_string("Success")?.into())), + Ok(_) => Ok(JValueGen::Object(env.new_string("success")?.into())), + } + } +} + +fn parse_human_schema_internal<'a>( + env: &mut JNIEnv<'a>, + schema_jstr: JString<'a>, +) -> Result> { + if schema_jstr.is_null() { + raise_npe(env) + } else { + let schema_jstring = env.get_string(&schema_jstr)?; + let schema_string = String::from(schema_jstring); + match Schema::from_str_natural(&schema_string) { + Err(e) => Err(Box::new(e)), + Ok(_) => Ok(JValueGen::Object(env.new_string("success")?.into())), } } } @@ -431,234 +422,3 @@ fn get_euid_repr_internal<'a>( Ok(jstring.into()) } } - -#[cfg(test)] -mod test { - use super::*; - use cedar_policy::ffi::{AuthorizationAnswer, ValidationAnswer}; - use cool_asserts::assert_matches; - - #[test] - fn parse_entityuid() { - let result = call_cedar("ParseEntityUidOperation", r#"{"euid": "User::\"Alice\""} "#); - assert_success(result); - } - - #[test] - fn empty_authorization_call_succeeds() { - let result = call_cedar( - "AuthorizationOperation", - r#" -{ - "principal" : { "type" : "User", "id" : "alice" }, - "action" : { "type" : "Photo", "id" : "view" }, - "resource" : { "type" : "Photo", "id" : "photo" }, - "slice": { - "policies": {}, - "entities": [] - }, - "context": {} -} - "#, - ); - assert_authorization_success(result); - } - - #[test] - fn empty_validation_call_json_schema_succeeds() { - let result = call_cedar( - "ValidateOperation", - r#"{ "schema": { "json": {} }, "policySet": {} }"#, - ); - assert_validation_success(result); - } - - #[test] - fn empty_validation_call_succeeds() { - let result = call_cedar( - "ValidateOperation", - r#"{ "schema": { "human": "" }, "policySet": {} }"#, - ); - assert_validation_success(result); - } - - #[test] - fn unrecognised_call_fails() { - let result = call_cedar("BadOperation", ""); - assert_failure(result); - } - - #[test] - fn test_unspecified_principal_call_succeeds() { - let result = call_cedar( - "AuthorizationOperation", - r#" - { - "context": {}, - "slice": { - "policies": { - "001": "permit(principal, action, resource);" - }, - "entities": [], - "templates": {}, - "templateInstantiations": [] - }, - "principal": null, - "action" : { "type" : "Action", "id" : "view" }, - "resource" : { "type" : "Resource", "id" : "thing" } - } - "#, - ); - assert_authorization_success(result); - } - - #[test] - fn test_unspecified_resource_call_succeeds() { - let result = call_cedar( - "AuthorizationOperation", - r#" - { - "context": {}, - "slice": { - "policies": { - "001": "permit(principal, action, resource);" - }, - "entities": [], - "templates": {}, - "templateInstantiations": [] - }, - "principal" : { "type" : "User", "id" : "alice" }, - "action" : { "type" : "Action", "id" : "view" }, - "resource": null - } - "#, - ); - assert_authorization_success(result); - } - - #[test] - fn template_authorization_call_succeeds() { - let result = call_cedar( - "AuthorizationOperation", - r#" - { - "principal" : { - "type" : "User", - "id" : "alice" - }, - "action" : { - "type" : "Photo", - "id" : "view" - }, - "resource" : { - "type" : "Photo", - "id" : "door" - }, - "context" : {}, - "slice" : { - "policies" : {} - , "entities" : [] - , "templates" : { - "ID0": "permit(principal == ?principal, action, resource);" - } - , "templateInstantiations" : [ - { - "templateId" : "ID0", - "resultPolicyId" : "ID0_User_alice", - "instantiations" : [ - { - "slot": "?principal", - "value": { - "ty" : "User", - "eid" : "alice" - } - } - ] - } - ] - } - } - "#, - ); - assert_authorization_success(result); - } - - #[cfg(feature = "partial-eval")] - #[test] - fn test_missing_resource_call_succeeds() { - let result = call_cedar( - "AuthorizationPartialOperation", - r#" - { - "context": {}, - "slice": { - "policies": { - "001": "permit(principal == User::\"alice\", action, resource == Photo::\"door\");" - }, - "entities": [], - "templates": {}, - "templateInstantiations": [] - }, - "principal" : { "type" : "User", "id" : "alice" }, - "action" : { "type" : "Action", "id" : "view" } - } - "#, - ); - assert_partial_authorization_success(result); - } - - #[cfg(feature = "partial-eval")] - #[test] - fn test_missing_principal_call_succeeds() { - let result = call_cedar( - "AuthorizationPartialOperation", - r#" - { - "context": {}, - "slice": { - "policies": { - "001": "permit(principal == User::\"alice\", action, resource == Photo::\"door\");" - }, - "entities": [], - "templates": {}, - "templateInstantiations": [] - }, - "action" : { "type" : "Action", "id" : "view" }, - "resource" : { "type" : "Photo", "id" : "door" } - } - "#, - ); - assert_partial_authorization_success(result); - } - - #[track_caller] - fn assert_success(result: String) { - let result: Answer = serde_json::from_str(result.as_str()).unwrap(); - assert_matches!(result, Answer::Success { .. }); - } - - #[track_caller] - fn assert_failure(result: String) { - let result: Answer = serde_json::from_str(result.as_str()).unwrap(); - assert_matches!(result, Answer::Failure { .. }); - } - - #[track_caller] - fn assert_authorization_success(result: String) { - let result: AuthorizationAnswer = serde_json::from_str(result.as_str()).unwrap(); - assert_matches!(result, AuthorizationAnswer::Success { .. }); - } - - #[cfg(feature = "partial-eval")] - #[track_caller] - fn assert_partial_authorization_success(result: String) { - let result: PartialAuthorizationAnswer = serde_json::from_str(result.as_str()).unwrap(); - assert_matches!(result, PartialAuthorizationAnswer::Residuals { .. }); - } - - #[track_caller] - fn assert_validation_success(result: String) { - let result: ValidationAnswer = serde_json::from_str(result.as_str()).unwrap(); - assert_matches!(result, ValidationAnswer::Success { .. }); - } -} diff --git a/CedarJavaFFI/src/lib.rs b/CedarJavaFFI/src/lib.rs index 22a5c418..98357a50 100644 --- a/CedarJavaFFI/src/lib.rs +++ b/CedarJavaFFI/src/lib.rs @@ -19,6 +19,7 @@ mod answer; mod interface; mod jlist; mod objects; +mod tests; mod utils; pub use interface::*; diff --git a/CedarJavaFFI/src/objects.rs b/CedarJavaFFI/src/objects.rs index 917e8d19..12117c93 100644 --- a/CedarJavaFFI/src/objects.rs +++ b/CedarJavaFFI/src/objects.rs @@ -62,7 +62,7 @@ impl<'a> JEntityTypeName<'a> { Ok(Self { obj, type_name }) } - /// Get the string representation for this EntityTYpeName + /// Get the string representation for this EntityTypeName pub fn get_string_repr(&self) -> String { self.get_rust_repr().to_string() } diff --git a/CedarJavaFFI/src/tests.rs b/CedarJavaFFI/src/tests.rs new file mode 100644 index 00000000..47cbc901 --- /dev/null +++ b/CedarJavaFFI/src/tests.rs @@ -0,0 +1,233 @@ +#![cfg(test)] + +use crate::answer::Answer; +use crate::call_cedar; +#[cfg(feature = "partial-eval")] +use cedar_policy::ffi::PartialAuthorizationAnswer; +use cedar_policy::ffi::{AuthorizationAnswer, ValidationAnswer}; +use cool_asserts::assert_matches; + +#[track_caller] +fn assert_failure(result: String) { + let result: Answer = serde_json::from_str(result.as_str()).unwrap(); + assert_matches!(result, Answer::Failure { .. }); +} + +#[track_caller] +fn assert_authorization_success(result: String) { + let result: AuthorizationAnswer = serde_json::from_str(result.as_str()).unwrap(); + assert_matches!(result, AuthorizationAnswer::Success { .. }); +} + +#[cfg(feature = "partial-eval")] +#[track_caller] +fn assert_partial_authorization_success(result: String) { + let result: PartialAuthorizationAnswer = serde_json::from_str(result.as_str()).unwrap(); + assert_matches!(result, PartialAuthorizationAnswer::Residuals { .. }); +} + +#[track_caller] +fn assert_validation_success(result: String) { + let result: ValidationAnswer = serde_json::from_str(result.as_str()).unwrap(); + assert_matches!(result, ValidationAnswer::Success { .. }); +} + +#[test] +fn unrecognized_call_fails() { + let result = call_cedar("BadOperation", ""); + assert_failure(result); +} + +mod authorization_tests { + use super::*; + + #[test] + fn empty_authorization_call_succeeds() { + let result = call_cedar( + "AuthorizationOperation", + r#" + { + "principal" : { "type" : "User", "id" : "alice" }, + "action" : { "type" : "Photo", "id" : "view" }, + "resource" : { "type" : "Photo", "id" : "photo" }, + "slice": { + "policies": {}, + "entities": [] + }, + "context": {} + } + "#, + ); + assert_authorization_success(result); + } + + #[test] + fn test_unspecified_principal_call_succeeds() { + let result = call_cedar( + "AuthorizationOperation", + r#" + { + "context": {}, + "slice": { + "policies": { + "001": "permit(principal, action, resource);" + }, + "entities": [], + "templates": {}, + "templateInstantiations": [] + }, + "principal": null, + "action" : { "type" : "Action", "id" : "view" }, + "resource" : { "type" : "Resource", "id" : "thing" } + } + "#, + ); + assert_authorization_success(result); + } + + #[test] + fn test_unspecified_resource_call_succeeds() { + let result = call_cedar( + "AuthorizationOperation", + r#" + { + "context": {}, + "slice": { + "policies": { + "001": "permit(principal, action, resource);" + }, + "entities": [], + "templates": {}, + "templateInstantiations": [] + }, + "principal" : { "type" : "User", "id" : "alice" }, + "action" : { "type" : "Action", "id" : "view" }, + "resource": null + } + "#, + ); + assert_authorization_success(result); + } + + #[test] + fn template_authorization_call_succeeds() { + let result = call_cedar( + "AuthorizationOperation", + r#" + { + "principal" : { + "type" : "User", + "id" : "alice" + }, + "action" : { + "type" : "Photo", + "id" : "view" + }, + "resource" : { + "type" : "Photo", + "id" : "door" + }, + "context" : {}, + "slice" : { + "policies" : {} + , "entities" : [] + , "templates" : { + "ID0": "permit(principal == ?principal, action, resource);" + } + , "templateInstantiations" : [ + { + "templateId" : "ID0", + "resultPolicyId" : "ID0_User_alice", + "instantiations" : [ + { + "slot": "?principal", + "value": { + "ty" : "User", + "eid" : "alice" + } + } + ] + } + ] + } + } + "#, + ); + assert_authorization_success(result); + } +} + +mod validation_tests { + use super::*; + + #[test] + fn empty_validation_call_json_schema_succeeds() { + let result = call_cedar( + "ValidateOperation", + r#"{ "schema": { "json": {} }, "policySet": {} }"#, + ); + assert_validation_success(result); + } + + #[test] + fn empty_validation_call_succeeds() { + let result = call_cedar( + "ValidateOperation", + r#"{ "schema": { "human": "" }, "policySet": {} }"#, + ); + assert_validation_success(result); + } +} + +#[cfg(feature = "partial-eval")] +mod partial_authorization_tests { + use super::*; + + #[test] + fn test_missing_resource_call_succeeds() { + let result = call_cedar( + "AuthorizationPartialOperation", + r#" + { + "context": {}, + "slice": { + "policies": { + "001": "permit(principal == User::\"alice\", action, resource == Photo::\"door\");" + }, + "entities": [], + "templates": {}, + "templateInstantiations": [] + }, + "principal" : { "type" : "User", "id" : "alice" }, + "action" : { "type" : "Action", "id" : "view" } + } + "#, + ); + assert_partial_authorization_success(result); + } + + #[test] + fn test_missing_principal_call_succeeds() { + let result = call_cedar( + "AuthorizationPartialOperation", + r#" + { + "context": {}, + "slice": { + "policies": { + "001": "permit(principal == User::\"alice\", action, resource == Photo::\"door\");" + }, + "entities": [], + "templates": {}, + "templateInstantiations": [] + }, + "action" : { "type" : "Action", "id" : "view" }, + "resource" : { "type" : "Photo", "id" : "door" } + } + "#, + ); + assert_partial_authorization_success(result); + } +} + +mod parsing_tests {}