diff --git a/cedar-policy-validator/Cargo.toml b/cedar-policy-validator/Cargo.toml index 5f8b506b5a..1121bc0622 100644 --- a/cedar-policy-validator/Cargo.toml +++ b/cedar-policy-validator/Cargo.toml @@ -45,6 +45,7 @@ arbitrary = ["dep:arbitrary", "cedar-policy-core/arbitrary"] # Experimental features. partial-validate = [] +level-validate = [] wasm = ["serde-wasm-bindgen", "tsify", "wasm-bindgen"] entity-manifest = [] entity-tags = [] diff --git a/cedar-policy-validator/src/diagnostics.rs b/cedar-policy-validator/src/diagnostics.rs index 50af83663d..6d1e7e357f 100644 --- a/cedar-policy-validator/src/diagnostics.rs +++ b/cedar-policy-validator/src/diagnostics.rs @@ -172,6 +172,12 @@ pub enum ValidationError { #[diagnostic(transparent)] #[cfg_attr(not(feature = "entity-tags"), allow(dead_code))] InternalInvariantViolation(#[from] validation_errors::InternalInvariantViolation), + #[cfg(feature = "level-validate")] + /// If a entity dereference level was provided, the policies cannot deref + /// more than `level` hops away from PARX + #[error(transparent)] + #[diagnostic(transparent)] + EntityDerefLevelViolation(#[from] validation_errors::EntityDerefLevelViolation), } impl ValidationError { diff --git a/cedar-policy-validator/src/diagnostics/validation_errors.rs b/cedar-policy-validator/src/diagnostics/validation_errors.rs index f3adc63b08..a2c55c0c62 100644 --- a/cedar-policy-validator/src/diagnostics/validation_errors.rs +++ b/cedar-policy-validator/src/diagnostics/validation_errors.rs @@ -20,6 +20,7 @@ use miette::Diagnostic; use thiserror::Error; use std::fmt::Display; +use std::ops::{Add, Neg}; use cedar_policy_core::impl_diagnostic_from_source_loc_opt_field; use cedar_policy_core::parser::Loc; @@ -448,6 +449,82 @@ impl Diagnostic for HierarchyNotRespected { } } +#[derive(Debug, Clone, Hash, Eq, PartialEq, Error, Copy, Ord, PartialOrd)] +/// Represents how many entity dereferences can be applied to a node. +pub struct EntityDerefLevel { + /// A negative value `-n` represents `n` too many dereferences + pub level: i64, +} + +impl Display for EntityDerefLevel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + write!(f, "{}", self.level) + } +} + +impl From for EntityDerefLevel { + fn from(value: u32) -> Self { + EntityDerefLevel { + level: value as i64, + } + } +} + +impl Default for EntityDerefLevel { + fn default() -> Self { + Self { level: 0 } + } +} + +impl Add for EntityDerefLevel { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + EntityDerefLevel { + level: self.level + rhs.level, + } + } +} + +impl Neg for EntityDerefLevel { + type Output = Self; + + fn neg(self) -> Self::Output { + EntityDerefLevel { level: -self.level } + } +} + +impl EntityDerefLevel { + /// Decrement the entity deref level + pub fn decrement(&self) -> Self { + EntityDerefLevel { + level: self.level - 1, + } + } +} + +/// Structure containing details about entity dereference level violation +#[derive(Debug, Clone, Hash, Eq, PartialEq, Error)] +#[error("for policy `{policy_id}`, the maximum allowed level {allowed_level} is violated. Actual level is {}", (allowed_level.add(actual_level.neg())))] +pub struct EntityDerefLevelViolation { + /// Source location + pub source_loc: Option, + /// Policy ID where the error occurred + pub policy_id: PolicyID, + /// The maximum level allowed by the schema + pub allowed_level: EntityDerefLevel, + /// The actual level this policy uses + pub actual_level: EntityDerefLevel, +} + +impl Diagnostic for EntityDerefLevelViolation { + impl_diagnostic_from_source_loc_opt_field!(source_loc); + + fn help<'a>(&'a self) -> Option> { + Some(Box::new(format!("Consider increasing the level"))) + } +} + /// The policy uses an empty set literal in a way that is forbidden #[derive(Debug, Clone, Hash, Eq, PartialEq, Error)] #[error("for policy `{policy_id}`, empty set literals are forbidden in policies")] diff --git a/cedar-policy-validator/src/entity_manifest.rs b/cedar-policy-validator/src/entity_manifest.rs index d0250b7254..0adbb4ea9e 100644 --- a/cedar-policy-validator/src/entity_manifest.rs +++ b/cedar-policy-validator/src/entity_manifest.rs @@ -407,7 +407,7 @@ pub fn compute_entity_manifest( // using static analysis compute_root_trie(&typechecked_expr, policy.id()) } - PolicyCheck::Irrelevant(_) => { + PolicyCheck::Irrelevant(_, _) => { // this policy is irrelevant, so we need no data Ok(RootAccessTrie::new()) } diff --git a/cedar-policy-validator/src/level_validate.rs b/cedar-policy-validator/src/level_validate.rs new file mode 100644 index 0000000000..50fb7dfb20 --- /dev/null +++ b/cedar-policy-validator/src/level_validate.rs @@ -0,0 +1,328 @@ +/* + * 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. + */ + +//! Implementation of level validation (RFC 76) + +use super::*; +use cedar_policy_core::ast::{BinaryOp, PolicyID}; +use typecheck::PolicyCheck; +use validation_errors::{EntityDerefLevel, EntityDerefLevelViolation}; + +impl Validator { + /// Run `validate_policy` in strict mode against a single static policy or template (note + /// that Core `Template` includes static policies as well), gathering all + /// validation errors and warnings in the returned iterators. + /// If strict validation passes, we will also perform level validation (see RFC 76). + pub(crate) fn strict_validate_policy_with_level<'a>( + &'a self, + p: &'a Template, + max_deref_level: u32, + ) -> ( + impl Iterator + 'a, + impl Iterator + 'a, + ) { + let (errors, warnings) = self.validate_policy(p, ValidationMode::Strict); + + let mut peekable_errors = errors.peekable(); + + // Only perform level validation if strict validation passed. + if peekable_errors.peek().is_none() { + let levels_errors = + self.check_entity_deref_level(p, &EntityDerefLevel::from(max_deref_level), p.id()); + (peekable_errors.chain(levels_errors), warnings) + } else { + (peekable_errors.into_iter().chain(vec![]), warnings) + } + } + + /// Check that `t` respects `max_allowed_level` + /// This assumes that (strict) typechecking has passed + fn check_entity_deref_level<'a>( + &'a self, + t: &'a Template, + max_allowed_level: &EntityDerefLevel, + policy_id: &PolicyID, + ) -> Vec { + let typechecker = Typechecker::new(&self.schema, ValidationMode::Strict, t.id().clone()); + let type_annotated_asts = typechecker.typecheck_by_request_env(t); + let mut errs = vec![]; + for (_, policy_check) in type_annotated_asts { + match policy_check { + PolicyCheck::Success(e) | PolicyCheck::Irrelevant(_, e) => { + let res = + self.check_entity_deref_level_helper(&e, max_allowed_level, policy_id); + match res.1 { + Some(e) => errs.push(ValidationError::EntityDerefLevelViolation(e)), + None => (), + } + } + // PANIC SAFETY: We only validate the level after strict validation passed + #[allow(clippy::unreachable)] + PolicyCheck::Fail(_) => unreachable!(), + } + } + errs + } + + fn min( + v: impl IntoIterator)>, + ) -> (EntityDerefLevel, Option) { + let p = v.into_iter().min_by(|(l1, _), (l2, _)| l1.cmp(l2)); + match p { + Some(p) => p.clone(), + None => (EntityDerefLevel { level: 0 }, None), + } + } + + /// Walk the type-annotated AST and compute the used level and possible violation + /// Returns a tuple of `(actual level used, optional violation information)` + fn check_entity_deref_level_helper<'a>( + &'a self, + e: &cedar_policy_core::ast::Expr>, + max_allowed_level: &EntityDerefLevel, + policy_id: &PolicyID, + ) -> (EntityDerefLevel, Option) { + use crate::types::{EntityRecordKind, Type}; + use cedar_policy_core::ast::ExprKind; + match e.expr_kind() { + ExprKind::Lit(_) => ( + EntityDerefLevel { level: 0 }, //Literals can't be dereferenced + None, + ), + ExprKind::Var(_) => (max_allowed_level.clone(), None), //Roots start at `max_allowed_level` + ExprKind::Slot(_) => (EntityDerefLevel { level: 0 }, None), //Slot will be replaced by Entity literal so treat the same + ExprKind::Unknown(_) => ( + EntityDerefLevel { level: 0 }, //Can't dereference an unknown + None, + ), + ExprKind::If { + test_expr, + then_expr, + else_expr, + } => { + let es = [test_expr, then_expr, else_expr]; + let v: Vec<(EntityDerefLevel, Option<_>)> = es + .iter() + .map(|l| self.check_entity_deref_level_helper(l, max_allowed_level, policy_id)) + .collect(); + Self::min(v) + } + ExprKind::And { left, right } | ExprKind::Or { left, right } => { + let es = [left, right]; + let v: Vec<(EntityDerefLevel, Option<_>)> = es + .iter() + .map(|l| self.check_entity_deref_level_helper(l, max_allowed_level, policy_id)) + .collect(); + Self::min(v) + } + ExprKind::UnaryApp { arg, .. } => { + self.check_entity_deref_level_helper(arg, max_allowed_level, policy_id) + } + // `In` operator decrements the LHS only + ExprKind::BinaryApp { op, arg1, arg2 } if op == &BinaryOp::In => { + let lhs = self.check_entity_deref_level_helper(arg1, max_allowed_level, policy_id); + let rhs = self.check_entity_deref_level_helper(arg2, max_allowed_level, policy_id); + let lhs = (lhs.0.decrement(), lhs.1); + let new_level = Self::min(vec![lhs, rhs]).0; + if new_level.level < 0 { + ( + new_level, + Some(EntityDerefLevelViolation { + source_loc: e.source_loc().cloned(), + policy_id: policy_id.clone(), + actual_level: new_level, + allowed_level: max_allowed_level.clone(), + }), + ) + } else { + (new_level, None) + } + } + ExprKind::BinaryApp { arg1, arg2, .. } => { + let es = [arg1, arg2]; + let v: Vec<(EntityDerefLevel, Option<_>)> = es + .iter() + .map(|l| self.check_entity_deref_level_helper(l, max_allowed_level, policy_id)) + .collect(); + Self::min(v) + } + ExprKind::ExtensionFunctionApp { args, .. } => { + let v: Vec<(EntityDerefLevel, Option<_>)> = args + .iter() + .map(|l| self.check_entity_deref_level_helper(l, max_allowed_level, policy_id)) + .collect(); + Self::min(v) + } + ExprKind::GetAttr { expr, attr } + if matches!(expr.expr_kind(), ExprKind::Record(..)) => + { + match expr.expr_kind() { + ExprKind::Record(m) => { + // PANIC SAFETY: Strict validation checked that this access is safe + #[allow(clippy::unwrap_used)] + self.check_entity_deref_level_helper( + m.get(attr).unwrap(), + max_allowed_level, + policy_id, + ) + } + // PANIC SAFETY: We just checked that this node is a Record + #[allow(clippy::unreachable)] + _ => unreachable!(), + } + } + ExprKind::GetAttr { expr, .. } | ExprKind::HasAttr { expr, .. } => match expr + .as_ref() + .data() + { + Some(ty) => { + let child_level_info = + self.check_entity_deref_level_helper(expr, max_allowed_level, policy_id); + match ty { + Type::EntityOrRecord(EntityRecordKind::Entity { .. }) + | Type::EntityOrRecord(EntityRecordKind::ActionEntity { .. }) => { + let child_level = child_level_info.0; + let new_level = child_level.decrement(); + if new_level.level < 0 { + ( + new_level, + Some(EntityDerefLevelViolation { + source_loc: e.source_loc().cloned(), + policy_id: policy_id.clone(), + actual_level: new_level, + allowed_level: max_allowed_level.clone(), + }), + ) + } else { + (new_level, None) + } + } + Type::EntityOrRecord(EntityRecordKind::AnyEntity) => { + // AnyEntity cannot be dereferenced + (EntityDerefLevel { level: 0 }, None) + } + _ => child_level_info, + } + } + // PANIC SAFETY: Strict validation passed, so annotating the AST will succeed + #[allow(clippy::unreachable)] + None => unreachable!("Expected type-annotated AST"), + }, + ExprKind::Like { expr, .. } | ExprKind::Is { expr, .. } => { + self.check_entity_deref_level_helper(expr, max_allowed_level, policy_id) + } + ExprKind::Set(elems) => { + let v: Vec<(EntityDerefLevel, Option<_>)> = elems + .iter() + .map(|l| self.check_entity_deref_level_helper(l, max_allowed_level, policy_id)) + .collect(); + Self::min(v) + } + ExprKind::Record(fields) => { + let v: Vec<(EntityDerefLevel, Option<_>)> = fields + .iter() + .map(|(_, l)| { + self.check_entity_deref_level_helper(l, max_allowed_level, policy_id) + }) + .collect(); + Self::min(v) + } + } + } +} + +#[cfg(test)] +mod levels_validation_tests { + use super::*; + use cedar_policy_core::parser; + + fn get_schema() -> ValidatorSchema { + json_schema::Fragment::from_json_str( + r#" + { + "": { + "entityTypes": { + "User": { + "memberOfTypes": ["User"] + }, + "Photo": { + "shape": { + "type": "Record", + "attributes": { + "foo": { + "type": "Entity", + "name": "User", + "required": true + } + } + } + } + }, + "actions": { + "view": { + "appliesTo": { + "resourceTypes": [ "Photo" ], + "principalTypes": [ "User" ] + } + } + } + } + } + "#, + ) + .expect("Schema parse error.") + .try_into() + .expect("Expected valid schema.") + } + + #[test] + fn test_levels_validation_passes() { + let schema = get_schema(); + let validator = Validator::new(schema); + + let mut set = PolicySet::new(); + let src = r#"permit(principal == User::"һenry", action, resource) when {1 > 0};"#; + let p = parser::parse_policy(None, src).unwrap(); + set.add_static(p).unwrap(); + + let template_name = PolicyID::from_string("policy0"); + let result = validator.check_entity_deref_level( + set.get_template(&template_name).unwrap(), + &EntityDerefLevel { level: 0 }, + &template_name, + ); + assert!(result.is_empty()); + } + + #[test] + fn test_levels_validation_fails() { + let schema = get_schema(); + let validator = Validator::new(schema); + + let mut set = PolicySet::new(); + let src = r#"permit(principal == User::"һenry", action, resource) when {principal in resource.foo};"#; + let p = parser::parse_policy(None, src).unwrap(); + set.add_static(p).unwrap(); + + let template_name = PolicyID::from_string("policy0"); + let result = validator.check_entity_deref_level( + set.get_template(&template_name).unwrap(), + &EntityDerefLevel { level: 0 }, + &template_name, + ); + assert!(result.len() == 1); + } +} diff --git a/cedar-policy-validator/src/lib.rs b/cedar-policy-validator/src/lib.rs index d9e6046772..8ccf184e80 100644 --- a/cedar-policy-validator/src/lib.rs +++ b/cedar-policy-validator/src/lib.rs @@ -34,6 +34,8 @@ use cedar_policy_core::ast::{Policy, PolicySet, Template}; use serde::Serialize; use std::collections::HashSet; +#[cfg(feature = "level-validate")] +mod level_validate; #[cfg(feature = "entity-manifest")] pub mod entity_manifest; @@ -56,6 +58,7 @@ pub use str_checks::confusable_string_checks; pub mod cedar_schema; pub mod typecheck; use typecheck::Typechecker; + pub mod types; /// Used to select how a policy will be validated. @@ -127,6 +130,33 @@ impl Validator { ) } + #[cfg(feature = "level-validate")] + /// Strictly validate all templates, links, and static policies in a policy set. + /// If strict validation passes, also run level validation with `max_deref_level` + /// (see RFC 76). + /// Return a `ValidationResult`. + pub fn strict_validate_with_level( + &self, + policies: &PolicySet, + max_deref_level: u32, + ) -> ValidationResult { + let validate_policy_results: (Vec<_>, Vec<_>) = policies + .all_templates() + .map(|p| self.strict_validate_policy_with_level(p, max_deref_level)) + .unzip(); + let template_and_static_policy_errs = validate_policy_results.0.into_iter().flatten(); + let template_and_static_policy_warnings = validate_policy_results.1.into_iter().flatten(); + let link_errs = policies + .policies() + .filter_map(|p| self.validate_slots(p, ValidationMode::Strict)) + .flatten(); + ValidationResult::new( + template_and_static_policy_errs.chain(link_errs), + template_and_static_policy_warnings + .chain(confusable_string_checks(policies.all_templates())), + ) + } + /// Run all validations against a single static policy or template (note /// that Core `Template` includes static policies as well), gathering all /// validation errors and warnings in the returned iterators. diff --git a/cedar-policy-validator/src/typecheck.rs b/cedar-policy-validator/src/typecheck.rs index a1038f2e9a..8e16d63339 100644 --- a/cedar-policy-validator/src/typecheck.rs +++ b/cedar-policy-validator/src/typecheck.rs @@ -52,7 +52,7 @@ pub enum PolicyCheck { /// Policy will evaluate to a bool Success(Expr>), /// Policy will always evaluate to false, and may have errors - Irrelevant(Vec), + Irrelevant(Vec, Expr>), /// Policy will have errors Fail(Vec), } @@ -106,7 +106,7 @@ impl<'a> Typechecker<'a> { (true, true), |(all_false, all_succ), (_, check)| match check { PolicyCheck::Success(_) => (false, all_succ), - PolicyCheck::Irrelevant(err) => { + PolicyCheck::Irrelevant(err, _) => { let no_err = err.is_empty(); type_errors.extend(err); (all_false, all_succ && no_err) @@ -155,7 +155,10 @@ impl<'a> Typechecker<'a> { (false, true, None) => PolicyCheck::Fail(type_errors), (false, true, Some(e)) => PolicyCheck::Success(e), (false, false, _) => PolicyCheck::Fail(type_errors), - (true, _, _) => PolicyCheck::Irrelevant(type_errors), + (true, _, Some(e)) => PolicyCheck::Irrelevant(type_errors, e), + // PANIC SAFETY: `is_false` implies `e` has a type implies `Some(e)`. + #[allow(clippy::unreachable)] + (true, _, None) => unreachable!(), } }) } @@ -218,7 +221,12 @@ impl<'a> Typechecker<'a> { (false, true, None) => policy_checks.push(PolicyCheck::Fail(type_errors)), (false, true, Some(e)) => policy_checks.push(PolicyCheck::Success(e)), (false, false, _) => policy_checks.push(PolicyCheck::Fail(type_errors)), - (true, _, _) => policy_checks.push(PolicyCheck::Irrelevant(type_errors)), + (true, _, Some(e)) => { + policy_checks.push(PolicyCheck::Irrelevant(type_errors, e)) + } + // PANIC SAFETY: `is_false` implies `e` has a type implies `Some(e)`. + #[allow(clippy::unreachable)] + (true, _, None) => unreachable!(), } } } diff --git a/cedar-policy-validator/src/typecheck/test/policy.rs b/cedar-policy-validator/src/typecheck/test/policy.rs index 9111574ab3..3ee6059f40 100644 --- a/cedar-policy-validator/src/typecheck/test/policy.rs +++ b/cedar-policy-validator/src/typecheck/test/policy.rs @@ -149,7 +149,7 @@ fn policy_checked_in_multiple_envs() { assert!( env_checks .iter() - .filter(|(_, check)| { matches!(check, PolicyCheck::Irrelevant(_)) }) + .filter(|(_, check)| { matches!(check, PolicyCheck::Irrelevant(_, _)) }) .count() == 1 ); @@ -171,7 +171,7 @@ fn policy_checked_in_multiple_envs() { assert!( env_checks .iter() - .filter(|(_, check)| { matches!(check, PolicyCheck::Irrelevant(_)) }) + .filter(|(_, check)| { matches!(check, PolicyCheck::Irrelevant(_, _)) }) .count() == 2 ); diff --git a/cedar-policy/Cargo.toml b/cedar-policy/Cargo.toml index 2a758f7f4a..e7b1a62e6d 100644 --- a/cedar-policy/Cargo.toml +++ b/cedar-policy/Cargo.toml @@ -47,12 +47,13 @@ corpus-timing = [] # Experimental features. # Enable all experimental features with `cargo build --features "experimental"` -experimental = ["partial-eval", "permissive-validate", "partial-validate", "entity-manifest", "entity-tags"] +experimental = ["partial-eval", "permissive-validate", "partial-validate", "level-validate", "entity-manifest", "entity-tags"] entity-manifest = ["cedar-policy-validator/entity-manifest"] entity-tags = ["cedar-policy-core/entity-tags", "cedar-policy-validator/entity-tags"] partial-eval = ["cedar-policy-core/partial-eval", "cedar-policy-validator/partial-eval"] permissive-validate = [] partial-validate = ["cedar-policy-validator/partial-validate"] +level-validate = ["cedar-policy-validator/level-validate"] wasm = ["serde-wasm-bindgen", "tsify", "wasm-bindgen"] [lib] diff --git a/cedar-policy/src/api.rs b/cedar-policy/src/api.rs index f3fdd3b3f5..8d7f5a6f9d 100644 --- a/cedar-policy/src/api.rs +++ b/cedar-policy/src/api.rs @@ -1256,6 +1256,25 @@ impl Validator { pub fn validate(&self, pset: &PolicySet, mode: ValidationMode) -> ValidationResult { ValidationResult::from(self.0.validate(&pset.ast, mode.into())) } + + #[cfg(feature = "level-validate")] + /// Strictly validate all policies in a policy set, collecting all validation errors + /// found into the returned `ValidationResult`. If strict validation passes, run level + /// validation (RFC 76). Each error is returned together with the policy id of the policy + /// where the error was found. If a policy id included in the input policy set does not + /// appear in the output iterator, then that policy passed the validator. If the function + /// `validation_passed` returns true, then there were no validation errors found, so + /// all policies in the policy set have passed the validator. + pub fn strict_validate_with_level( + &self, + pset: &PolicySet, + max_deref_level: u32, + ) -> ValidationResult { + ValidationResult::from( + self.0 + .strict_validate_with_level(&pset.ast, max_deref_level), + ) + } } /// Contains all the type information used to construct a `Schema` that can be diff --git a/cedar-policy/src/api/err.rs b/cedar-policy/src/api/err.rs index 1b92974413..49cbe68134 100644 --- a/cedar-policy/src/api/err.rs +++ b/cedar-policy/src/api/err.rs @@ -421,6 +421,10 @@ pub enum ValidationError { #[error(transparent)] #[diagnostic(transparent)] InternalInvariantViolation(#[from] validation_errors::InternalInvariantViolation), + /// Entity level violation + #[error(transparent)] + #[diagnostic(transparent)] + EntityDerefLevelViolation(#[from] validation_errors::EntityDerefLevelViolation), } impl ValidationError { @@ -445,6 +449,7 @@ impl ValidationError { Self::NonLitExtConstructor(e) => e.policy_id(), Self::HierarchyNotRespected(e) => e.policy_id(), Self::InternalInvariantViolation(e) => e.policy_id(), + Self::EntityDerefLevelViolation(e) => e.policy_id(), } } } @@ -503,6 +508,10 @@ impl From for ValidationError { cedar_policy_validator::ValidationError::InternalInvariantViolation(e) => { Self::InternalInvariantViolation(e.into()) } + #[cfg(feature = "level-validate")] + cedar_policy_validator::ValidationError::EntityDerefLevelViolation(e) => { + Self::EntityDerefLevelViolation(e.into()) + } } } } diff --git a/cedar-policy/src/api/err/validation_errors.rs b/cedar-policy/src/api/err/validation_errors.rs index 0e443b5cec..61ff0598a9 100644 --- a/cedar-policy/src/api/err/validation_errors.rs +++ b/cedar-policy/src/api/err/validation_errors.rs @@ -71,6 +71,7 @@ wrap_core_error!(UndefinedFunction); wrap_core_error!(WrongNumberArguments); wrap_core_error!(FunctionArgumentValidation); wrap_core_error!(HierarchyNotRespected); +wrap_core_error!(EntityDerefLevelViolation); wrap_core_error!(EmptySetForbidden); wrap_core_error!(NonLitExtConstructor); wrap_core_error!(InternalInvariantViolation); diff --git a/cedar-policy/src/tests.rs b/cedar-policy/src/tests.rs index 46709d27c3..40fa8f8757 100644 --- a/cedar-policy/src/tests.rs +++ b/cedar-policy/src/tests.rs @@ -3923,6 +3923,248 @@ mod partial_schema { } } +#[cfg(feature = "level-validate")] +mod level_validation_tests { + use crate::{Policy, PolicySet, ValidationError, Validator}; + use cool_asserts::assert_matches; + use serde_json::json; + + use super::Schema; + + fn get_schema() -> Schema { + Schema::from_json_value(json!( + { + "": { + "entityTypes": { + "User": { + "shape": { + "type": "Record", + "attributes": { + "is_admin": { + "type": "Boolean", + "required": true + }, + "profile_pic": { + "type": "Entity", + "name": "Photo", + "required": true + } + } + }, + "memberOfTypes": ["User"] + }, + "Photo": { + "shape": { + "type": "Record", + "attributes": { + "foo": { + "type": "Entity", + "name": "User", + "required": true + } + } + } + } + }, + "actions": { + "view": { + "appliesTo": { + "resourceTypes": [ "Photo" ], + "principalTypes": [ "User" ] + } + } + } + } + })) + .expect("Schema parse error.") + .try_into() + .expect("Expected valid schema.") + } + + #[test] + fn level_validation_passes() { + let schema = get_schema(); + let validator = Validator::new(schema); + + let mut set = PolicySet::new(); + let src = r#"permit(principal == User::"һenry", action, resource) when {1 > 0};"#; + let p = Policy::parse(None, src).unwrap(); + set.add(p).unwrap(); + + let result = validator.strict_validate_with_level(&set, 0); + assert!(result.validation_passed()); + } + + #[test] + fn level_validation_fails() { + let schema = get_schema(); + let validator = Validator::new(schema); + + let mut set = PolicySet::new(); + let src = r#"permit(principal == User::"һenry", action, resource) when {principal in resource.foo};"#; + let p = Policy::parse(None, src).unwrap(); + set.add(p).unwrap(); + + let result = validator.strict_validate_with_level(&set, 0); + assert!(!result.validation_passed()); + assert_eq!(result.validation_errors().count(), 1); + assert_matches!( + result.validation_errors().next().unwrap(), + ValidationError::EntityDerefLevelViolation(_) + ); + } + + #[test] + fn level_validation_fails_rhs_in() { + let schema = get_schema(); + let validator = Validator::new(schema); + + let mut set = PolicySet::new(); + let src = r#"permit(principal == User::"һenry", action, resource) when {principal.profile_pic in resource.foo.profile_pic};"#; + let p = Policy::parse(None, src).unwrap(); + set.add(p).unwrap(); + + let result = validator.strict_validate_with_level(&set, 0); + assert!(!result.validation_passed()); + assert_eq!(result.validation_errors().count(), 1); + let err = result.validation_errors().next().unwrap(); + assert_matches!(err, ValidationError::EntityDerefLevelViolation(_)); + match err { + ValidationError::EntityDerefLevelViolation(inner) => { + assert!(format!("{}", inner.to_string()).contains("Actual level is 2")); + } + _ => unreachable!(), + }; + } + + #[test] + fn level_validation_passes_level2() { + let schema = get_schema(); + let validator = Validator::new(schema); + + let mut set = PolicySet::new(); + let src = r#"permit(principal == User::"һenry", action, resource) when { resource.foo.is_admin };"#; + let p = Policy::parse(None, src).unwrap(); + set.add(p).unwrap(); + + let result = validator.strict_validate_with_level(&set, 2); + assert!(result.validation_passed()); + } + + #[test] + fn level_validation_irrelevant_policy_passes() { + let schema = get_schema(); + let validator = Validator::new(schema); + + let mut set = PolicySet::new(); + let src = r#"permit(principal == User::"һenry", action, resource) when { false && principal.is_admin };"#; + let p = Policy::parse(None, src).unwrap(); + set.add(p).unwrap(); + + let result = validator.strict_validate_with_level(&set, 0); + assert!(result.validation_passed()); + } + + #[test] + fn level_validation_irrelevant_policy_fails() { + let schema = get_schema(); + let validator = Validator::new(schema); + + let mut set = PolicySet::new(); + let src = r#"permit(principal == User::"һenry", action, resource) when { principal.is_admin && false };"#; + let p = Policy::parse(None, src).unwrap(); + set.add(p).unwrap(); + + let result = validator.strict_validate_with_level(&set, 0); + assert!(!result.validation_passed()); + assert_eq!(result.validation_errors().count(), 1); + assert_matches!( + result.validation_errors().next().unwrap(), + ValidationError::EntityDerefLevelViolation(_) + ); + } + + #[test] + fn level_validation_fails_ite() { + let schema = get_schema(); + let validator = Validator::new(schema); + + let mut set = PolicySet::new(); + let src = r#"permit(principal == User::"һenry", action, resource) when { if principal == User::"henry" then true else principal in resource.foo };"#; + let p = Policy::parse(None, src).unwrap(); + set.add(p).unwrap(); + + let result = validator.strict_validate_with_level(&set, 0); + assert!(!result.validation_passed()); + assert_eq!(result.validation_errors().count(), 1); + assert_matches!( + result.validation_errors().next().unwrap(), + ValidationError::EntityDerefLevelViolation(_) + ); + } + + #[test] + fn level_validation_passes_ite() { + let schema = get_schema(); + let validator = Validator::new(schema); + + let mut set = PolicySet::new(); + let src = r#"permit(principal == User::"һenry", action, resource) when { if principal == User::"henry" then true else principal in resource.foo };"#; + let p = Policy::parse(None, src).unwrap(); + set.add(p).unwrap(); + + let result = validator.strict_validate_with_level(&set, 1); + assert!(result.validation_passed()); + } + + #[test] + fn level_validation_fails_record() { + let schema = get_schema(); + let validator = Validator::new(schema); + + let mut set = PolicySet::new(); + let src = r#"permit(principal == User::"һenry", action, resource) when { { "foo": true, "bar": resource.foo.is_admin }.bar };"#; + let p = Policy::parse(None, src).unwrap(); + set.add(p).unwrap(); + + let result = validator.strict_validate_with_level(&set, 0); + assert!(!result.validation_passed()); + assert_eq!(result.validation_errors().count(), 1); + assert_matches!( + result.validation_errors().next().unwrap(), + ValidationError::EntityDerefLevelViolation(_) + ); + } + + #[test] + fn level_validation_passes_record_increased_level() { + let schema = get_schema(); + let validator = Validator::new(schema); + + let mut set = PolicySet::new(); + let src = r#"permit(principal == User::"һenry", action, resource) when { { "foo": true, "bar": resource.foo.is_admin }.bar };"#; + let p = Policy::parse(None, src).unwrap(); + set.add(p).unwrap(); + + let result = validator.strict_validate_with_level(&set, 2); + assert!(result.validation_passed()); + } + + #[test] + fn level_validation_passes_record_other_attr() { + let schema = get_schema(); + let validator = Validator::new(schema); + + let mut set = PolicySet::new(); + let src = r#"permit(principal == User::"һenry", action, resource) when { { "foo": true, "bar": resource.foo.is_admin }.foo };"#; + let p = Policy::parse(None, src).unwrap(); + set.add(p).unwrap(); + + let result = validator.strict_validate_with_level(&set, 0); + assert!(result.validation_passed()); + } +} + mod template_tests { use std::str::FromStr;