From d75338910a109a56027283343e72f5082091162b Mon Sep 17 00:00:00 2001 From: Mudit Chaudhary Date: Fri, 18 Jul 2025 19:36:05 +0000 Subject: [PATCH 1/6] adds validate_with_level FFI helper methods Signed-off-by: Mudit Chaudhary --- CedarJavaFFI/Cargo.toml | 1 + CedarJavaFFI/src/helpers.rs | 355 ++++++++++++++++++++++++++++++++++++ CedarJavaFFI/src/lib.rs | 2 +- 3 files changed, 357 insertions(+), 1 deletion(-) create mode 100644 CedarJavaFFI/src/helpers.rs diff --git a/CedarJavaFFI/Cargo.toml b/CedarJavaFFI/Cargo.toml index 386af560..7805f68f 100644 --- a/CedarJavaFFI/Cargo.toml +++ b/CedarJavaFFI/Cargo.toml @@ -13,6 +13,7 @@ serde = { version = "1.0", features = ["derive", "rc"] } serde_json = "1.0" thiserror = "2.0" itertools = "0.14" +miette = "7.6.0" # JNI Support jni = "0.21.1" diff --git a/CedarJavaFFI/src/helpers.rs b/CedarJavaFFI/src/helpers.rs new file mode 100644 index 00000000..7e7e476d --- /dev/null +++ b/CedarJavaFFI/src/helpers.rs @@ -0,0 +1,355 @@ +use cedar_policy::{ + ffi::{ + JsonValueWithNoDuplicateKeys, PolicySet as FFIPolicySet, ValidationAnswer, ValidationError, + }, + PolicySet, Schema, SchemaFragment, SchemaWarning, ValidationMode, Validator, +}; +use miette::Context; +use serde::{Deserialize, Serialize}; +/// +/// This needs to be in cedar_policy::ffi. However, in order to not wait for another release of cedar for this new FFI API to be used in CedarJava4.4, recreating MVP +/// by borrowing some ffi code here. This will be deleted after this change is published in a subsequent Cedar release +/// + +/// Configuration for the validation call +#[derive(Serialize, Deserialize, Debug, Default)] +#[serde(rename_all = "camelCase")] +#[serde(deny_unknown_fields)] +pub struct ValidationSettings { + /// Used to control how a policy is validated. See comments on [`ValidationMode`]. + mode: ValidationMode, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +#[serde(deny_unknown_fields)] +pub struct LevelValidationCall { + /// Validation settings + #[serde(default)] + pub validation_settings: ValidationSettings, + /// Schema to use for validation + pub schema: FFISchema, + /// Policies to validate + pub policies: FFIPolicySet, + /// Max deref level + pub max_deref_level: u32, +} + +pub struct WithWarnings { + pub t: T, + pub warnings: Vec, +} + +/// Represents a schema in either the Cedar or JSON schema format +#[derive(Debug, Serialize, Deserialize)] +#[serde(untagged)] +#[serde( + expecting = "expected a schema in the Cedar or JSON policy format (with no duplicate keys)" +)] +pub enum FFISchema { + /// Schema in the Cedar schema format. See + Cedar(String), + /// Schema in Cedar's JSON schema format. See + Json(JsonValueWithNoDuplicateKeys), +} + +impl FFISchema { + /// Parse a [`Schema`] into a [`crate::Schema`] + pub(super) fn parse( + self, + ) -> Result<(Schema, Box>), miette::Report> { + let (schema_frag, warnings) = self.parse_schema_fragment()?; + Ok((schema_frag.try_into()?, warnings)) + } + + /// Return a [`crate::SchemaFragment`], which can be printed with `.to_string()` + /// and converted to JSON with `.to_json()`. + pub(super) fn parse_schema_fragment( + self, + ) -> Result<(SchemaFragment, Box>), miette::Report> { + match self { + Self::Cedar(str) => SchemaFragment::from_cedarschema_str(&str) + .map(|(sch, warnings)| { + ( + sch, + Box::new(warnings) as Box>, + ) + }) + .wrap_err("failed to parse schema from string"), + Self::Json(val) => SchemaFragment::from_json_value(val.into()) + .map(|sch| { + ( + sch, + Box::new(std::iter::empty()) as Box>, + ) + }) + .wrap_err("failed to parse schema from JSON"), + } + } +} + +impl LevelValidationCall { + fn get_components( + self, + ) -> WithWarnings>> + { + let mut errs = vec![]; + let policies = match self.policies.parse() { + Ok(policies) => policies, + Err(e) => { + errs.extend(e); + PolicySet::new() + } + }; + let pair = match self.schema.parse() { + Ok((schema, warnings)) => Some((schema, warnings)), + Err(e) => { + errs.push(e); + None + } + }; + match (errs.is_empty(), pair) { + (true, Some((schema, warnings))) => WithWarnings { + t: Ok(( + policies, + schema, + self.validation_settings, + self.max_deref_level, + )), + warnings: warnings.map(miette::Report::new).collect(), + }, + _ => WithWarnings { + t: Err(errs), + warnings: vec![], + }, + } + } +} + +pub fn validate__with_level_json_str(json: &str) -> Result { + let ans = validate_with_level(serde_json::from_str(json)?); + serde_json::to_string(&ans) +} + +pub fn validate_with_level(call: LevelValidationCall) -> ValidationAnswer { + match call.get_components() { + WithWarnings { + t: Ok((policies, schema, settings, max_deref_level)), + warnings, + } => { + // otherwise, call `Validator::validate` + let validator = Validator::new(schema); + + let validation_result = + validator.validate_with_level(&policies, settings.mode, max_deref_level); + let validation_errors = validation_result.validation_errors(); + let validation_warnings = validation_result.validation_warnings(); + + let validation_errors: Vec = validation_errors + .map(|error| ValidationError { + policy_id: error.policy_id().clone(), + error: miette::Report::new(error.clone()).into(), + }) + .collect(); + let validation_warnings: Vec = validation_warnings + .map(|error| ValidationError { + policy_id: error.policy_id().clone(), + error: miette::Report::new(error.clone()).into(), + }) + .collect(); + ValidationAnswer::Success { + validation_errors, + validation_warnings, + other_warnings: warnings.into_iter().map(Into::into).collect(), + } + } + WithWarnings { + t: Err(errors), + warnings, + } => ValidationAnswer::Failure { + errors: errors.into_iter().map(Into::into).collect(), + warnings: warnings.into_iter().map(Into::into).collect(), + }, + } +} + +#[cfg(test)] +mod test { + use crate::helpers::validate_with_level; + use cedar_policy::ffi::{ValidationAnswer, ValidationError}; + use cool_asserts::assert_matches; + use serde_json::json; + + #[track_caller] + fn assert_validates_without_errors(json: serde_json::Value) { + let ans = validate_with_level(serde_json::from_value(json).unwrap()); + let ans_val = serde_json::to_value(ans).unwrap(); + let result: Result = serde_json::from_value(ans_val); + println!("{:?}", result); + assert_matches!(result, Ok(ValidationAnswer::Success { validation_errors, validation_warnings: _, other_warnings: _ }) => { + assert_eq!(validation_errors.len(), 0, "Unexpected validation errors: {validation_errors:?}"); + }); + } + + #[track_caller] + fn assert_validates_with_errors(json: serde_json::Value) -> Vec { + let ans = validate_with_level(serde_json::from_value(json).unwrap()); + let ans_val = serde_json::to_value(ans).unwrap(); + assert_matches!(ans_val.get("validationErrors"), Some(_)); // should be present, with this camelCased name + assert_matches!(ans_val.get("validationWarnings"), Some(_)); // should be present, with this camelCased name + let result: Result = serde_json::from_value(ans_val); + assert_matches!(result, Ok(ValidationAnswer::Success { validation_errors, validation_warnings: _, other_warnings: _ }) => { + validation_errors + }) + } + + #[test] + fn test_correct_policy_validates_without_errors() { + let json = json!({ + "schema": { "": { + "entityTypes": { + "User": { + "memberOfTypes": [ "UserGroup" ], + "shape": { + "type": "Record", + "attributes": { + "friend": { + "type": "Entity", + "name": "User" + } + } + } + }, + "Photo": { + "memberOfTypes": [ "Album", "Account" ], + "shape":{ + "type": "Record", + "attributes": { + "owner": { + "type": "Entity", + "name": "User" + } + } + } + }, + "Album": { + "memberOfTypes": [ "Album", "Account" ] + }, + "Account": { }, + "UserGroup": {} + }, + "actions": { + "readOnly": { }, + "readWrite": { }, + "createAlbum": { + "appliesTo": { + "resourceTypes": [ "Account", "Album" ], + "principalTypes": [ "User" ] + } + }, + "addPhotoToAlbum": { + "appliesTo": { + "resourceTypes": [ "Album" ], + "principalTypes": [ "User" ] + } + }, + "viewPhoto": { + "appliesTo": { + "resourceTypes": [ "Photo" ], + "principalTypes": [ "User" ] + } + }, + "viewComments": { + "appliesTo": { + "resourceTypes": [ "Photo" ], + "principalTypes": [ "User" ] + } + } + } + }}, + "policies": { + "staticPolicies": { + "policy0": "permit(principal in UserGroup::\"alice_friends\", action == Action::\"viewPhoto\", resource) when {principal in resource.owner.friend};" + } + }, + "maxDerefLevel": 2}); + assert_validates_without_errors(json); + } + + #[test] + fn test_invalid_policy_validates_with_errors() { + let json = json!({ + "schema": { "": { + "entityTypes": { + "User": { + "memberOfTypes": [ "UserGroup" ], + "shape": { + "type": "Record", + "attributes": { + "friend": { + "type": "Entity", + "name": "User" + } + } + } + }, + "Photo": { + "memberOfTypes": [ "Album", "Account" ], + "shape":{ + "type": "Record", + "attributes": { + "owner": { + "type": "Entity", + "name": "User" + } + } + } + }, + "Album": { + "memberOfTypes": [ "Album", "Account" ] + }, + "Account": { }, + "UserGroup": {} + }, + "actions": { + "readOnly": { }, + "readWrite": { }, + "createAlbum": { + "appliesTo": { + "resourceTypes": [ "Account", "Album" ], + "principalTypes": [ "User" ] + } + }, + "addPhotoToAlbum": { + "appliesTo": { + "resourceTypes": [ "Album" ], + "principalTypes": [ "User" ] + } + }, + "viewPhoto": { + "appliesTo": { + "resourceTypes": [ "Photo" ], + "principalTypes": [ "User" ] + } + }, + "viewComments": { + "appliesTo": { + "resourceTypes": [ "Photo" ], + "principalTypes": [ "User" ] + } + } + } + }}, + "policies": { + "staticPolicies": { + "policy0": "permit(principal in UserGroup::\"alice_friends\", action == Action::\"viewPhoto\", resource) when {principal in resource.owner.friend};" + } + }, + "maxDerefLevel": 1}); + + let errs = assert_validates_with_errors(json); + + assert_eq!(errs.len(), 1, "expected 1 error but saw {}", errs.len()); + assert_eq!(errs[0].error.message, "for policy `policy0`, this policy requires level 2, which exceeds the maximum allowed level (1)"); + } +} diff --git a/CedarJavaFFI/src/lib.rs b/CedarJavaFFI/src/lib.rs index 5b740146..c1cd54a5 100644 --- a/CedarJavaFFI/src/lib.rs +++ b/CedarJavaFFI/src/lib.rs @@ -16,6 +16,7 @@ #![forbid(unsafe_code)] mod answer; +mod helpers; mod interface; mod jlist; mod jmap; @@ -24,5 +25,4 @@ mod jvm_test_utils; mod objects; mod tests; mod utils; - pub use interface::*; From 71f31de796a28b26d0e6ef79bf8f139c4d22fdf2 Mon Sep 17 00:00:00 2001 From: Mudit Chaudhary Date: Fri, 18 Jul 2025 20:19:02 +0000 Subject: [PATCH 2/6] adds validate with level operation to interface Signed-off-by: Mudit Chaudhary --- CedarJavaFFI/src/interface.rs | 4 +- CedarJavaFFI/src/tests.rs | 78 +++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/CedarJavaFFI/src/interface.rs b/CedarJavaFFI/src/interface.rs index 8f88f3d3..343ee16e 100644 --- a/CedarJavaFFI/src/interface.rs +++ b/CedarJavaFFI/src/interface.rs @@ -34,7 +34,6 @@ use serde::{Deserialize, Serialize}; use serde_json::{from_str, Value}; use std::{error::Error, str::FromStr, thread}; -use crate::objects::JFormatterConfig; use crate::{ answer::Answer, jmap::Map, @@ -42,6 +41,7 @@ use crate::{ objects::{JEntityId, JEntityTypeName, JEntityUID, JPolicy, Object}, utils::raise_npe, }; +use crate::{helpers::validate__with_level_json_str, objects::JFormatterConfig}; type Result = std::result::Result>; @@ -49,6 +49,7 @@ 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_VALIDATE_LEVEL_OP: &str = "ValidateWithLevelOperation"; const V0_VALIDATE_ENTITIES: &str = "ValidateEntities"; fn build_err_obj(env: &JNIEnv<'_>, err: &str) -> jstring { @@ -123,6 +124,7 @@ pub(crate) fn call_cedar(call: &str, input: &str) -> String { V0_AUTH_PARTIAL_OP => is_authorized_partial_json_str(input), V0_VALIDATE_OP => validate_json_str(input), V0_VALIDATE_ENTITIES => json_validate_entities(&input), + V0_VALIDATE_LEVEL_OP => validate__with_level_json_str(input), _ => { let ires = Answer::fail_internally(format!("unsupported operation: {}", call)); serde_json::to_string(&ires) diff --git a/CedarJavaFFI/src/tests.rs b/CedarJavaFFI/src/tests.rs index 8b9e2e64..483c5fed 100644 --- a/CedarJavaFFI/src/tests.rs +++ b/CedarJavaFFI/src/tests.rs @@ -194,6 +194,84 @@ mod validation_tests { let result = call_cedar("ValidateOperation", r#"{ "schema": "", "policies": {} }"#); assert_validation_success(&result); } + + #[test] + fn validate_with_level_succeeds() { + let input = r#" { + "schema": { "": { + "entityTypes": { + "User": { + "memberOfTypes": [ "UserGroup" ], + "shape": { + "type": "Record", + "attributes": { + "friend": { + "type": "Entity", + "name": "User" + } + } + } + }, + "Photo": { + "memberOfTypes": [ "Album", "Account" ], + "shape":{ + "type": "Record", + "attributes": { + "owner": { + "type": "Entity", + "name": "User" + } + } + } + }, + "Album": { + "memberOfTypes": [ "Album", "Account" ] + }, + "Account": { }, + "UserGroup": {} + }, + "actions": { + "readOnly": { }, + "readWrite": { }, + "createAlbum": { + "appliesTo": { + "resourceTypes": [ "Account", "Album" ], + "principalTypes": [ "User" ] + } + }, + "addPhotoToAlbum": { + "appliesTo": { + "resourceTypes": [ "Album" ], + "principalTypes": [ "User" ] + } + }, + "viewPhoto": { + "appliesTo": { + "resourceTypes": [ "Photo" ], + "principalTypes": [ "User" ] + } + }, + "viewComments": { + "appliesTo": { + "resourceTypes": [ "Photo" ], + "principalTypes": [ "User" ] + } + } + } + }}, + "policies": { + "staticPolicies": { + "policy0": "permit(principal in UserGroup::\"alice_friends\", action == Action::\"viewPhoto\", resource) when {principal in resource.owner.friend};" + } + }, + "maxDerefLevel": 2 + } + "#; + + let result = call_cedar("ValidateWithLevelOperation", input); + + assert_validation_success(&result); + } } mod entity_validation_tests { From f47ba08941c108be46e5c6c83ce19867bbb048ea Mon Sep 17 00:00:00 2001 From: Mudit Chaudhary Date: Wed, 23 Jul 2025 15:56:21 +0000 Subject: [PATCH 3/6] adds validateWithLevel API Signed-off-by: Mudit Chaudhary --- .../com/cedarpolicy/AuthorizationEngine.java | 15 ++- .../cedarpolicy/BasicAuthorizationEngine.java | 26 +++-- .../model/LevelValidationRequest.java | 109 ++++++++++++++++++ 3 files changed, 139 insertions(+), 11 deletions(-) create mode 100644 CedarJava/src/main/java/com/cedarpolicy/model/LevelValidationRequest.java diff --git a/CedarJava/src/main/java/com/cedarpolicy/AuthorizationEngine.java b/CedarJava/src/main/java/com/cedarpolicy/AuthorizationEngine.java index abcde75e..c5ba13d8 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/AuthorizationEngine.java +++ b/CedarJava/src/main/java/com/cedarpolicy/AuthorizationEngine.java @@ -30,6 +30,7 @@ import com.cedarpolicy.model.exception.AuthException; import com.cedarpolicy.model.exception.BadRequestException; import com.cedarpolicy.model.policy.PolicySet; +import com.cedarpolicy.model.LevelValidationRequest; /** * Implementations of the AuthorizationEngine interface invoke Cedar to respond to an authorization @@ -120,9 +121,21 @@ PartialAuthorizationResponse isAuthorizedPartial(PartialAuthorizationRequest req */ ValidationResponse validate(ValidationRequest request) throws AuthException; + /** + * Asks whether the policies in the given {@link LevelValidationRequest} q are correct + * when validated against the schema it describes. If validation passes, run level validation (RFC 76) + * + * @param request The request containing the policies to validate, the schema to validate them + * against and maximum dereferencing level. + * @return A {@link ValidationResponse} describing any validation errors found in the policies. + * @throws BadRequestException if any errors were found in the syntax of the policies. + * @throws AuthException if any internal errors occurred while validating the policies. + */ + ValidationResponse validateWithLevel(LevelValidationRequest request) throws AuthException; + /** * Asks whether the entities in the given {@link EntityValidationRequest} q are correct - * when validated against the schema it describes. + * when validated against the schema it describes. * * @param request The request containing the entities to validate and the schema to validate them * against. diff --git a/CedarJava/src/main/java/com/cedarpolicy/BasicAuthorizationEngine.java b/CedarJava/src/main/java/com/cedarpolicy/BasicAuthorizationEngine.java index 1b001bb6..e794cdbe 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/BasicAuthorizationEngine.java +++ b/CedarJava/src/main/java/com/cedarpolicy/BasicAuthorizationEngine.java @@ -26,6 +26,7 @@ import com.cedarpolicy.loader.LibraryLoader; import com.cedarpolicy.model.AuthorizationResponse; import com.cedarpolicy.model.EntityValidationRequest; +import com.cedarpolicy.model.LevelValidationRequest; import com.cedarpolicy.model.PartialAuthorizationResponse; import com.cedarpolicy.model.ValidationRequest; import com.cedarpolicy.model.ValidationResponse; @@ -57,7 +58,7 @@ public BasicAuthorizationEngine() { @Override public AuthorizationResponse isAuthorized(com.cedarpolicy.model.AuthorizationRequest q, - PolicySet policySet, Set entities) throws AuthException { + PolicySet policySet, Set entities) throws AuthException { final AuthorizationRequest request = new AuthorizationRequest(q, policySet, entities); return call("AuthorizationOperation", AuthorizationResponse.class, request); } @@ -67,14 +68,14 @@ public AuthorizationResponse isAuthorized(com.cedarpolicy.model.AuthorizationReq */ @Override public AuthorizationResponse isAuthorized(com.cedarpolicy.model.AuthorizationRequest q, - PolicySet policySet, Entities entities) throws AuthException { + PolicySet policySet, Entities entities) throws AuthException { return isAuthorized(q, policySet, entities.getEntities()); } @Experimental(ExperimentalFeature.PARTIAL_EVALUATION) @Override public PartialAuthorizationResponse isAuthorizedPartial(com.cedarpolicy.model.PartialAuthorizationRequest q, - PolicySet policySet, Set entities) throws AuthException { + PolicySet policySet, Set entities) throws AuthException { try { final PartialAuthorizationRequest request = new PartialAuthorizationRequest(q, policySet, entities); return call("AuthorizationPartialOperation", PartialAuthorizationResponse.class, request); @@ -93,7 +94,7 @@ public PartialAuthorizationResponse isAuthorizedPartial(com.cedarpolicy.model.Pa @Experimental(ExperimentalFeature.PARTIAL_EVALUATION) @Override public PartialAuthorizationResponse isAuthorizedPartial(com.cedarpolicy.model.PartialAuthorizationRequest q, - PolicySet policySet, Entities entities) throws AuthException { + PolicySet policySet, Entities entities) throws AuthException { return isAuthorizedPartial(q, policySet, entities.getEntities()); } @@ -102,6 +103,11 @@ public ValidationResponse validate(ValidationRequest q) throws AuthException { return call("ValidateOperation", ValidationResponse.class, q); } + @Override + public ValidationResponse validateWithLevel(LevelValidationRequest q) throws AuthException { + return call("ValidateWithLevelOperation", ValidationResponse.class, q); + } + @Override public void validateEntities(EntityValidationRequest q) throws AuthException { EntityValidationResponse entityValidationResponse = call("ValidateEntities", EntityValidationResponse.class, q); @@ -199,12 +205,12 @@ private static final class PartialAuthorizationRequest extends com.cedarpolicy.m PartialAuthorizationRequest(com.cedarpolicy.model.PartialAuthorizationRequest request, PolicySet policySet, Set entities) { super( - request.principal, - request.action, - request.resource, - request.context, - request.schema, - request.enableRequestValidation); + request.principal, + request.action, + request.resource, + request.context, + request.schema, + request.enableRequestValidation); this.policies = policySet; this.entities = entities; } diff --git a/CedarJava/src/main/java/com/cedarpolicy/model/LevelValidationRequest.java b/CedarJava/src/main/java/com/cedarpolicy/model/LevelValidationRequest.java new file mode 100644 index 00000000..03244f5d --- /dev/null +++ b/CedarJava/src/main/java/com/cedarpolicy/model/LevelValidationRequest.java @@ -0,0 +1,109 @@ +/* + * Copyright Cedar Contributors + * + * 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; + +import com.cedarpolicy.model.schema.Schema; +import com.cedarpolicy.model.policy.PolicySet; +import com.fasterxml.jackson.annotation.JsonProperty; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +import java.util.Objects; + +/** Information passed to Cedar for level validation. */ +public final class LevelValidationRequest { + private final Schema schema; + private final PolicySet policies; + private final long maxDerefLevel; + + /** + * Construct a validation request. + * + * @param schema Schema for the request + * @param policies Map of Policy ID to policy + * @param maxDerefLevel Maximum level of dereferencing allowed for validation + */ + @SuppressFBWarnings + public LevelValidationRequest(Schema schema, PolicySet policies, long maxDerefLevel) { + if (schema == null) { + throw new NullPointerException("schema"); + } + + if (policies == null) { + throw new NullPointerException("policies"); + } + + if (maxDerefLevel < 0) { + throw new IllegalArgumentException("maxDerefLevel must be non-negative"); + } + + this.schema = schema; + this.policies = policies; + this.maxDerefLevel = maxDerefLevel; + } + + /** + * Get the schema. + * + * @return The schema. + */ + public Schema getSchema() { + return this.schema; + } + + /** + * Get the policy set. + * + * @return A `PolicySet` object + */ + @JsonProperty("policies") + public PolicySet getPolicySet() { + return this.policies; + } + + /** + * Get the maximum deref level. + * + * @return The maximum deref level value for validation + */ + public long getMaxDerefLevel() { + return this.maxDerefLevel; + } + + /** Test equality. */ + @Override + public boolean equals(final Object o) { + if (!(o instanceof LevelValidationRequest)) { + return false; + } + + final LevelValidationRequest other = (LevelValidationRequest) o; + return schema.equals(other.schema) && policies.equals(other.policies) && maxDerefLevel == other.maxDerefLevel; + } + + /** Hash. */ + @Override + public int hashCode() { + return Objects.hash(schema, policies, maxDerefLevel); + } + + /** Get readable string representation. */ + public String toString() { + return "ValidationRequest(schema=" + schema + ", policies=" + policies + ", maxDerefLevel=" + maxDerefLevel + + ")"; + } +} From 553849244d3a772b249ca464622aba00246a15c7 Mon Sep 17 00:00:00 2001 From: Mudit Chaudhary Date: Wed, 23 Jul 2025 15:56:57 +0000 Subject: [PATCH 4/6] adds validateWithLevel tests Signed-off-by: Mudit Chaudhary --- .../java/com/cedarpolicy/ValidationTests.java | 82 ++++++++++++++++--- .../src/test/resources/level_schema.json | 63 ++++++++++++++ 2 files changed, 135 insertions(+), 10 deletions(-) create mode 100644 CedarJava/src/test/resources/level_schema.json diff --git a/CedarJava/src/test/java/com/cedarpolicy/ValidationTests.java b/CedarJava/src/test/java/com/cedarpolicy/ValidationTests.java index dcd0cbcb..2478d400 100644 --- a/CedarJava/src/test/java/com/cedarpolicy/ValidationTests.java +++ b/CedarJava/src/test/java/com/cedarpolicy/ValidationTests.java @@ -23,6 +23,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import com.cedarpolicy.model.DetailedError; +import com.cedarpolicy.model.LevelValidationRequest; import com.cedarpolicy.model.ValidationRequest; import com.cedarpolicy.model.ValidationResponse; import com.cedarpolicy.model.ValidationResponse.SuccessOrFailure; @@ -56,6 +57,8 @@ public void givenEmptySchemaAndNoPolicyReturnsValid() { givenSchema(EMPTY_SCHEMA); ValidationResponse response = whenValidated(); thenIsValid(response); + ValidationResponse levelResponse = whenLevelValidated(1); + thenIsValid(levelResponse); } /** Test. */ @@ -71,6 +74,8 @@ public void givenExampleSchemaAndCorrectPolicyReturnsValid() { + ");"); ValidationResponse response = whenValidated(); thenIsValid(response); + ValidationResponse levelResponse = whenLevelValidated(1); + thenIsValid(levelResponse); } /** Test. */ @@ -86,6 +91,8 @@ public void givenExampleSchemaAndIncorrectPolicyReturnsValid() { + ");"); ValidationResponse response = whenValidated(); thenIsNotValid(response); + ValidationResponse levelResponse = whenLevelValidated(1); + thenIsNotValid(levelResponse); } /** Test. */ @@ -95,6 +102,8 @@ public void givenInvalidPolicyThrowsBadRequestError() { givenPolicy("policy0", "permit { }"); ValidationResponse response = whenValidated(); thenValidationFailed(response); + ValidationResponse levelResponse = whenLevelValidated(1); + thenValidationFailed(levelResponse); } @Test @@ -119,6 +128,8 @@ public void validateTemplateLinkedPolicySuccessTest() { this.policies = new PolicySet(new HashSet<>(), templates, templateLinks); ValidationResponse response = whenValidated(); thenIsValid(response); + ValidationResponse levelResponse = whenLevelValidated(1); + thenIsValid(levelResponse); } @Test @@ -136,34 +147,79 @@ public void validateTemplateLinkedPolicyFailsWhenExpected() { // fails if we provide a value for the wrong slot this.policies = new PolicySet(new HashSet<>(), templates, List.of(new TemplateLink("template1", "policy", List.of(principalLink)))); - ValidationResponse response2 = whenValidated(); - thenValidationFailed(response2); + ValidationResponse response1 = whenValidated(); + thenValidationFailed(response1); + ValidationResponse levelResponse1 = whenLevelValidated(1); + thenValidationFailed(levelResponse1); // fails if we provide a value for too many slots this.policies = new PolicySet(new HashSet<>(), templates, List.of(new TemplateLink("template2", "policy", List.of(principalLink, resourceLink)))); - ValidationResponse response3 = whenValidated(); - thenValidationFailed(response3); + ValidationResponse response2 = whenValidated(); + thenValidationFailed(response2); + ValidationResponse levelResponse2 = whenLevelValidated(1); + thenValidationFailed(levelResponse2); // fails if we don't provide a value for all slots this.policies = new PolicySet(new HashSet<>(), templates, List.of(new TemplateLink("template0", "policy", List.of(resourceLink)))); - ValidationResponse response4 = whenValidated(); - thenValidationFailed(response4); + ValidationResponse response3 = whenValidated(); + thenValidationFailed(response3); + ValidationResponse levelResponse3 = whenLevelValidated(1); + thenValidationFailed(levelResponse3); + // validation returns an error if we provide a link with the wrong type LinkValue badLink1 = new LinkValue("?resource", EntityUID.parse("Library::User::\"Victor\"").get()); this.policies = new PolicySet(new HashSet<>(), templates, List.of(new TemplateLink("template1", "policy", List.of(badLink1)))); - ValidationResponse response5 = whenValidated(); - thenIsNotValid(response5); + ValidationResponse response4 = whenValidated(); + thenIsNotValid(response4); + ValidationResponse levelResponse4 = whenLevelValidated(1); + thenIsNotValid(levelResponse4); // validation returns an error if we provide a link with an invalid type LinkValue badLink2 = new LinkValue("?resource", EntityUID.parse("Library::BOOK::\"The black Swan\"").get()); this.policies = new PolicySet(new HashSet<>(), templates, List.of(new TemplateLink("template1", "policy", List.of(badLink2)))); - ValidationResponse response6 = whenValidated(); - thenIsNotValid(response6); + ValidationResponse response5 = whenValidated(); + thenIsNotValid(response5); + ValidationResponse levelResponse5 = whenLevelValidated(1); + thenIsNotValid(levelResponse5); + } + + @Test + public void validateLevelPolicySuccessTest() { + givenSchema(LEVEL_SCHEMA); + givenPolicy( + "policy0", + "permit(\n" + + " principal in UserGroup::\"alice_friends\",\n" + + " action == Action::\"viewPhoto\",\n" + + " resource\n" + + ") when {principal in resource.owner.friend};"); + + ValidationResponse response = whenValidated(); + thenIsValid(response); + ValidationResponse levelResponse = whenLevelValidated(2); + thenIsValid(levelResponse); + } + + @Test + public void validateLevelPolicyFailsWhenExpected() { + givenSchema(LEVEL_SCHEMA); + givenPolicy( + "policy0", + "permit(\n" + + " principal in UserGroup::\"alice_friends\",\n" + + " action == Action::\"viewPhoto\",\n" + + " resource\n" + + ") when {principal in resource.owner.friend};"); + + ValidationResponse response = whenValidated(); + thenIsValid(response); + ValidationResponse levelResponse = whenLevelValidated(1); + thenIsNotValid(levelResponse); } private void givenSchema(Schema testSchema) { @@ -180,6 +236,11 @@ private ValidationResponse whenValidated() { return assertDoesNotThrow(() -> engine.validate(request)); } + private ValidationResponse whenLevelValidated(long maxDerefLevel) { + LevelValidationRequest request = new LevelValidationRequest(schema, policies, maxDerefLevel); + return assertDoesNotThrow(() -> engine.validateWithLevel(request)); + } + private void thenIsValid(ValidationResponse response) { assertEquals(response.type, SuccessOrFailure.Success); final ValidationSuccessResponse success = assertDoesNotThrow(() -> response.success.get()); @@ -223,4 +284,5 @@ private void reset() { private static final Schema EMPTY_SCHEMA = loadSchemaResource("/empty_schema.json"); private static final Schema PHOTOFLASH_SCHEMA = loadSchemaResource("/photoflash_schema.json"); private static final Schema LIBRARY_SCHEMA = loadSchemaResource("/library_schema.json"); + private static final Schema LEVEL_SCHEMA = loadSchemaResource("/level_schema.json"); } diff --git a/CedarJava/src/test/resources/level_schema.json b/CedarJava/src/test/resources/level_schema.json new file mode 100644 index 00000000..ad991b0b --- /dev/null +++ b/CedarJava/src/test/resources/level_schema.json @@ -0,0 +1,63 @@ +{ +"": { + "entityTypes": { + "User": { + "memberOfTypes": [ "UserGroup" ], + "shape": { + "type": "Record", + "attributes": { + "friend": { + "type": "Entity", + "name": "User" + } + } + } + }, + "Photo": { + "memberOfTypes": [ "Album", "Account" ], + "shape":{ + "type": "Record", + "attributes": { + "owner": { + "type": "Entity", + "name": "User" + } + } + } + }, + "Album": { + "memberOfTypes": [ "Album", "Account" ] + }, + "Account": { }, + "UserGroup": {} + }, + "actions": { + "readOnly": { }, + "readWrite": { }, + "createAlbum": { + "appliesTo": { + "resourceTypes": [ "Account", "Album" ], + "principalTypes": [ "User" ] + } + }, + "addPhotoToAlbum": { + "appliesTo": { + "resourceTypes": [ "Album" ], + "principalTypes": [ "User" ] + } + }, + "viewPhoto": { + "appliesTo": { + "resourceTypes": [ "Photo" ], + "principalTypes": [ "User" ] + } + }, + "viewComments": { + "appliesTo": { + "resourceTypes": [ "Photo" ], + "principalTypes": [ "User" ] + } + } + } + } +} \ No newline at end of file From 2ca83060cca2915a2febb551158a9d44814994d0 Mon Sep 17 00:00:00 2001 From: Mudit Chaudhary Date: Wed, 23 Jul 2025 17:49:20 +0000 Subject: [PATCH 5/6] fixes nits Signed-off-by: Mudit Chaudhary --- .../src/test/resources/level_schema.json | 2 +- CedarJavaFFI/src/helpers.rs | 20 ++- CedarJavaFFI/src/tests.rs | 147 +++++++++++------- 3 files changed, 103 insertions(+), 66 deletions(-) diff --git a/CedarJava/src/test/resources/level_schema.json b/CedarJava/src/test/resources/level_schema.json index ad991b0b..ffa7d9f0 100644 --- a/CedarJava/src/test/resources/level_schema.json +++ b/CedarJava/src/test/resources/level_schema.json @@ -60,4 +60,4 @@ } } } -} \ No newline at end of file +} diff --git a/CedarJavaFFI/src/helpers.rs b/CedarJavaFFI/src/helpers.rs index 7e7e476d..79e83036 100644 --- a/CedarJavaFFI/src/helpers.rs +++ b/CedarJavaFFI/src/helpers.rs @@ -1,3 +1,18 @@ +/* + * Copyright Cedar Contributors + * + * 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. + */ use cedar_policy::{ ffi::{ JsonValueWithNoDuplicateKeys, PolicySet as FFIPolicySet, ValidationAnswer, ValidationError, @@ -6,10 +21,6 @@ use cedar_policy::{ }; use miette::Context; use serde::{Deserialize, Serialize}; -/// -/// This needs to be in cedar_policy::ffi. However, in order to not wait for another release of cedar for this new FFI API to be used in CedarJava4.4, recreating MVP -/// by borrowing some ffi code here. This will be deleted after this change is published in a subsequent Cedar release -/// /// Configuration for the validation call #[derive(Serialize, Deserialize, Debug, Default)] @@ -185,7 +196,6 @@ mod test { let ans = validate_with_level(serde_json::from_value(json).unwrap()); let ans_val = serde_json::to_value(ans).unwrap(); let result: Result = serde_json::from_value(ans_val); - println!("{:?}", result); assert_matches!(result, Ok(ValidationAnswer::Success { validation_errors, validation_warnings: _, other_warnings: _ }) => { assert_eq!(validation_errors.len(), 0, "Unexpected validation errors: {validation_errors:?}"); }); diff --git a/CedarJavaFFI/src/tests.rs b/CedarJavaFFI/src/tests.rs index 483c5fed..4ac2b1e9 100644 --- a/CedarJavaFFI/src/tests.rs +++ b/CedarJavaFFI/src/tests.rs @@ -198,71 +198,98 @@ mod validation_tests { #[test] fn validate_with_level_succeeds() { let input = r#" { - "schema": { "": { - "entityTypes": { - "User": { - "memberOfTypes": [ "UserGroup" ], - "shape": { - "type": "Record", - "attributes": { - "friend": { - "type": "Entity", - "name": "User" - } - } - } - }, - "Photo": { - "memberOfTypes": [ "Album", "Account" ], - "shape":{ - "type": "Record", - "attributes": { - "owner": { - "type": "Entity", - "name": "User" + "schema": { + "": { + "entityTypes": { + "User": { + "memberOfTypes": [ + "UserGroup" + ], + "shape": { + "type": "Record", + "attributes": { + "friend": { + "type": "Entity", + "name": "User" + } + } + } + }, + "Photo": { + "memberOfTypes": [ + "Album", + "Account" + ], + "shape": { + "type": "Record", + "attributes": { + "owner": { + "type": "Entity", + "name": "User" + } + } + } + }, + "Album": { + "memberOfTypes": [ + "Album", + "Account" + ] + }, + "Account": {}, + "UserGroup": {} + }, + "actions": { + "readOnly": {}, + "readWrite": {}, + "createAlbum": { + "appliesTo": { + "resourceTypes": [ + "Account", + "Album" + ], + "principalTypes": [ + "User" + ] + } + }, + "addPhotoToAlbum": { + "appliesTo": { + "resourceTypes": [ + "Album" + ], + "principalTypes": [ + "User" + ] + } + }, + "viewPhoto": { + "appliesTo": { + "resourceTypes": [ + "Photo" + ], + "principalTypes": [ + "User" + ] + } + }, + "viewComments": { + "appliesTo": { + "resourceTypes": [ + "Photo" + ], + "principalTypes": [ + "User" + ] + } } } } - }, - "Album": { - "memberOfTypes": [ "Album", "Account" ] - }, - "Account": { }, - "UserGroup": {} }, - "actions": { - "readOnly": { }, - "readWrite": { }, - "createAlbum": { - "appliesTo": { - "resourceTypes": [ "Account", "Album" ], - "principalTypes": [ "User" ] - } - }, - "addPhotoToAlbum": { - "appliesTo": { - "resourceTypes": [ "Album" ], - "principalTypes": [ "User" ] - } - }, - "viewPhoto": { - "appliesTo": { - "resourceTypes": [ "Photo" ], - "principalTypes": [ "User" ] - } - }, - "viewComments": { - "appliesTo": { - "resourceTypes": [ "Photo" ], - "principalTypes": [ "User" ] - } - } - } - }}, "policies": { - "staticPolicies": { - "policy0": "permit(principal in UserGroup::\"alice_friends\", action == Action::\"viewPhoto\", resource) when {principal in resource.owner.friend};" - } + "staticPolicies": { + "policy0": "permit(principal in UserGroup::\"alice_friends\", action == Action::\"viewPhoto\", resource) when {principal in resource.owner.friend};" + } }, "maxDerefLevel": 2 } From 0aac0947e0550867b790bebc59157d0501f28c67 Mon Sep 17 00:00:00 2001 From: Mudit Chaudhary Date: Fri, 1 Aug 2025 14:45:10 +0000 Subject: [PATCH 6/6] fixes nits Signed-off-by: Mudit Chaudhary --- .../main/java/com/cedarpolicy/AuthorizationEngine.java | 2 +- .../com/cedarpolicy/model/LevelValidationRequest.java | 4 ++-- CedarJavaFFI/src/helpers.rs | 3 +-- CedarJavaFFI/src/interface.rs | 8 ++------ 4 files changed, 6 insertions(+), 11 deletions(-) diff --git a/CedarJava/src/main/java/com/cedarpolicy/AuthorizationEngine.java b/CedarJava/src/main/java/com/cedarpolicy/AuthorizationEngine.java index c5ba13d8..e3fc5ef4 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/AuthorizationEngine.java +++ b/CedarJava/src/main/java/com/cedarpolicy/AuthorizationEngine.java @@ -135,7 +135,7 @@ PartialAuthorizationResponse isAuthorizedPartial(PartialAuthorizationRequest req /** * Asks whether the entities in the given {@link EntityValidationRequest} q are correct - * when validated against the schema it describes. + * when validated against the schema it describes. * * @param request The request containing the entities to validate and the schema to validate them * against. diff --git a/CedarJava/src/main/java/com/cedarpolicy/model/LevelValidationRequest.java b/CedarJava/src/main/java/com/cedarpolicy/model/LevelValidationRequest.java index 03244f5d..166149b8 100644 --- a/CedarJava/src/main/java/com/cedarpolicy/model/LevelValidationRequest.java +++ b/CedarJava/src/main/java/com/cedarpolicy/model/LevelValidationRequest.java @@ -28,14 +28,14 @@ public final class LevelValidationRequest { private final Schema schema; private final PolicySet policies; - private final long maxDerefLevel; + private final long maxDerefLevel; // Must be non-negative (>=0) /** * Construct a validation request. * * @param schema Schema for the request * @param policies Map of Policy ID to policy - * @param maxDerefLevel Maximum level of dereferencing allowed for validation + * @param maxDerefLevel Maximum level of dereferencing allowed for validation. Must be non-negative (>=0) */ @SuppressFBWarnings public LevelValidationRequest(Schema schema, PolicySet policies, long maxDerefLevel) { diff --git a/CedarJavaFFI/src/helpers.rs b/CedarJavaFFI/src/helpers.rs index 79e83036..4d989935 100644 --- a/CedarJavaFFI/src/helpers.rs +++ b/CedarJavaFFI/src/helpers.rs @@ -137,7 +137,7 @@ impl LevelValidationCall { } } -pub fn validate__with_level_json_str(json: &str) -> Result { +pub fn validate_with_level_json_str(json: &str) -> Result { let ans = validate_with_level(serde_json::from_str(json)?); serde_json::to_string(&ans) } @@ -148,7 +148,6 @@ pub fn validate_with_level(call: LevelValidationCall) -> ValidationAnswer { t: Ok((policies, schema, settings, max_deref_level)), warnings, } => { - // otherwise, call `Validator::validate` let validator = Validator::new(schema); let validation_result = diff --git a/CedarJavaFFI/src/interface.rs b/CedarJavaFFI/src/interface.rs index 343ee16e..bab88455 100644 --- a/CedarJavaFFI/src/interface.rs +++ b/CedarJavaFFI/src/interface.rs @@ -41,7 +41,7 @@ use crate::{ objects::{JEntityId, JEntityTypeName, JEntityUID, JPolicy, Object}, utils::raise_npe, }; -use crate::{helpers::validate__with_level_json_str, objects::JFormatterConfig}; +use crate::{helpers::validate_with_level_json_str, objects::JFormatterConfig}; type Result = std::result::Result>; @@ -124,7 +124,7 @@ pub(crate) fn call_cedar(call: &str, input: &str) -> String { V0_AUTH_PARTIAL_OP => is_authorized_partial_json_str(input), V0_VALIDATE_OP => validate_json_str(input), V0_VALIDATE_ENTITIES => json_validate_entities(&input), - V0_VALIDATE_LEVEL_OP => validate__with_level_json_str(input), + V0_VALIDATE_LEVEL_OP => validate_with_level_json_str(input), _ => { let ires = Answer::fail_internally(format!("unsupported operation: {}", call)); serde_json::to_string(&ires) @@ -785,10 +785,6 @@ pub(crate) mod jvm_based_tests { // Static JVM to be used by all the tests. LazyLock for thread-safe lazy initialization mod policy_tests { - use std::result; - - use cedar_policy::Effect; - use super::*; #[track_caller]