use crate::{
ast::{
self, ActionConstraint, Eid, EntityReference, EntityUID, Policy, PolicyID,
PrincipalConstraint, PrincipalOrResourceConstraint, ResourceConstraint, SlotEnv, Template,
},
entities::conformance::is_valid_enumerated_entity,
parser::Loc,
validator::validation_errors::get_suggested_entity_type,
};
use std::{collections::HashSet, sync::Arc};
use crate::validator::{
expr_iterator::{policy_entity_type_names, policy_entity_uids},
validation_errors::unrecognized_action_id_help,
ValidationError,
};
use super::{schema::*, Validator};
impl Validator {
pub fn validate_enum_entity<'a>(
schema: &'a ValidatorSchema,
template: &'a Template,
) -> impl Iterator<Item = ValidationError> + 'a {
policy_entity_uids(template)
.filter(|e| !e.is_action())
.filter_map(|e: &EntityUID| {
if let Some(ValidatorEntityType {
kind: ValidatorEntityTypeKind::Enum(choices),
..
}) = schema.get_entity_type(e.entity_type())
{
match is_valid_enumerated_entity(&Vec::from(choices.clone().map(Eid::new)), e) {
Ok(_) => {}
Err(err) => {
return Some(ValidationError::invalid_enum_entity(
e.loc().cloned(),
template.id().clone(),
err,
));
}
};
}
None
})
}
pub fn validate_entity_types<'a>(
schema: &'a ValidatorSchema,
template: &'a Template,
) -> impl Iterator<Item = ValidationError> + 'a {
policy_entity_type_names(template)
.filter(|ety| !schema.is_known_entity_type(ety))
.map(|ety| {
ValidationError::unrecognized_entity_type(
ety.loc().cloned(),
template.id().clone(),
ety.to_string(),
get_suggested_entity_type(ety, schema),
)
})
}
pub fn validate_action_ids<'a>(
schema: &'a ValidatorSchema,
template: &'a Template,
) -> impl Iterator<Item = ValidationError> + 'a {
policy_entity_uids(template).filter_map(move |euid| {
let entity_type = euid.entity_type();
if entity_type.is_action() && !schema.is_known_action_id(euid) {
Some(ValidationError::unrecognized_action_id(
euid.loc().cloned(),
template.id().clone(),
euid.to_string(),
unrecognized_action_id_help(euid, schema),
))
} else {
None
}
})
}
pub(crate) fn validate_entity_types_in_slots<'a>(
&'a self,
policy_id: &'a PolicyID,
slots: &'a SlotEnv,
) -> impl Iterator<Item = ValidationError> + 'a {
slots
.values()
.filter(|euid| !self.schema.is_known_entity_type(euid.entity_type()))
.map(|euid| {
ValidationError::unrecognized_entity_type(
None,
policy_id.clone(),
euid.entity_type().to_string(),
get_suggested_entity_type(euid.entity_type(), &self.schema),
)
})
}
fn check_if_in_fixes_principal(
&self,
principal_constraint: &PrincipalConstraint,
action_constraint: &ActionConstraint,
) -> bool {
self.check_if_in_fixes(
principal_constraint.as_inner(),
&self
.get_apply_specs_for_action(action_constraint)
.collect::<Vec<_>>(),
&|spec| Box::new(spec.applicable_principal_types()),
)
}
fn check_if_in_fixes_resource(
&self,
resource_constraint: &ResourceConstraint,
action_constraint: &ActionConstraint,
) -> bool {
self.check_if_in_fixes(
resource_constraint.as_inner(),
&self
.get_apply_specs_for_action(action_constraint)
.collect::<Vec<_>>(),
&|spec| Box::new(spec.applicable_resource_types()),
)
}
fn check_if_in_fixes<'a>(
&'a self,
scope_constraint: &PrincipalOrResourceConstraint,
apply_specs: &[&'a ValidatorApplySpec<ast::EntityType>],
select_apply_spec: &impl Fn(
&'a ValidatorApplySpec<ast::EntityType>,
) -> Box<dyn Iterator<Item = &'a ast::EntityType> + 'a>,
) -> bool {
let entity_type = Validator::get_eq_comparison(scope_constraint);
self.check_if_none_equal(apply_specs, entity_type, &select_apply_spec)
&& self.check_if_any_contain(apply_specs, entity_type, &select_apply_spec)
}
fn check_if_none_equal<'a>(
&'a self,
specs: &[&'a ValidatorApplySpec<ast::EntityType>],
lit_opt: Option<&ast::EntityType>,
select_apply_spec: &impl Fn(
&'a ValidatorApplySpec<ast::EntityType>,
) -> Box<dyn Iterator<Item = &'a ast::EntityType> + 'a>,
) -> bool {
if let Some(lit) = lit_opt {
!specs
.iter()
.any(|spec| select_apply_spec(spec).any(|e| e == lit))
} else {
false
}
}
fn check_if_any_contain<'a>(
&'a self,
specs: &[&'a ValidatorApplySpec<ast::EntityType>],
lit_opt: Option<&ast::EntityType>,
select_apply_spec: &impl Fn(
&'a ValidatorApplySpec<ast::EntityType>,
) -> Box<dyn Iterator<Item = &'a ast::EntityType> + 'a>,
) -> bool {
if let Some(etype) = lit_opt.and_then(|typename| self.schema.get_entity_type(typename)) {
specs
.iter()
.any(|spec| select_apply_spec(spec).any(|p| etype.descendants.contains(p)))
} else {
false
}
}
fn get_eq_comparison(
scope_constraint: &PrincipalOrResourceConstraint,
) -> Option<&ast::EntityType> {
match scope_constraint {
PrincipalOrResourceConstraint::Eq(EntityReference::EUID(euid)) => {
Some(euid.entity_type())
}
_ => None,
}
}
pub(crate) fn validate_linked_action_application<'a>(
&self,
p: &'a Policy,
) -> impl Iterator<Item = ValidationError> + 'a {
self.validate_action_application(
p.loc(),
p.id(),
&p.principal_constraint(),
p.action_constraint(),
&p.resource_constraint(),
)
}
pub(crate) fn validate_template_action_application<'a>(
&self,
t: &'a Template,
) -> impl Iterator<Item = ValidationError> + 'a {
self.validate_action_application(
t.loc(),
t.id(),
t.principal_constraint(),
t.action_constraint(),
t.resource_constraint(),
)
}
fn validate_action_application(
&self,
source_loc: Option<&Loc>,
policy_id: &PolicyID,
principal_constraint: &PrincipalConstraint,
action_constraint: &ActionConstraint,
resource_constraint: &ResourceConstraint,
) -> impl Iterator<Item = ValidationError> {
let mut apply_specs = self.get_apply_specs_for_action(action_constraint);
let resources_for_scope: HashSet<&ast::EntityType> = self
.get_resources_satisfying_constraint(resource_constraint)
.collect();
let principals_for_scope: HashSet<&ast::EntityType> = self
.get_principals_satisfying_constraint(principal_constraint)
.collect();
let would_in_fix_principal =
self.check_if_in_fixes_principal(principal_constraint, action_constraint);
let would_in_fix_resource =
self.check_if_in_fixes_resource(resource_constraint, action_constraint);
Some(ValidationError::invalid_action_application(
source_loc.cloned(),
policy_id.clone(),
would_in_fix_principal,
would_in_fix_resource,
))
.filter(|_| {
!apply_specs.any(|spec| {
let action_principals = spec.applicable_principal_types().collect::<HashSet<_>>();
let action_resources = spec.applicable_resource_types().collect::<HashSet<_>>();
let matching_principal = !principals_for_scope.is_disjoint(&action_principals);
let matching_resource = !resources_for_scope.is_disjoint(&action_resources);
matching_principal && matching_resource
})
})
.into_iter()
}
pub(crate) fn get_apply_specs_for_action<'a>(
&'a self,
action_constraint: &'a ActionConstraint,
) -> impl Iterator<Item = &'a ValidatorApplySpec<ast::EntityType>> + 'a {
self.get_actions_satisfying_constraint(action_constraint)
.filter_map(|action_id| self.schema.get_action_id(action_id))
.map(|action| &action.applies_to)
}
fn get_actions_satisfying_constraint<'a>(
&'a self,
action_constraint: &'a ActionConstraint,
) -> Box<dyn Iterator<Item = &'a EntityUID> + 'a> {
match action_constraint {
ActionConstraint::Any => {
Box::new(self.schema.action_ids().map(ValidatorActionId::name))
}
ActionConstraint::Eq(euid) => Box::new(std::iter::once(euid.as_ref())),
ActionConstraint::In(euids) => Box::new(
self.schema
.get_actions_in_set(euids.iter().map(Arc::as_ref))
.unwrap_or_default()
.into_iter(),
),
#[cfg(feature = "tolerant-ast")]
ActionConstraint::ErrorConstraint => {
let v = vec![].into_iter();
Box::new(v)
}
}
}
pub(crate) fn get_principals_satisfying_constraint<'a>(
&'a self,
principal_constraint: &'a PrincipalConstraint,
) -> impl Iterator<Item = &'a ast::EntityType> + 'a {
self.get_entity_types_satisfying_constraint(principal_constraint.as_inner())
}
pub(crate) fn get_resources_satisfying_constraint<'a>(
&'a self,
resource_constraint: &'a ResourceConstraint,
) -> impl Iterator<Item = &'a ast::EntityType> + 'a {
self.get_entity_types_satisfying_constraint(resource_constraint.as_inner())
}
fn get_entity_types_satisfying_constraint<'a>(
&'a self,
scope_constraint: &'a PrincipalOrResourceConstraint,
) -> Box<dyn Iterator<Item = &'a ast::EntityType> + 'a> {
match scope_constraint {
PrincipalOrResourceConstraint::Any => Box::new(self.schema.entity_type_names()),
PrincipalOrResourceConstraint::Eq(EntityReference::EUID(euid)) => {
Box::new(std::iter::once(euid.entity_type()))
}
PrincipalOrResourceConstraint::In(EntityReference::EUID(euid)) => Box::new(
self.schema
.get_entity_types_in(euid.entity_type())
.into_iter(),
),
PrincipalOrResourceConstraint::Eq(EntityReference::Slot(_))
| PrincipalOrResourceConstraint::In(EntityReference::Slot(_)) => {
Box::new(self.schema.entity_type_names())
}
PrincipalOrResourceConstraint::Is(entity_type)
| PrincipalOrResourceConstraint::IsIn(entity_type, EntityReference::Slot(_)) => {
Box::new(
if self.schema.is_known_entity_type(entity_type) {
Some(entity_type.as_ref())
} else {
None
}
.into_iter(),
)
}
PrincipalOrResourceConstraint::IsIn(entity_type, EntityReference::EUID(in_entity)) => {
Box::new(
self.schema
.get_entity_types_in(in_entity.entity_type())
.into_iter()
.filter(move |k| &entity_type.as_ref() == k),
)
}
}
}
}
#[cfg(test)]
#[expect(clippy::panic, clippy::indexing_slicing, reason = "unit tests")]
mod test {
use std::collections::{HashMap, HashSet};
use crate::{
ast::{Effect, Eid, EntityUID, PolicyID, PrincipalConstraint, ResourceConstraint},
est::Annotations,
parser::{parse_policy, parse_policy_or_template},
test_utils::{expect_err, ExpectedErrorMessageBuilder},
};
use miette::Report;
use super::*;
use crate::validator::{
json_schema, validation_errors::UnrecognizedEntityType, RawName, ValidationMode,
ValidationWarning, Validator,
};
#[test]
fn validate_entity_type_empty_schema() {
let src = r#"permit(principal, action, resource == foo_type::"foo_name");"#;
let policy = parse_policy_or_template(None, src).unwrap();
let validate = Validator::new(ValidatorSchema::empty());
let notes: Vec<ValidationError> =
Validator::validate_entity_types(validate.schema(), &policy).collect();
expect_err(
src,
&Report::new(notes.first().unwrap().clone()),
&ExpectedErrorMessageBuilder::error(
"for policy `policy0`, unrecognized entity type `foo_type`",
)
.exactly_one_underline("foo_type")
.build(),
);
assert_eq!(notes.len(), 1, "{notes:?}");
}
#[test]
fn validate_equals_instead_of_in() {
let schema_file: json_schema::NamespaceDefinition<RawName> =
serde_json::from_value(serde_json::json!(
{
"entityTypes": {
"user": {
"memberOfTypes": ["admins"]
},
"admins": {},
"widget": {
"memberOfTypes": ["bin"]
},
"bin": {}
},
"actions": {
"act": {
"appliesTo": {
"principalTypes": ["user"],
"resourceTypes": ["widget"]
}
}
}
}
))
.unwrap();
let schema = schema_file.try_into().unwrap();
let src = r#"permit(principal == admins::"admin1", action == Action::"act", resource == bin::"bin");"#;
let p = parse_policy_or_template(None, src).unwrap();
let validate = Validator::new(schema);
let notes: Vec<ValidationError> =
validate.validate_template_action_application(&p).collect();
expect_err(
src,
&Report::new(notes.first().unwrap().clone()),
&ExpectedErrorMessageBuilder::error(
r#"for policy `policy0`, unable to find an applicable action given the policy scope constraints"#,
)
.help("try replacing `==` with `in` in the principal clause and the resource clause")
.exactly_one_underline(src)
.build(),
);
assert_eq!(notes.len(), 1, "{notes:?}");
}
#[test]
fn validate_entity_type_in_singleton_schema() {
let foo_type = "foo_type";
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(),
)],
[],
);
let singleton_schema = schema_file.try_into().unwrap();
let policy = Template::new(
PolicyID::from_string("policy0"),
None,
ast::Annotations::new(),
Effect::Permit,
PrincipalConstraint::any(),
ActionConstraint::any(),
ResourceConstraint::is_eq(Arc::new(
EntityUID::with_eid_and_type(foo_type, "foo_name")
.expect("should be a valid identifier"),
)),
None,
);
let validate = Validator::new(singleton_schema);
assert!(
Validator::validate_entity_types(validate.schema(), &policy)
.next()
.is_none(),
"Did not expect any validation errors."
);
}
#[test]
fn validate_entity_type_not_in_singleton_schema() {
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(),
)],
[],
);
let singleton_schema = schema_file.try_into().unwrap();
let src = r#"permit(principal, action, resource == bar_type::"bar_name");"#;
let policy = parse_policy_or_template(None, src).unwrap();
let validate = Validator::new(singleton_schema);
let notes: Vec<ValidationError> =
Validator::validate_entity_types(validate.schema(), &policy).collect();
expect_err(
src,
&Report::new(notes.first().unwrap().clone()),
&ExpectedErrorMessageBuilder::error(
"for policy `policy0`, unrecognized entity type `bar_type`",
)
.exactly_one_underline("bar_type")
.help("did you mean `foo_type`?")
.build(),
);
assert_eq!(notes.len(), 1, "{notes:?}");
}
#[test]
fn validate_action_id_empty_schema() {
let src = r#"permit(principal, action == Action::"foo_name", resource);"#;
let policy = parse_policy_or_template(None, src).unwrap();
let validate = Validator::new(ValidatorSchema::empty());
let notes: Vec<ValidationError> =
Validator::validate_action_ids(validate.schema(), &policy).collect();
expect_err(
src,
&Report::new(notes.first().unwrap().clone()),
&ExpectedErrorMessageBuilder::error(
r#"for policy `policy0`, unrecognized action `Action::"foo_name"`"#,
)
.exactly_one_underline(r#"Action::"foo_name""#)
.build(),
);
assert_eq!(notes.len(), 1, "{notes:?}");
}
#[test]
fn validate_action_id_in_singleton_schema() {
let foo_name = "foo_name";
let schema_file = json_schema::NamespaceDefinition::new(
[],
[(
foo_name.into(),
json_schema::ActionType {
applies_to: None,
member_of: None,
attributes: None,
annotations: Annotations::new(),
loc: None,
#[cfg(feature = "extended-schema")]
defn_loc: None,
},
)],
);
let singleton_schema = schema_file.try_into().unwrap();
let entity =
EntityUID::with_eid_and_type("Action", foo_name).expect("should be a valid identifier");
let policy = Template::new(
PolicyID::from_string("policy0"),
None,
ast::Annotations::new(),
Effect::Permit,
PrincipalConstraint::any(),
ActionConstraint::is_eq(entity),
ResourceConstraint::any(),
None,
);
let validate = Validator::new(singleton_schema);
assert!(
Validator::validate_action_ids(validate.schema(), &policy)
.next()
.is_none(),
"Did not expect any validation errors."
);
}
#[test]
fn validate_principal_slot_in_singleton_schema() {
let p_name = "User";
let schema_file = json_schema::NamespaceDefinition::new(
[(
p_name.parse().unwrap(),
json_schema::StandardEntityType {
member_of_types: vec![],
shape: json_schema::AttributesOrContext::default(),
tags: None,
}
.into(),
)],
[],
);
let schema = schema_file.try_into().unwrap();
let principal_constraint = PrincipalConstraint::is_eq_slot();
let validator = Validator::new(schema);
let entities = validator
.get_principals_satisfying_constraint(&principal_constraint)
.collect::<Vec<_>>();
assert_eq!(entities.len(), 1);
let name = entities[0];
assert_eq!(name, &p_name.parse().expect("Expected valid entity type."));
}
#[test]
fn validate_resource_slot_in_singleton_schema() {
let p_name = "Package";
let schema_file = json_schema::NamespaceDefinition::new(
[(
p_name.parse().unwrap(),
json_schema::StandardEntityType {
member_of_types: vec![],
shape: json_schema::AttributesOrContext::default(),
tags: None,
}
.into(),
)],
[],
);
let schema = schema_file.try_into().unwrap();
let principal_constraint = PrincipalConstraint::any();
let validator = Validator::new(schema);
let entities = validator
.get_principals_satisfying_constraint(&principal_constraint)
.collect::<Vec<_>>();
assert_eq!(entities.len(), 1);
let name = entities[0];
assert_eq!(name, &p_name.parse().expect("Expected valid entity type."));
}
#[test]
fn undefined_entity_type_in_principal_slot() {
let p_name = "User";
let schema_file = json_schema::NamespaceDefinition::new(
[(
p_name.parse().unwrap(),
json_schema::StandardEntityType {
member_of_types: vec![],
shape: json_schema::AttributesOrContext::default(),
tags: None,
}
.into(),
)],
[],
);
let schema = schema_file.try_into().expect("Invalid schema");
let undefined_euid: EntityUID = "Undefined::\"foo\""
.parse()
.expect("Expected entity UID to parse.");
let env = HashMap::from([(ast::SlotId::principal(), undefined_euid)]);
let validator = Validator::new(schema);
let notes: Vec<ValidationError> = validator
.validate_entity_types_in_slots(&PolicyID::from_string("0"), &env)
.collect();
assert_eq!(1, notes.len());
match notes.first() {
Some(ValidationError::UnrecognizedEntityType(UnrecognizedEntityType {
actual_entity_type,
suggested_entity_type,
..
})) => {
assert_eq!("Undefined", actual_entity_type);
assert_eq!(
"User",
suggested_entity_type
.as_ref()
.expect("Expected a suggested entity type")
);
}
_ => panic!("Unexpected variant of ValidationErrorKind."),
};
}
#[test]
fn validate_action_id_not_in_singleton_schema() {
let schema_file = json_schema::NamespaceDefinition::new(
[],
[(
"foo_name".into(),
json_schema::ActionType {
applies_to: None,
member_of: None,
attributes: None,
annotations: Annotations::new(),
loc: None,
#[cfg(feature = "extended-schema")]
defn_loc: None,
},
)],
);
let singleton_schema = schema_file.try_into().unwrap();
let src = r#"permit(principal, action == Action::"bar_name", resource);"#;
let policy = parse_policy_or_template(None, src).unwrap();
let validate = Validator::new(singleton_schema);
let notes: Vec<ValidationError> =
Validator::validate_action_ids(validate.schema(), &policy).collect();
expect_err(
src,
&Report::new(notes.first().unwrap().clone()),
&ExpectedErrorMessageBuilder::error(
r#"for policy `policy0`, unrecognized action `Action::"bar_name"`"#,
)
.exactly_one_underline(r#"Action::"bar_name""#)
.help(r#"did you mean `Action::"foo_name"`?"#)
.build(),
);
assert_eq!(notes.len(), 1, "{notes:?}");
}
#[test]
fn validate_action_id_with_action_type() {
let schema_file = json_schema::NamespaceDefinition::new(
[],
[(
"Action::view".into(),
json_schema::ActionType {
applies_to: None,
member_of: None,
attributes: None,
annotations: Annotations::new(),
loc: None,
#[cfg(feature = "extended-schema")]
defn_loc: None,
},
)],
);
let singleton_schema = schema_file.try_into().unwrap();
let src = r#"permit(principal, action == Action::"view", resource);"#;
let policy = parse_policy_or_template(None, src).unwrap();
let validate = Validator::new(singleton_schema);
let notes: Vec<ValidationError> =
Validator::validate_action_ids(validate.schema(), &policy).collect();
expect_err(
src,
&Report::new(notes.first().unwrap().clone()),
&ExpectedErrorMessageBuilder::error(
r#"for policy `policy0`, unrecognized action `Action::"view"`"#,
)
.exactly_one_underline(r#"Action::"view""#)
.help(r#"did you intend to include the type in action `Action::"Action::view"`?"#)
.build(),
);
assert_eq!(notes.len(), 1, "{notes:?}");
}
#[test]
fn validate_action_id_with_action_type_namespace() {
let schema_src = r#"
{
"foo::foo::bar::baz": {
"entityTypes": {},
"actions": {
"Action::view": {}
}
}
}"#;
let schema_fragment: json_schema::Fragment<RawName> =
serde_json::from_str(schema_src).expect("Parse Error");
let schema = schema_fragment.try_into().unwrap();
let src = r#"permit(principal, action == Action::"view", resource);"#;
let policy = parse_policy_or_template(None, src).unwrap();
let validate = Validator::new(schema);
let notes: Vec<ValidationError> =
Validator::validate_action_ids(validate.schema(), &policy).collect();
expect_err(
src,
&Report::new(notes.first().unwrap().clone()),
&ExpectedErrorMessageBuilder::error(
r#"for policy `policy0`, unrecognized action `Action::"view"`"#,
)
.exactly_one_underline(r#"Action::"view""#)
.help(r#"did you intend to include the type in action `foo::foo::bar::baz::Action::"Action::view"`?"#)
.build(),
);
assert_eq!(notes.len(), 1, "{notes:?}");
}
#[test]
fn validate_namespaced_action_id_in_schema() {
let descriptors = json_schema::Fragment::from_json_str(
r#"
{
"NS": {
"entityTypes": {},
"actions": { "foo_name": {} }
}
}"#,
)
.expect("Expected schema parse.");
let schema = descriptors.try_into().unwrap();
let entity: EntityUID = "NS::Action::\"foo_name\""
.parse()
.expect("Expected entity parse.");
let policy = Template::new(
PolicyID::from_string("policy0"),
None,
ast::Annotations::new(),
Effect::Permit,
PrincipalConstraint::any(),
ActionConstraint::is_eq(entity),
ResourceConstraint::any(),
None,
);
let validate = Validator::new(schema);
let notes: Vec<ValidationError> =
Validator::validate_action_ids(validate.schema(), &policy).collect();
assert_eq!(notes, vec![], "Did not expect any invalid action.");
}
#[test]
fn validate_namespaced_invalid_action() {
let descriptors = json_schema::Fragment::from_json_str(
r#"
{
"NS": {
"entityTypes": {},
"actions": { "foo_name": {} }
}
}"#,
)
.expect("Expected schema parse.");
let schema = descriptors.try_into().unwrap();
let src = r#"permit(principal, action == Bogus::Action::"foo_name", resource);"#;
let policy = parse_policy_or_template(None, src).unwrap();
let validate = Validator::new(schema);
let notes: Vec<ValidationError> =
Validator::validate_action_ids(validate.schema(), &policy).collect();
expect_err(
src,
&Report::new(notes.first().unwrap().clone()),
&ExpectedErrorMessageBuilder::error(
r#"for policy `policy0`, unrecognized action `Bogus::Action::"foo_name"`"#,
)
.exactly_one_underline(r#"Bogus::Action::"foo_name""#)
.help(r#"did you mean `NS::Action::"foo_name"`?"#)
.build(),
);
assert_eq!(notes.len(), 1, "{notes:?}");
}
#[test]
fn validate_namespaced_entity_type_in_schema() {
let descriptors = json_schema::Fragment::from_json_str(
r#"
{
"NS": {
"entityTypes": {"Foo": {} },
"actions": {}
}
}"#,
)
.expect("Expected schema parse.");
let schema = descriptors.try_into().unwrap();
let entity_type: ast::EntityType = "NS::Foo".parse().expect("Expected entity type parse.");
let policy = Template::new(
PolicyID::from_string("policy0"),
None,
ast::Annotations::new(),
Effect::Permit,
PrincipalConstraint::is_eq(Arc::new(EntityUID::from_components(
entity_type,
Eid::new("bar"),
None,
))),
ActionConstraint::any(),
ResourceConstraint::any(),
None,
);
let validate = Validator::new(schema);
let notes: Vec<ValidationError> =
Validator::validate_entity_types(validate.schema(), &policy).collect();
assert_eq!(notes, vec![], "Did not expect any invalid action.");
}
#[test]
fn validate_namespaced_invalid_entity_type() {
let descriptors = json_schema::Fragment::from_json_str(
r#"
{
"NS": {
"entityTypes": {"Foo": {} },
"actions": {}
}
}"#,
)
.expect("Expected schema parse.");
let schema = descriptors.try_into().unwrap();
let src = r#"permit(principal == Bogus::Foo::"bar", action, resource);"#;
let policy = parse_policy_or_template(None, src).unwrap();
let validate = Validator::new(schema);
let notes: Vec<ValidationError> =
Validator::validate_entity_types(validate.schema(), &policy).collect();
expect_err(
src,
&Report::new(notes.first().unwrap().clone()),
&ExpectedErrorMessageBuilder::error(
"for policy `policy0`, unrecognized entity type `Bogus::Foo`",
)
.exactly_one_underline("Bogus::Foo")
.help("did you mean `NS::Foo`?")
.build(),
);
assert_eq!(notes.len(), 1, "{notes:?}");
}
#[test]
fn get_possible_actions_eq() {
let foo_name = "foo_name";
let euid_foo =
EntityUID::with_eid_and_type("Action", foo_name).expect("should be a valid identifier");
let action_constraint = ActionConstraint::is_eq(euid_foo.clone());
let schema_file = json_schema::NamespaceDefinition::new(
[],
[(
foo_name.into(),
json_schema::ActionType {
applies_to: None,
member_of: None,
attributes: None,
annotations: Annotations::new(),
loc: None,
#[cfg(feature = "extended-schema")]
defn_loc: None,
},
)],
);
let singleton_schema = schema_file.try_into().unwrap();
let validate = Validator::new(singleton_schema);
let actions = validate
.get_actions_satisfying_constraint(&action_constraint)
.collect();
assert_eq!(HashSet::from([&euid_foo]), actions);
}
#[test]
fn get_possible_actions_in_no_parents() {
let foo_name = "foo_name";
let euid_foo =
EntityUID::with_eid_and_type("Action", foo_name).expect("should be a valid identifier");
let action_constraint = ActionConstraint::is_in(vec![euid_foo.clone()]);
let schema_file = json_schema::NamespaceDefinition::new(
[],
[(
foo_name.into(),
json_schema::ActionType {
applies_to: None,
member_of: None,
attributes: None,
annotations: Annotations::new(),
loc: None,
#[cfg(feature = "extended-schema")]
defn_loc: None,
},
)],
);
let singleton_schema = schema_file.try_into().unwrap();
let validate = Validator::new(singleton_schema);
let actions = validate
.get_actions_satisfying_constraint(&action_constraint)
.collect();
assert_eq!(HashSet::from([&euid_foo]), actions);
}
#[test]
fn get_possible_actions_in_set_no_parents() {
let foo_name = "foo_name";
let euid_foo =
EntityUID::with_eid_and_type("Action", foo_name).expect("should be a valid identifier");
let action_constraint = ActionConstraint::is_in(vec![euid_foo.clone()]);
let schema_file = json_schema::NamespaceDefinition::new(
[],
[(
foo_name.into(),
json_schema::ActionType {
applies_to: None,
member_of: None,
attributes: None,
annotations: Annotations::new(),
loc: None,
#[cfg(feature = "extended-schema")]
defn_loc: None,
},
)],
);
let singleton_schema = schema_file.try_into().unwrap();
let validate = Validator::new(singleton_schema);
let actions = validate
.get_actions_satisfying_constraint(&action_constraint)
.collect();
assert_eq!(HashSet::from([&euid_foo]), actions);
}
#[test]
fn get_possible_principals_eq() {
let foo_type = "foo_type";
let euid_foo = EntityUID::with_eid_and_type(foo_type, "foo_name")
.expect("should be a valid identifier");
let principal_constraint = PrincipalConstraint::is_eq(Arc::new(euid_foo.clone()));
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(),
)],
[],
);
let singleton_schema = schema_file.try_into().unwrap();
let validate = Validator::new(singleton_schema);
let principals = validate
.get_principals_satisfying_constraint(&principal_constraint)
.cloned()
.collect::<HashSet<_>>();
assert_eq!(HashSet::from([euid_foo.components().0]), principals);
}
fn schema_with_single_principal_action_resource(
) -> (EntityUID, EntityUID, EntityUID, ValidatorSchema) {
let action_name = "foo";
let action_euid = EntityUID::with_eid_and_type("Action", action_name)
.expect("should be a valid identifier");
let principal_type = "bar";
let principal_euid = EntityUID::with_eid_and_type(principal_type, "principal")
.expect("should be a valid identifier");
let resource_type = "baz";
let resource_euid = EntityUID::with_eid_and_type(resource_type, "resource")
.expect("should be a valid identifier");
let schema = json_schema::NamespaceDefinition::new(
[
(
principal_type.parse().unwrap(),
json_schema::StandardEntityType {
member_of_types: vec![],
shape: json_schema::AttributesOrContext::default(),
tags: None,
}
.into(),
),
(
resource_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 {
resource_types: vec![resource_type.parse().unwrap()],
principal_types: vec![principal_type.parse().unwrap()],
context: json_schema::AttributesOrContext::default(),
}),
member_of: Some(vec![]),
attributes: None,
annotations: Annotations::new(),
loc: None,
#[cfg(feature = "extended-schema")]
defn_loc: None,
},
)],
)
.try_into()
.expect("Expected valid schema file.");
(principal_euid, action_euid, resource_euid, schema)
}
#[track_caller] fn assert_validate_policy_succeeds(validator: &Validator, policy: &Template) {
assert!(
validator
.validate_policy(policy, ValidationMode::default())
.0
.next()
.is_none(),
"Did not expect any validation errors."
);
assert!(
validator
.validate_policy(policy, ValidationMode::default())
.1
.next()
.is_none(),
"Did not expect any validation warnings."
);
}
#[track_caller] fn assert_validate_policy_fails(
validator: &Validator,
policy: &Template,
expected: &[ValidationError],
) {
assert_eq!(
validator
.validate_policy(policy, ValidationMode::default())
.0
.collect::<Vec<ValidationError>>(),
expected,
"Unexpected validation errors."
);
}
#[track_caller] fn assert_validate_policy_flags_impossible_policy(validator: &Validator, policy: &Template) {
assert_eq!(
validator
.validate_policy(policy, ValidationMode::default())
.1
.collect::<Vec<ValidationWarning>>(),
vec![ValidationWarning::impossible_policy(
policy.loc().cloned(),
policy.id().clone()
)],
"Unexpected validation warnings."
);
}
#[test]
fn validate_action_apply_correct() {
let (principal, action, resource, schema) = schema_with_single_principal_action_resource();
let policy = Template::new(
PolicyID::from_string("policy0"),
None,
ast::Annotations::new(),
Effect::Permit,
PrincipalConstraint::is_eq(Arc::new(principal)),
ActionConstraint::is_eq(action),
ResourceConstraint::is_eq(Arc::new(resource)),
None,
);
let validator = Validator::new(schema);
assert_validate_policy_succeeds(&validator, &policy);
}
#[test]
fn validate_action_apply_incorrect_principal() {
let (_, _, _, schema) = schema_with_single_principal_action_resource();
let src =
r#"permit(principal == baz::"p", action == Action::"foo", resource == baz::"r");"#;
let p = parse_policy_or_template(None, src).unwrap();
let validate = Validator::new(schema);
let notes: Vec<ValidationError> =
validate.validate_template_action_application(&p).collect();
expect_err(
src,
&Report::new(notes.first().unwrap().clone()),
&ExpectedErrorMessageBuilder::error(
r#"for policy `policy0`, unable to find an applicable action given the policy scope constraints"#,
)
.exactly_one_underline(src)
.build(),
);
assert_eq!(notes.len(), 1, "{notes:?}");
}
#[test]
fn validate_action_apply_incorrect_resource() {
let (_, _, _, schema) = schema_with_single_principal_action_resource();
let src =
r#"permit(principal == bar::"p", action == Action::"foo", resource == bar::"r");"#;
let p = parse_policy_or_template(None, src).unwrap();
let validate = Validator::new(schema);
let notes: Vec<ValidationError> =
validate.validate_template_action_application(&p).collect();
expect_err(
src,
&Report::new(notes.first().unwrap().clone()),
&ExpectedErrorMessageBuilder::error(
r#"for policy `policy0`, unable to find an applicable action given the policy scope constraints"#,
)
.exactly_one_underline(src)
.build(),
);
assert_eq!(notes.len(), 1, "{notes:?}");
}
#[test]
fn validate_action_apply_incorrect_principal_and_resource() {
let (_, _, _, schema) = schema_with_single_principal_action_resource();
let src =
r#"permit(principal == baz::"p", action == Action::"foo", resource == bar::"r");"#;
let p = parse_policy_or_template(None, src).unwrap();
let validate = Validator::new(schema);
let notes: Vec<ValidationError> =
validate.validate_template_action_application(&p).collect();
expect_err(
src,
&Report::new(notes.first().unwrap().clone()),
&ExpectedErrorMessageBuilder::error(
r#"for policy `policy0`, unable to find an applicable action given the policy scope constraints"#,
)
.exactly_one_underline(src)
.build(),
);
assert_eq!(notes.len(), 1, "{notes:?}");
}
#[test]
fn validate_principal_is() {
let (_, _, _, schema) = schema_with_single_principal_action_resource();
let policy =
parse_policy_or_template(None, "permit(principal is bar, action, resource);").unwrap();
let validator = Validator::new(schema);
assert_validate_policy_succeeds(&validator, &policy);
let policy = parse_policy_or_template(
None,
r#"permit(principal is bar in bar::"baz", action, resource);"#,
)
.unwrap();
assert_validate_policy_succeeds(&validator, &policy);
}
#[test]
fn validate_principal_is_err() {
let (_, _, _, schema) = schema_with_single_principal_action_resource();
let src = "permit(principal is baz, action, resource);";
let policy = parse_policy_or_template(None, src).unwrap();
let validator = Validator::new(schema);
assert_validate_policy_fails(
&validator,
&policy,
&[ValidationError::invalid_action_application(
Some(Loc::new(0..43, Arc::from(src))),
PolicyID::from_string("policy0"),
false,
false,
)],
);
assert_validate_policy_flags_impossible_policy(&validator, &policy);
let src = r#"permit(principal is biz in faz::"a", action, resource);"#;
let policy = parse_policy_or_template(None, src).unwrap();
assert_validate_policy_fails(
&validator,
&policy,
&[
ValidationError::unrecognized_entity_type(
Some(Loc::new(27..30, Arc::from(src))),
PolicyID::from_string("policy0"),
"faz".into(),
Some("baz".into()),
),
ValidationError::unrecognized_entity_type(
Some(Loc::new(20..23, Arc::from(src))),
PolicyID::from_string("policy0"),
"biz".into(),
Some("baz".into()),
),
ValidationError::invalid_action_application(
Some(Loc::new(0..55, Arc::from(src))),
PolicyID::from_string("policy0"),
false,
false,
),
],
);
assert_validate_policy_flags_impossible_policy(&validator, &policy);
let src = r#"permit(principal is bar in baz::"buz", action, resource);"#;
let policy = parse_policy_or_template(None, src).unwrap();
assert_validate_policy_fails(
&validator,
&policy,
&[ValidationError::invalid_action_application(
Some(Loc::new(0..57, Arc::from(src))),
PolicyID::from_string("policy0"),
false,
false,
)],
);
assert_validate_policy_flags_impossible_policy(&validator, &policy);
}
#[test]
fn validate_resource_is() {
let (_, _, _, schema) = schema_with_single_principal_action_resource();
let policy =
parse_policy_or_template(None, "permit(principal, action, resource is baz);").unwrap();
let validator = Validator::new(schema);
assert_validate_policy_succeeds(&validator, &policy);
let policy = parse_policy_or_template(
None,
r#"permit(principal, action, resource is baz in baz::"bar");"#,
)
.unwrap();
assert_validate_policy_succeeds(&validator, &policy);
}
#[test]
fn validate_resource_is_err() {
let (_, _, _, schema) = schema_with_single_principal_action_resource();
let src = "permit(principal, action, resource is bar);";
let policy = parse_policy_or_template(None, src).unwrap();
let validator = Validator::new(schema);
assert_validate_policy_fails(
&validator,
&policy,
&[ValidationError::invalid_action_application(
Some(Loc::new(0..43, Arc::from(src))),
PolicyID::from_string("policy0"),
false,
false,
)],
);
assert_validate_policy_flags_impossible_policy(&validator, &policy);
let src = r#"permit(principal, action, resource is baz in bar::"buz");"#;
let policy = parse_policy_or_template(None, src).unwrap();
assert_validate_policy_fails(
&validator,
&policy,
&[ValidationError::invalid_action_application(
Some(Loc::new(0..57, Arc::from(src))),
PolicyID::from_string("policy0"),
false,
false,
)],
);
assert_validate_policy_flags_impossible_policy(&validator, &policy);
let src = r#"permit(principal, action, resource is biz in faz::"a");"#;
let policy = parse_policy_or_template(None, src).unwrap();
assert_validate_policy_fails(
&validator,
&policy,
&[
ValidationError::unrecognized_entity_type(
Some(Loc::new(45..48, Arc::from(src))),
PolicyID::from_string("policy0"),
"faz".into(),
Some("baz".into()),
),
ValidationError::unrecognized_entity_type(
Some(Loc::new(38..41, Arc::from(src))),
PolicyID::from_string("policy0"),
"biz".into(),
Some("baz".into()),
),
ValidationError::invalid_action_application(
Some(Loc::new(0..55, Arc::from(src))),
PolicyID::from_string("policy0"),
false,
false,
),
],
);
assert_validate_policy_flags_impossible_policy(&validator, &policy);
}
#[test]
fn is_unknown_entity_condition() {
let (_, _, _, schema) = schema_with_single_principal_action_resource();
let src = r#"permit(principal, action, resource) when { resource is biz };"#;
let policy = parse_policy_or_template(None, src).unwrap();
let validator = Validator::new(schema);
let err = validator
.validate_policy(&policy, ValidationMode::default())
.0
.next()
.unwrap();
expect_err(
src,
&Report::new(err),
&ExpectedErrorMessageBuilder::error(
"for policy `policy0`, unrecognized entity type `biz`",
)
.exactly_one_underline("biz")
.help("did you mean `baz`?")
.build(),
);
assert_validate_policy_flags_impossible_policy(&validator, &policy);
}
#[test]
fn test_with_tc_computation() {
let action_name = "foo";
let action_parent_name = "foo_parent";
let action_grandparent_name = "foo_grandparent";
let action_grandparent_euid =
EntityUID::with_eid_and_type("Action", action_grandparent_name)
.expect("should be a valid identifier");
let principal_type = "bar";
let resource_type = "baz";
let resource_parent_type = "baz_parent";
let resource_grandparent_type = "baz_grandparent";
let resource_grandparent_euid =
EntityUID::with_eid_and_type(resource_parent_type, "resource")
.expect("should be a valid identifier");
let schema_file = json_schema::NamespaceDefinition::new(
[
(
principal_type.parse().unwrap(),
json_schema::StandardEntityType {
member_of_types: vec![],
shape: json_schema::AttributesOrContext::default(),
tags: None,
}
.into(),
),
(
resource_type.parse().unwrap(),
json_schema::StandardEntityType {
member_of_types: vec![resource_parent_type.parse().unwrap()],
shape: json_schema::AttributesOrContext::default(),
tags: None,
}
.into(),
),
(
resource_parent_type.parse().unwrap(),
json_schema::StandardEntityType {
member_of_types: vec![resource_grandparent_type.parse().unwrap()],
shape: json_schema::AttributesOrContext::default(),
tags: None,
}
.into(),
),
(
resource_grandparent_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 {
resource_types: vec![resource_type.parse().unwrap()],
principal_types: vec![principal_type.parse().unwrap()],
context: json_schema::AttributesOrContext::default(),
}),
member_of: Some(vec![json_schema::ActionEntityUID::new(
None,
action_parent_name.into(),
)]),
attributes: None,
annotations: Annotations::new(),
loc: None,
#[cfg(feature = "extended-schema")]
defn_loc: None,
},
),
(
action_parent_name.into(),
json_schema::ActionType {
applies_to: None,
member_of: Some(vec![json_schema::ActionEntityUID::new(
None,
action_grandparent_name.into(),
)]),
attributes: None,
annotations: Annotations::new(),
loc: None,
#[cfg(feature = "extended-schema")]
defn_loc: None,
},
),
(
action_grandparent_name.into(),
json_schema::ActionType {
applies_to: None,
member_of: Some(vec![]),
attributes: None,
annotations: Annotations::new(),
loc: None,
#[cfg(feature = "extended-schema")]
defn_loc: None,
},
),
],
);
let schema = schema_file.try_into().unwrap();
let policy = Template::new(
PolicyID::from_string("policy0"),
None,
ast::Annotations::new(),
Effect::Permit,
PrincipalConstraint::any(),
ActionConstraint::is_in([action_grandparent_euid]),
ResourceConstraint::is_in(Arc::new(resource_grandparent_euid)),
None,
);
let validator = Validator::new(schema);
assert_validate_policy_succeeds(&validator, &policy);
}
#[test]
fn unspecified_principal_resource_with_scope_conditions() {
let schema = serde_json::from_str::<json_schema::NamespaceDefinition<RawName>>(
r#"
{
"entityTypes": {"a": {}},
"actions": {
"": { }
}
}
"#,
)
.unwrap()
.try_into()
.unwrap();
let policy = parse_policy(
Some(PolicyID::from_string("0")),
r#"permit(principal == a::"p", action, resource == a::"r");"#,
)
.unwrap();
let validator = Validator::new(schema);
let (template, _) = Template::link_static_policy(policy);
assert_validate_policy_flags_impossible_policy(&validator, &template);
}
}
#[cfg(test)]
#[cfg(feature = "partial-validate")]
mod partial_schema {
use crate::{
ast::{PolicyID, StaticPolicy, Template},
parser::parse_policy,
};
use crate::validator::{json_schema, RawName, Validator};
#[track_caller] fn assert_validates_with_empty_schema(policy: StaticPolicy) {
let schema: json_schema::NamespaceDefinition<RawName> = serde_json::from_str(
r#"
{
"entityTypes": { },
"actions": {}
}
"#,
)
.unwrap();
let schema = schema.try_into().unwrap();
let (template, _) = Template::link_static_policy(policy);
let validate = Validator::new(schema);
let errs = validate
.validate_policy(&template, crate::validator::ValidationMode::Partial)
.0
.collect::<Vec<_>>();
assert_eq!(errs, vec![], "Did not expect any validation errors.");
}
#[test]
fn undeclared_entity_type_partial_schema() {
let policy = parse_policy(
Some(PolicyID::from_string("0")),
r#"permit(principal == User::"alice", action, resource);"#,
)
.unwrap();
assert_validates_with_empty_schema(policy);
let policy = parse_policy(
Some(PolicyID::from_string("0")),
r#"permit(principal, action == Action::"view", resource);"#,
)
.unwrap();
assert_validates_with_empty_schema(policy);
let policy = parse_policy(
Some(PolicyID::from_string("0")),
r#"permit(principal, action, resource == Photo::"party.jpg");"#,
)
.unwrap();
assert_validates_with_empty_schema(policy);
}
}