#![deny(
missing_docs,
rustdoc::broken_intra_doc_links,
rustdoc::private_intra_doc_links,
rustdoc::invalid_codeblock_attributes,
rustdoc::invalid_html_tags,
rustdoc::invalid_rust_codeblocks,
rustdoc::bare_urls,
clippy::doc_markdown
)]
#![cfg_attr(
feature = "wasm",
allow(
non_snake_case,
reason = "Wasm/TypeScript doesn't use snake case identifiers by convention"
)
)]
use crate::ast::{Policy, PolicySet, Template};
use std::collections::HashSet;
mod level_validate;
mod coreschema;
#[cfg(feature = "entity-manifest")]
pub mod entity_manifest;
pub use coreschema::*;
mod diagnostics;
pub use diagnostics::*;
mod expr_iterator;
mod extension_schema;
mod extensions;
mod rbac;
mod schema;
pub use schema::err::*;
pub use schema::*;
mod deprecated_schema_compat;
pub mod json_schema;
mod str_checks;
pub use str_checks::confusable_string_checks;
pub mod cedar_schema;
pub mod typecheck;
use typecheck::Typechecker;
mod partition_nonempty;
pub mod types;
#[derive(Default, Eq, PartialEq, Copy, Clone, Debug)]
pub enum ValidationMode {
#[default]
Strict,
Permissive,
#[cfg(feature = "partial-validate")]
Partial,
}
impl ValidationMode {
fn is_partial(self) -> bool {
match self {
ValidationMode::Strict | ValidationMode::Permissive => false,
#[cfg(feature = "partial-validate")]
ValidationMode::Partial => true,
}
}
fn is_strict(self) -> bool {
match self {
ValidationMode::Strict => true,
ValidationMode::Permissive => false,
#[cfg(feature = "partial-validate")]
ValidationMode::Partial => false,
}
}
}
#[derive(Debug, Clone)]
pub struct Validator {
schema: ValidatorSchema,
}
impl Validator {
pub fn new(schema: ValidatorSchema) -> Validator {
Self { schema }
}
pub fn schema(&self) -> &ValidatorSchema {
&self.schema
}
pub fn validate(&self, policies: &PolicySet, mode: ValidationMode) -> ValidationResult {
let validate_policy_results: (Vec<_>, Vec<_>) = policies
.all_templates()
.map(|p| self.validate_policy(p, mode))
.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, mode))
.flatten();
ValidationResult::new(
template_and_static_policy_errs.chain(link_errs),
template_and_static_policy_warnings
.chain(confusable_string_checks(policies.all_templates())),
)
}
pub fn validate_with_level(
&self,
policies: &PolicySet,
mode: ValidationMode,
max_deref_level: u32,
) -> ValidationResult {
let validate_policy_results: (Vec<_>, Vec<_>) = policies
.all_templates()
.map(|p| self.validate_policy_with_level(p, mode, 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, mode))
.flatten();
ValidationResult::new(
template_and_static_policy_errs.chain(link_errs),
template_and_static_policy_warnings
.chain(confusable_string_checks(policies.all_templates())),
)
}
fn validate_policy<'a>(
&'a self,
p: &'a Template,
mode: ValidationMode,
) -> (
impl Iterator<Item = ValidationError> + 'a,
impl Iterator<Item = ValidationWarning> + 'a,
) {
let validation_errors = if mode.is_partial() {
None
} else {
Some(
Validator::validate_entity_types(&self.schema, p)
.chain(Validator::validate_enum_entity(&self.schema, p))
.chain(Validator::validate_action_ids(&self.schema, p))
.chain(self.validate_template_action_application(p)),
)
}
.into_iter()
.flatten();
let (errors, warnings) = self.typecheck_policy(p, mode);
(validation_errors.chain(errors), warnings)
}
pub fn validate_entity_types_and_literals<'a>(
schema: &'a ValidatorSchema,
p: &'a Template,
) -> impl Iterator<Item = ValidationError> + 'a {
Validator::validate_entity_types(schema, p)
.chain(Validator::validate_enum_entity(schema, p))
.chain(Validator::validate_action_ids(schema, p))
}
fn validate_slots<'a>(
&'a self,
p: &'a Policy,
mode: ValidationMode,
) -> Option<impl Iterator<Item = ValidationError> + 'a> {
if p.is_static() {
return None;
}
if mode.is_partial() {
return None;
}
Some(
self.validate_entity_types_in_slots(p.id(), p.env())
.chain(self.validate_linked_action_application(p)),
)
}
fn typecheck_policy<'a>(
&'a self,
t: &'a Template,
mode: ValidationMode,
) -> (
impl Iterator<Item = ValidationError> + 'a,
impl Iterator<Item = ValidationWarning> + 'a,
) {
let typecheck = Typechecker::new(&self.schema, mode);
let mut errors = HashSet::new();
let mut warnings = HashSet::new();
typecheck.typecheck_policy(t, &mut errors, &mut warnings);
(errors.into_iter(), warnings.into_iter())
}
}
#[cfg(test)]
mod test {
use itertools::Itertools;
use std::{collections::HashMap, sync::Arc};
use crate::validator::types::Type;
use crate::validator::validation_errors::UnrecognizedActionIdHelp;
use crate::validator::Result;
use super::*;
use crate::{
ast::{self, PolicyID},
est::Annotations,
parser::{self, Loc},
};
use similar_asserts::assert_eq;
#[test]
fn top_level_validate() -> Result<()> {
let mut set = PolicySet::new();
let foo_type = "foo_type";
let bar_type = "bar_type";
let action_name = "action";
let schema_file = json_schema::NamespaceDefinition::new(
[
(
foo_type.parse().unwrap(),
json_schema::StandardEntityType {
member_of_types: vec![],
shape: json_schema::AttributesOrContext::default(),
tags: None,
}
.into(),
),
(
bar_type.parse().unwrap(),
json_schema::StandardEntityType {
member_of_types: vec![],
shape: json_schema::AttributesOrContext::default(),
tags: None,
}
.into(),
),
],
[(
action_name.into(),
json_schema::ActionType {
applies_to: Some(json_schema::ApplySpec {
principal_types: vec!["foo_type".parse().unwrap()],
resource_types: vec!["bar_type".parse().unwrap()],
context: json_schema::AttributesOrContext::default(),
}),
member_of: None,
attributes: None,
annotations: Annotations::new(),
loc: None,
#[cfg(feature = "extended-schema")]
defn_loc: None,
},
)],
);
let schema = schema_file.try_into().unwrap();
let validator = Validator::new(schema);
let policy_a_src = r#"permit(principal in foo_type::"a", action == Action::"actin", resource == bar_type::"b");"#;
let policy_a = parser::parse_policy(Some(PolicyID::from_string("pola")), policy_a_src)
.expect("Test Policy Should Parse");
set.add_static(policy_a)
.expect("Policy already present in PolicySet");
let policy_b_src = r#"permit(principal in foo_tye::"a", action == Action::"action", resource == br_type::"b");"#;
let policy_b = parser::parse_policy(Some(PolicyID::from_string("polb")), policy_b_src)
.expect("Test Policy Should Parse");
set.add_static(policy_b)
.expect("Policy already present in PolicySet");
let result = validator.validate(&set, ValidationMode::default());
let principal_err = ValidationError::unrecognized_entity_type(
Some(Loc::new(20..27, Arc::from(policy_b_src))),
PolicyID::from_string("polb"),
"foo_tye".to_string(),
Some("foo_type".to_string()),
);
let resource_err = ValidationError::unrecognized_entity_type(
Some(Loc::new(74..81, Arc::from(policy_b_src))),
PolicyID::from_string("polb"),
"br_type".to_string(),
Some("bar_type".to_string()),
);
let action_err = ValidationError::unrecognized_action_id(
Some(Loc::new(45..60, Arc::from(policy_a_src))),
PolicyID::from_string("pola"),
"Action::\"actin\"".to_string(),
Some(UnrecognizedActionIdHelp::SuggestAlternative(
"Action::\"action\"".to_string(),
)),
);
assert!(!result.validation_passed());
assert!(
result.validation_errors().contains(&principal_err),
"{result:?}"
);
assert!(
result.validation_errors().contains(&resource_err),
"{result:?}"
);
assert!(
result.validation_errors().contains(&action_err),
"{result:?}"
);
Ok(())
}
#[test]
fn top_level_validate_with_links() -> Result<()> {
let mut set = PolicySet::new();
let schema: ValidatorSchema = json_schema::Fragment::from_json_str(
r#"
{
"some_namespace": {
"entityTypes": {
"User": {
"shape": {
"type": "Record",
"attributes": {
"department": {
"type": "String"
},
"jobLevel": {
"type": "Long"
}
}
},
"memberOfTypes": [
"UserGroup"
]
},
"UserGroup": {},
"Photo" : {}
},
"actions": {
"view": {
"appliesTo": {
"resourceTypes": [
"Photo"
],
"principalTypes": [
"User"
]
}
}
}
}
}
"#,
)
.expect("Schema parse error.")
.try_into()
.expect("Expected valid schema.");
let validator = Validator::new(schema);
let t = parser::parse_policy_or_template(
Some(PolicyID::from_string("template")),
r#"permit(principal == some_namespace::User::"Alice", action, resource in ?resource);"#,
)
.expect("Parse Error");
let loc = t.loc().cloned();
set.add_template(t)
.expect("Template already present in PolicySet");
let result = validator.validate(&set, ValidationMode::default());
assert_eq!(
result.validation_errors().collect::<Vec<_>>(),
Vec::<&ValidationError>::new()
);
let mut values = HashMap::new();
values.insert(
ast::SlotId::resource(),
ast::EntityUID::from_components(
"some_namespace::Photo".parse().unwrap(),
ast::Eid::new("foo"),
None,
),
);
set.link(
ast::PolicyID::from_string("template"),
ast::PolicyID::from_string("link1"),
values,
)
.expect("Linking failed!");
let result = validator.validate(&set, ValidationMode::default());
assert!(result.validation_passed());
let mut values = HashMap::new();
values.insert(
ast::SlotId::resource(),
ast::EntityUID::from_components(
"some_namespace::Undefined".parse().unwrap(),
ast::Eid::new("foo"),
None,
),
);
set.link(
ast::PolicyID::from_string("template"),
ast::PolicyID::from_string("link2"),
values,
)
.expect("Linking failed!");
let result = validator.validate(&set, ValidationMode::default());
assert!(!result.validation_passed());
assert_eq!(result.validation_errors().count(), 2);
let undefined_err = ValidationError::unrecognized_entity_type(
None,
PolicyID::from_string("link2"),
"some_namespace::Undefined".to_string(),
Some("some_namespace::User".to_string()),
);
let invalid_action_err = ValidationError::invalid_action_application(
loc.clone(),
PolicyID::from_string("link2"),
false,
false,
);
let actual_undef_error = result
.validation_errors()
.find(|e| matches!(e, ValidationError::UnrecognizedEntityType(_)))
.unwrap();
assert_eq!(actual_undef_error, &undefined_err);
let actual_action_error = result
.validation_errors()
.find(|e| matches!(e, ValidationError::InvalidActionApplication(_)))
.unwrap();
assert_eq!(actual_action_error, &invalid_action_err);
let mut values = HashMap::new();
values.insert(
ast::SlotId::resource(),
ast::EntityUID::from_components(
"some_namespace::User".parse().unwrap(),
ast::Eid::new("foo"),
None,
),
);
set.link(
ast::PolicyID::from_string("template"),
ast::PolicyID::from_string("link3"),
values,
)
.expect("Linking failed!");
let result = validator.validate(&set, ValidationMode::default());
assert!(!result.validation_passed());
assert_eq!(result.validation_errors().count(), 3);
let invalid_action_err = ValidationError::invalid_action_application(
loc,
PolicyID::from_string("link3"),
false,
false,
);
assert!(result.validation_errors().contains(&invalid_action_err));
Ok(())
}
#[test]
fn validate_finds_warning_and_error() {
let schema: ValidatorSchema = json_schema::Fragment::from_json_str(
r#"
{
"": {
"entityTypes": {
"User": { }
},
"actions": {
"view": {
"appliesTo": {
"resourceTypes": [ "User" ],
"principalTypes": [ "User" ]
}
}
}
}
}
"#,
)
.expect("Schema parse error.")
.try_into()
.expect("Expected valid schema.");
let validator = Validator::new(schema);
let mut set = PolicySet::new();
let src = r#"permit(principal == User::"һenry", action, resource) when {1 > true};"#;
let p = parser::parse_policy(None, src).unwrap();
set.add_static(p).unwrap();
let result = validator.validate(&set, ValidationMode::default());
assert_eq!(
result.validation_errors().collect::<Vec<_>>(),
vec![&ValidationError::expected_type(
typecheck::test::test_utils::get_loc(src, "true"),
PolicyID::from_string("policy0"),
Type::primitive_long(),
Type::singleton_boolean(true),
None,
)]
);
assert_eq!(
result.validation_warnings().collect::<Vec<_>>(),
vec![&ValidationWarning::mixed_script_identifier(
None,
PolicyID::from_string("policy0"),
"һenry"
)]
);
}
}
#[cfg(test)]
#[expect(clippy::cognitive_complexity, reason = "unit test code")]
mod enumerated_entity_types {
use std::collections::HashMap;
use crate::{
ast::{Eid, EntityUID, ExprBuilder, PolicyID, PolicySet, SlotId, Template},
expr_builder::ExprBuilder as _,
extensions::Extensions,
parser::parse_policy_or_template,
};
use cool_asserts::assert_matches;
use itertools::Itertools;
use crate::validator::{
typecheck::test::test_utils::get_loc,
types::{EntityLUB, Type},
validation_errors::AttributeAccess,
ValidationError, ValidationWarning, Validator, ValidatorSchema,
};
#[track_caller]
fn schema() -> ValidatorSchema {
ValidatorSchema::from_json_value(
serde_json::json!(
{
"": { "entityTypes": {
"Foo": {
"enum": [ "foo" ],
},
"Bar": {
"memberOfTypes": ["Foo"],
},
"Other": { },
},
"actions": {
"a": {
"appliesTo": {
"principalTypes": ["Foo"],
"resourceTypes": ["Bar"],
}
}
}
}
}
),
Extensions::none(),
)
.unwrap()
}
#[test]
fn basic() {
let schema = schema();
let template = parse_policy_or_template(None, r#"permit(principal, action == Action::"a", resource) when { principal == Foo::"foo" };"#).unwrap();
let validator = Validator::new(schema);
let (errors, warnings) =
validator.validate_policy(&template, crate::validator::ValidationMode::Strict);
assert!(warnings.collect_vec().is_empty());
assert!(errors.collect_vec().is_empty());
}
#[test]
fn link() {
let schema = schema();
let template = parse_policy_or_template(
None,
r#"permit(principal in ?principal, action == Action::"a", resource);"#,
)
.unwrap();
let policy = Template::link(
std::sync::Arc::new(template),
PolicyID::from_string("test"),
HashMap::from_iter([(SlotId::principal(), r#"Other::"foo""#.parse().unwrap())]),
)
.unwrap();
let mut policy_set = PolicySet::new();
let _ = policy_set.add(policy);
let validator = Validator::new(schema);
let result = validator.validate(&policy_set, crate::validator::ValidationMode::Strict);
assert_eq!(result.validation_errors().collect_vec().len(), 1);
}
#[test]
fn basic_invalid() {
let schema = schema();
let template = parse_policy_or_template(None, r#"permit(principal, action == Action::"a", resource) when { principal == Foo::"fo" };"#).unwrap();
let validator = Validator::new(schema.clone());
let (errors, warnings) =
validator.validate_policy(&template, crate::validator::ValidationMode::Strict);
assert!(warnings.collect_vec().is_empty());
assert_matches!(&errors.collect_vec(), [ValidationError::InvalidEnumEntity(err)] => {
assert_eq!(err.err.choices, vec![Eid::new("foo")]);
assert_eq!(err.err.uid, EntityUID::with_eid_and_type("Foo", "fo").unwrap());
});
let template = parse_policy_or_template(
None,
r#"permit(principal == Foo::"🏈", action == Action::"a", resource);"#,
)
.unwrap();
let validator = Validator::new(schema.clone());
let (errors, warnings) =
validator.validate_policy(&template, crate::validator::ValidationMode::Strict);
assert!(warnings.collect_vec().is_empty());
assert_matches!(&errors.collect_vec(), [ValidationError::InvalidEnumEntity(err)] => {
assert_eq!(err.err.choices, vec![Eid::new("foo")]);
assert_eq!(err.err.uid, EntityUID::with_eid_and_type("Foo", "🏈").unwrap());
});
let template = parse_policy_or_template(
None,
r#"permit(principal in Foo::"🏈", action == Action::"a", resource);"#,
)
.unwrap();
let validator = Validator::new(schema.clone());
let (errors, warnings) =
validator.validate_policy(&template, crate::validator::ValidationMode::Strict);
assert!(warnings.collect_vec().is_empty());
assert_matches!(&errors.collect_vec(), [ValidationError::InvalidEnumEntity(err)] => {
assert_eq!(err.err.choices, vec![Eid::new("foo")]);
assert_eq!(err.err.uid, EntityUID::with_eid_and_type("Foo", "🏈").unwrap());
});
let template = parse_policy_or_template(
None,
r#"permit(principal, action == Action::"a", resource)
when { {"🏈": Foo::"🏈"} has "🏈" };
"#,
)
.unwrap();
let validator = Validator::new(schema.clone());
let (errors, warnings) =
validator.validate_policy(&template, crate::validator::ValidationMode::Strict);
assert!(warnings.collect_vec().is_empty());
assert_matches!(&errors.collect_vec(), [ValidationError::InvalidEnumEntity(err)] => {
assert_eq!(err.err.choices, vec![Eid::new("foo")]);
assert_eq!(err.err.uid, EntityUID::with_eid_and_type("Foo", "🏈").unwrap());
});
let template = parse_policy_or_template(
None,
r#"permit(principal, action == Action::"a", resource)
when { [Foo::"🏈"].isEmpty() };
"#,
)
.unwrap();
let validator = Validator::new(schema.clone());
let (errors, warnings) =
validator.validate_policy(&template, crate::validator::ValidationMode::Strict);
assert!(warnings.collect_vec().is_empty());
assert_matches!(&errors.collect_vec(), [ValidationError::InvalidEnumEntity(err)] => {
assert_eq!(err.err.choices, vec![Eid::new("foo")]);
assert_eq!(err.err.uid, EntityUID::with_eid_and_type("Foo", "🏈").unwrap());
});
let template = parse_policy_or_template(
None,
r#"permit(principal, action == Action::"a", resource)
when { [{"🏈": Foo::"🏈"}].isEmpty() };
"#,
)
.unwrap();
let validator = Validator::new(schema);
let (errors, warnings) =
validator.validate_policy(&template, crate::validator::ValidationMode::Strict);
assert!(warnings.collect_vec().is_empty());
assert_matches!(&errors.collect_vec(), [ValidationError::InvalidEnumEntity(err)] => {
assert_eq!(err.err.choices, vec![Eid::new("foo")]);
assert_eq!(err.err.uid, EntityUID::with_eid_and_type("Foo", "🏈").unwrap());
});
}
#[test]
fn no_attrs_allowed() {
let schema = schema();
let src = r#"permit(principal, action == Action::"a", resource) when { principal.foo == "foo" };"#;
let template = parse_policy_or_template(None, src).unwrap();
let validator = Validator::new(schema);
let (errors, warnings) =
validator.validate_policy(&template, crate::validator::ValidationMode::Strict);
assert!(warnings.collect_vec().is_empty());
assert_eq!(
errors.collect_vec(),
[ValidationError::unsafe_attribute_access(
get_loc(src, "principal.foo"),
PolicyID::from_string("policy0"),
AttributeAccess::EntityLUB(
EntityLUB::single_entity("Foo".parse().unwrap()),
vec!["foo".into()],
),
None,
false,
)]
);
}
#[test]
fn no_ancestors() {
let schema = schema();
let src = r#"permit(principal, action == Action::"a", resource) when { principal in Bar::"bar" };"#;
let template = parse_policy_or_template(None, src).unwrap();
let validator = Validator::new(schema);
let (errors, warnings) =
validator.validate_policy(&template, crate::validator::ValidationMode::Strict);
assert_eq!(
warnings.collect_vec(),
[ValidationWarning::impossible_policy(
get_loc(src, src),
PolicyID::from_string("policy0")
)]
);
assert!(errors.collect_vec().is_empty());
}
#[test]
fn no_tags_allowed() {
let schema = schema();
let src = r#"permit(principal, action == Action::"a", resource) when { principal.getTag("foo") == "foo" };"#;
let template = parse_policy_or_template(None, src).unwrap();
let validator = Validator::new(schema);
let (errors, warnings) =
validator.validate_policy(&template, crate::validator::ValidationMode::Strict);
assert!(warnings.collect_vec().is_empty());
assert_eq!(
errors.collect_vec(),
[ValidationError::unsafe_tag_access(
get_loc(src, r#"principal.getTag("foo")"#),
PolicyID::from_string("policy0"),
Some(EntityLUB::single_entity("Foo".parse().unwrap()),),
{
let builder = ExprBuilder::new();
let mut expr = builder.val("foo");
expr.set_data(Some(Type::primitive_string()));
expr
},
)]
);
}
}