#![forbid(unsafe_code)]
use std::collections::HashSet;
use cedar_policy_core::ast::{Policy, PolicySet, Template};
mod err;
mod str_checks;
pub use err::*;
mod expr_iterator;
mod extension_schema;
mod extensions;
mod fuzzy_match;
mod validation_result;
use serde::Serialize;
pub use validation_result::*;
mod rbac;
mod schema;
pub use schema::*;
mod schema_file_format;
pub use schema_file_format::*;
mod type_error;
pub use type_error::*;
pub mod typecheck;
pub mod types;
pub use str_checks::{confusable_string_checks, ValidationWarning, ValidationWarningKind};
use self::typecheck::Typechecker;
#[derive(Default, Eq, PartialEq, Copy, Clone, Debug, Serialize)]
pub enum ValidationMode {
#[default]
Strict,
Permissive,
}
impl ValidationMode {
fn is_strict(self) -> bool {
match self {
ValidationMode::Strict => true,
ValidationMode::Permissive => false,
}
}
}
#[derive(Debug)]
pub struct Validator {
schema: ValidatorSchema,
}
impl Validator {
pub fn new(schema: ValidatorSchema) -> Validator {
Self { schema }
}
pub fn validate<'a>(
&'a self,
policies: &'a PolicySet,
mode: ValidationMode,
) -> ValidationResult<'a> {
let template_and_static_policy_errs = policies
.all_templates()
.flat_map(|p| self.validate_policy(p, mode));
let link_errs = policies
.policies()
.filter_map(|p| self.validate_slots(p))
.flatten();
ValidationResult::new(template_and_static_policy_errs.chain(link_errs))
}
fn validate_policy<'a>(
&'a self,
p: &'a Template,
mode: ValidationMode,
) -> impl Iterator<Item = ValidationError> + 'a {
self.validate_entity_types(p)
.chain(self.validate_action_ids(p))
.chain(self.validate_action_application(
p.principal_constraint(),
p.action_constraint(),
p.resource_constraint(),
))
.map(move |note| ValidationError::with_policy_id(p.id(), None, note))
.chain(self.typecheck_policy(p, mode))
}
fn validate_slots<'a>(
&'a self,
p: &'a Policy,
) -> Option<impl Iterator<Item = ValidationError> + 'a> {
if p.is_static() {
return None;
}
Some(
self.validate_entity_types_in_slots(p.env())
.chain(self.validate_action_application(
&p.principal_constraint(),
p.action_constraint(),
&p.resource_constraint(),
))
.map(move |note| ValidationError::with_policy_id(p.id(), None, note)),
)
}
fn typecheck_policy<'a>(
&'a self,
t: &'a Template,
mode: ValidationMode,
) -> impl Iterator<Item = ValidationError> + 'a {
let typecheck = Typechecker::new(&self.schema, mode);
let mut type_errors = HashSet::new();
typecheck.typecheck_policy(t, &mut type_errors);
type_errors.into_iter().map(|type_error| {
let (kind, location) = type_error.kind_and_location();
ValidationError::with_policy_id(t.id(), location, ValidationErrorKind::type_error(kind))
})
}
}
#[cfg(test)]
mod test {
use std::collections::HashMap;
use super::*;
use cedar_policy_core::{ast, parser};
#[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 = NamespaceDefinition::new(
[
(
foo_type.into(),
EntityType {
member_of_types: vec![],
shape: AttributesOrContext::default(),
},
),
(
bar_type.into(),
EntityType {
member_of_types: vec![],
shape: AttributesOrContext::default(),
},
),
],
[(
action_name.into(),
ActionType {
applies_to: Some(ApplySpec {
resource_types: None,
principal_types: None,
context: AttributesOrContext::default(),
}),
member_of: None,
attributes: 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("pola".to_string()), policy_a_src)
.expect("Test Policy Should Parse");
set.add_static(policy_a.clone())
.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("polb".to_string()), policy_b_src)
.expect("Test Policy Should Parse");
set.add_static(policy_b.clone())
.expect("Policy already present in PolicySet");
let result = validator.validate(&set, ValidationMode::default());
let principal_err = ValidationError::with_policy_id(
policy_b.id(),
None,
ValidationErrorKind::unrecognized_entity_type(
"foo_tye".to_string(),
Some("foo_type".to_string()),
),
);
let resource_err = ValidationError::with_policy_id(
policy_b.id(),
None,
ValidationErrorKind::unrecognized_entity_type(
"br_type".to_string(),
Some("bar_type".to_string()),
),
);
let action_err = ValidationError::with_policy_id(
policy_a.id(),
None,
ValidationErrorKind::unrecognized_action_id(
"Action::\"actin\"".to_string(),
Some("Action::\"action\"".to_string()),
),
);
assert!(!result.validation_passed());
assert!(result.validation_errors().any(|x| x == &principal_err));
assert!(result.validation_errors().any(|x| x == &resource_err));
assert!(result.validation_errors().any(|x| x == &action_err));
Ok(())
}
#[test]
fn top_level_validate_with_instantiations() -> Result<()> {
let mut set = PolicySet::new();
let schema: ValidatorSchema = serde_json::from_str::<SchemaFragment>(
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_template(
Some("template".to_string()),
r#"permit(principal == some_namespace::User::"Alice", action, resource in ?resource);"#,
)
.expect("Parse Error");
set.add_template(t)
.expect("Template already present in PolicySet");
let result = validator.validate(&set, ValidationMode::default());
assert_eq!(
result.into_validation_errors().collect::<Vec<_>>(),
Vec::new()
);
let mut values = HashMap::new();
values.insert(
ast::SlotId::resource(),
ast::EntityUID::from_components(
"some_namespace::Photo".parse().unwrap(),
ast::Eid::new("foo"),
),
);
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"),
),
);
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 id = ast::PolicyID::from_string("link2");
let undefined_err = ValidationError::with_policy_id(
&id,
None,
ValidationErrorKind::unrecognized_entity_type(
"some_namespace::Undefined".to_string(),
Some("some_namespace::User".to_string()),
),
);
let invalid_action_err = ValidationError::with_policy_id(
&id,
None,
ValidationErrorKind::invalid_action_application(false, false),
);
assert!(result.validation_errors().any(|x| x == &undefined_err));
assert!(result.validation_errors().any(|x| x == &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"),
),
);
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 id = ast::PolicyID::from_string("link3");
let invalid_action_err = ValidationError::with_policy_id(
&id,
None,
ValidationErrorKind::invalid_action_application(false, false),
);
assert!(result.validation_errors().any(|x| x == &invalid_action_err));
Ok(())
}
}