Skip to main content

cedar_policy_core/validator/
rbac.rs

1/*
2 * Copyright Cedar Contributors
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17//! Contains the validation logic specific to RBAC policy validation.
18
19use crate::{
20    ast::{
21        self, ActionConstraint, EntityReference, EntityUID, Policy, PolicyID, PrincipalConstraint,
22        PrincipalOrResourceConstraint, ResourceConstraint, SlotEnv, Template,
23    },
24    entities::conformance::is_valid_enumerated_entity,
25    parser::Loc,
26    validator::validation_errors::get_suggested_entity_type,
27};
28
29use std::{collections::HashSet, sync::Arc};
30
31use crate::validator::{
32    expr_iterator::{policy_entity_type_names, policy_entity_uids},
33    validation_errors::unrecognized_action_id_help,
34    ValidationError,
35};
36
37use super::{schema::*, Validator};
38
39impl Validator {
40    /// Validate if a [`Template`] contains entities of enumerated entity types
41    /// but with invalid UIDs
42    pub fn validate_enum_entity<'a>(
43        schema: &'a ValidatorSchema,
44        template: &'a Template,
45    ) -> impl Iterator<Item = ValidationError> + 'a {
46        policy_entity_uids(template)
47            .filter(|e| !e.is_action())
48            .filter_map(|e: &EntityUID| {
49                if let Some(ValidatorEntityType {
50                    kind: ValidatorEntityTypeKind::Enum(choices),
51                    ..
52                }) = schema.get_entity_type(e.entity_type())
53                {
54                    match is_valid_enumerated_entity(choices, e) {
55                        Ok(_) => {}
56                        Err(err) => {
57                            return Some(ValidationError::invalid_enum_entity(
58                                e.loc().cloned(),
59                                template.id().clone(),
60                                err,
61                            ));
62                        }
63                    };
64                }
65                None
66            })
67    }
68    /// Generate `UnrecognizedEntityType` error for every entity type in the
69    /// expression that could not also be found in the schema.
70    pub fn validate_entity_types<'a>(
71        schema: &'a ValidatorSchema,
72        template: &'a Template,
73    ) -> impl Iterator<Item = ValidationError> + 'a {
74        policy_entity_type_names(template)
75            .filter(|ety| !schema.is_known_entity_type(ety))
76            .map(|ety| {
77                ValidationError::unrecognized_entity_type(
78                    ety.loc().cloned(),
79                    template.id().clone(),
80                    ety.to_string(),
81                    get_suggested_entity_type(ety, schema),
82                )
83            })
84    }
85
86    /// Generate `UnrecognizedActionId` error for every entity id with an action
87    /// entity type where the id could not be found in the actions list from the
88    /// schema.
89    pub fn validate_action_ids<'a>(
90        schema: &'a ValidatorSchema,
91        template: &'a Template,
92    ) -> impl Iterator<Item = ValidationError> + 'a {
93        policy_entity_uids(template).filter_map(move |euid| {
94            let entity_type = euid.entity_type();
95            if entity_type.is_action() && !schema.is_known_action_id(euid) {
96                Some(ValidationError::unrecognized_action_id(
97                    euid.loc().cloned(),
98                    template.id().clone(),
99                    euid.to_string(),
100                    unrecognized_action_id_help(euid, schema),
101                ))
102            } else {
103                None
104            }
105        })
106    }
107
108    /// Generate `UnrecognizedEntityType` error for
109    /// every entity type in the slot environment that is not in the schema
110    pub(crate) fn validate_entity_types_in_slots<'a>(
111        &'a self,
112        policy_id: &'a PolicyID,
113        slots: &'a SlotEnv,
114    ) -> impl Iterator<Item = ValidationError> + 'a {
115        slots
116            .values()
117            .filter(|euid| !self.schema.is_known_entity_type(euid.entity_type()))
118            .map(|euid| {
119                ValidationError::unrecognized_entity_type(
120                    None,
121                    policy_id.clone(),
122                    euid.entity_type().to_string(),
123                    get_suggested_entity_type(euid.entity_type(), &self.schema),
124                )
125            })
126    }
127
128    fn check_if_in_fixes_principal(
129        &self,
130        principal_constraint: &PrincipalConstraint,
131        action_constraint: &ActionConstraint,
132    ) -> bool {
133        self.check_if_in_fixes(
134            principal_constraint.as_inner(),
135            &self
136                .get_apply_specs_for_action(action_constraint)
137                .collect::<Vec<_>>(),
138            &|spec| Box::new(spec.applicable_principal_types()),
139        )
140    }
141
142    fn check_if_in_fixes_resource(
143        &self,
144        resource_constraint: &ResourceConstraint,
145        action_constraint: &ActionConstraint,
146    ) -> bool {
147        self.check_if_in_fixes(
148            resource_constraint.as_inner(),
149            &self
150                .get_apply_specs_for_action(action_constraint)
151                .collect::<Vec<_>>(),
152            &|spec| Box::new(spec.applicable_resource_types()),
153        )
154    }
155
156    fn check_if_in_fixes<'a>(
157        &'a self,
158        scope_constraint: &PrincipalOrResourceConstraint,
159        apply_specs: &[&'a ValidatorApplySpec<ast::EntityType>],
160        select_apply_spec: &impl Fn(
161            &'a ValidatorApplySpec<ast::EntityType>,
162        ) -> Box<dyn Iterator<Item = &'a ast::EntityType> + 'a>,
163    ) -> bool {
164        let entity_type = Validator::get_eq_comparison(scope_constraint);
165
166        // Now we check the following property
167        // not exists spec in apply_specs such that lit in spec.principals
168        // AND
169        // exists spec in apply_specs such that there exists principal in spec.principals such that lit `memberOf` principal
170        // (as well as for resource)
171        self.check_if_none_equal(apply_specs, entity_type, &select_apply_spec)
172            && self.check_if_any_contain(apply_specs, entity_type, &select_apply_spec)
173    }
174
175    // This checks the first property:
176    // not exists spec in apply_specs such that lit in spec.principals
177    fn check_if_none_equal<'a>(
178        &'a self,
179        specs: &[&'a ValidatorApplySpec<ast::EntityType>],
180        lit_opt: Option<&ast::EntityType>,
181        select_apply_spec: &impl Fn(
182            &'a ValidatorApplySpec<ast::EntityType>,
183        ) -> Box<dyn Iterator<Item = &'a ast::EntityType> + 'a>,
184    ) -> bool {
185        if let Some(lit) = lit_opt {
186            !specs
187                .iter()
188                .any(|spec| select_apply_spec(spec).any(|e| e == lit))
189        } else {
190            false
191        }
192    }
193
194    // This checks the second property
195    // exists spec in apply_specs such that there exists principal in spec.principals such that lit `memberOf` principal
196    fn check_if_any_contain<'a>(
197        &'a self,
198        specs: &[&'a ValidatorApplySpec<ast::EntityType>],
199        lit_opt: Option<&ast::EntityType>,
200        select_apply_spec: &impl Fn(
201            &'a ValidatorApplySpec<ast::EntityType>,
202        ) -> Box<dyn Iterator<Item = &'a ast::EntityType> + 'a>,
203    ) -> bool {
204        if let Some(etype) = lit_opt.and_then(|typename| self.schema.get_entity_type(typename)) {
205            specs
206                .iter()
207                .any(|spec| select_apply_spec(spec).any(|p| etype.descendants.contains(p)))
208        } else {
209            false
210        }
211    }
212
213    /// Check if an expression is an equality comparison between a literal EUID
214    /// and a scope variable.  If it is, return the type of the literal EUID.
215    fn get_eq_comparison(
216        scope_constraint: &PrincipalOrResourceConstraint,
217    ) -> Option<&ast::EntityType> {
218        match scope_constraint {
219            PrincipalOrResourceConstraint::Eq(EntityReference::EUID(euid)) => {
220                Some(euid.entity_type())
221            }
222            _ => None,
223        }
224    }
225
226    pub(crate) fn validate_linked_action_application<'a>(
227        &self,
228        p: &'a Policy,
229    ) -> impl Iterator<Item = ValidationError> + 'a {
230        self.validate_action_application(
231            p.loc(),
232            p.id(),
233            &p.principal_constraint(),
234            p.action_constraint(),
235            &p.resource_constraint(),
236        )
237    }
238
239    pub(crate) fn validate_template_action_application<'a>(
240        &self,
241        t: &'a Template,
242    ) -> impl Iterator<Item = ValidationError> + 'a {
243        self.validate_action_application(
244            t.loc(),
245            t.id(),
246            t.principal_constraint(),
247            t.action_constraint(),
248            t.resource_constraint(),
249        )
250    }
251
252    // Check that there exists a (action id, principal type, resource type)
253    // entity type pair where the action can be applied to both the principal
254    // and resource. This function takes the three scope constraints as input
255    // (rather than a template) to facilitate code reuse.
256    fn validate_action_application(
257        &self,
258        source_loc: Option<&Loc>,
259        policy_id: &PolicyID,
260        principal_constraint: &PrincipalConstraint,
261        action_constraint: &ActionConstraint,
262        resource_constraint: &ResourceConstraint,
263    ) -> impl Iterator<Item = ValidationError> {
264        let mut apply_specs = self.get_apply_specs_for_action(action_constraint);
265        let resources_for_scope: HashSet<&ast::EntityType> = self
266            .get_resources_satisfying_constraint(resource_constraint)
267            .collect();
268        let principals_for_scope: HashSet<&ast::EntityType> = self
269            .get_principals_satisfying_constraint(principal_constraint)
270            .collect();
271
272        let would_in_fix_principal =
273            self.check_if_in_fixes_principal(principal_constraint, action_constraint);
274        let would_in_fix_resource =
275            self.check_if_in_fixes_resource(resource_constraint, action_constraint);
276
277        Some(ValidationError::invalid_action_application(
278            source_loc.cloned(),
279            policy_id.clone(),
280            would_in_fix_principal,
281            would_in_fix_resource,
282        ))
283        .filter(|_| {
284            !apply_specs.any(|spec| {
285                let action_principals = spec.applicable_principal_types().collect::<HashSet<_>>();
286                let action_resources = spec.applicable_resource_types().collect::<HashSet<_>>();
287                let matching_principal = !principals_for_scope.is_disjoint(&action_principals);
288                let matching_resource = !resources_for_scope.is_disjoint(&action_resources);
289                matching_principal && matching_resource
290            })
291        })
292        .into_iter()
293    }
294
295    /// Gather all `ApplySpec` objects for all actions in the schema.
296    pub(crate) fn get_apply_specs_for_action<'a>(
297        &'a self,
298        action_constraint: &'a ActionConstraint,
299    ) -> impl Iterator<Item = &'a ValidatorApplySpec<ast::EntityType>> + 'a {
300        self.get_actions_satisfying_constraint(action_constraint)
301            // Get the action type if the id string exists, and then the
302            // applies_to list.
303            .filter_map(|action_id| self.schema.get_action_id(action_id))
304            .map(|action| &action.applies_to)
305    }
306
307    /// Get the set of actions (action entity id strings) that satisfy the
308    /// action scope constraint of the policy.
309    fn get_actions_satisfying_constraint<'a>(
310        &'a self,
311        action_constraint: &'a ActionConstraint,
312    ) -> Box<dyn Iterator<Item = &'a EntityUID> + 'a> {
313        match action_constraint {
314            // <var>
315            ActionConstraint::Any => {
316                Box::new(self.schema.action_ids().map(ValidatorActionId::name))
317            }
318            // <var> == <literal euid>
319            ActionConstraint::Eq(euid) => Box::new(std::iter::once(euid.as_ref())),
320            // <var> in [<literal euid>...]
321            ActionConstraint::In(euids) => Box::new(
322                self.schema
323                    .get_actions_in_set(euids.iter().map(Arc::as_ref))
324                    .unwrap_or_default()
325                    .into_iter(),
326            ),
327            #[cfg(feature = "tolerant-ast")]
328            ActionConstraint::ErrorConstraint => {
329                let v = vec![].into_iter();
330                Box::new(v)
331            }
332        }
333    }
334
335    /// Get the set of principals (entity type strings) that satisfy the principal
336    /// scope constraint of the policy.
337    pub(crate) fn get_principals_satisfying_constraint<'a>(
338        &'a self,
339        principal_constraint: &'a PrincipalConstraint,
340    ) -> impl Iterator<Item = &'a ast::EntityType> + 'a {
341        self.get_entity_types_satisfying_constraint(principal_constraint.as_inner())
342    }
343
344    /// Get the set of resources (entity type strings) that satisfy the resource
345    /// scope constraint of the policy.
346    pub(crate) fn get_resources_satisfying_constraint<'a>(
347        &'a self,
348        resource_constraint: &'a ResourceConstraint,
349    ) -> impl Iterator<Item = &'a ast::EntityType> + 'a {
350        self.get_entity_types_satisfying_constraint(resource_constraint.as_inner())
351    }
352
353    // Get the set of entity types satisfying the condition for the principal
354    // or resource variable in the policy scope.
355    fn get_entity_types_satisfying_constraint<'a>(
356        &'a self,
357        scope_constraint: &'a PrincipalOrResourceConstraint,
358    ) -> Box<dyn Iterator<Item = &'a ast::EntityType> + 'a> {
359        match scope_constraint {
360            // <var>
361            PrincipalOrResourceConstraint::Any => Box::new(self.schema.entity_type_names()),
362            // <var> == <literal euid>
363            PrincipalOrResourceConstraint::Eq(EntityReference::EUID(euid)) => {
364                Box::new(std::iter::once(euid.entity_type()))
365            }
366            // <var> in <literal euid>
367            PrincipalOrResourceConstraint::In(EntityReference::EUID(euid)) => Box::new(
368                self.schema
369                    .get_entity_types_in(euid.entity_type())
370                    .into_iter(),
371            ),
372            PrincipalOrResourceConstraint::Eq(EntityReference::Slot(_))
373            | PrincipalOrResourceConstraint::In(EntityReference::Slot(_)) => {
374                Box::new(self.schema.entity_type_names())
375            }
376            PrincipalOrResourceConstraint::Is(entity_type)
377            | PrincipalOrResourceConstraint::IsIn(entity_type, EntityReference::Slot(_)) => {
378                Box::new(
379                    if self.schema.is_known_entity_type(entity_type) {
380                        Some(entity_type.as_ref())
381                    } else {
382                        None
383                    }
384                    .into_iter(),
385                )
386            }
387            PrincipalOrResourceConstraint::IsIn(entity_type, EntityReference::EUID(in_entity)) => {
388                Box::new(
389                    self.schema
390                        .get_entity_types_in(in_entity.entity_type())
391                        .into_iter()
392                        .filter(move |k| &entity_type.as_ref() == k),
393                )
394            }
395        }
396    }
397}
398
399#[cfg(test)]
400#[expect(clippy::panic, clippy::indexing_slicing, reason = "unit tests")]
401mod test {
402    use std::collections::{HashMap, HashSet};
403
404    use crate::{
405        ast::{Effect, Eid, EntityUID, PolicyID, PrincipalConstraint, ResourceConstraint},
406        est::Annotations,
407        parser::{parse_policy, parse_policy_or_template},
408        test_utils::{expect_err, ExpectedErrorMessageBuilder},
409    };
410    use miette::Report;
411
412    use super::*;
413    use crate::validator::{
414        json_schema, validation_errors::UnrecognizedEntityType, RawName, ValidationMode,
415        ValidationWarning, Validator,
416    };
417
418    #[test]
419    fn validate_entity_type_empty_schema() {
420        let src = r#"permit(principal, action, resource == foo_type::"foo_name");"#;
421        let policy = parse_policy_or_template(None, src).unwrap();
422        let validate = Validator::new(ValidatorSchema::empty());
423        let notes: Vec<ValidationError> =
424            Validator::validate_entity_types(validate.schema(), &policy).collect();
425        expect_err(
426            src,
427            &Report::new(notes.first().unwrap().clone()),
428            &ExpectedErrorMessageBuilder::error(
429                "for policy `policy0`, unrecognized entity type `foo_type`",
430            )
431            .exactly_one_underline("foo_type")
432            .build(),
433        );
434        assert_eq!(notes.len(), 1, "{notes:?}");
435    }
436
437    #[test]
438    fn validate_equals_instead_of_in() {
439        let schema_file: json_schema::NamespaceDefinition<RawName> =
440            serde_json::from_value(serde_json::json!(
441                {
442                    "entityTypes": {
443                        "user": {
444                            "memberOfTypes": ["admins"]
445                        },
446                        "admins": {},
447                        "widget": {
448                            "memberOfTypes": ["bin"]
449                        },
450                        "bin": {}
451                    },
452                    "actions": {
453                        "act": {
454                            "appliesTo": {
455                                "principalTypes": ["user"],
456                                "resourceTypes": ["widget"]
457                            }
458                        }
459                    }
460                }
461            ))
462            .unwrap();
463        let schema = schema_file.try_into().unwrap();
464
465        let src = r#"permit(principal == admins::"admin1", action == Action::"act", resource == bin::"bin");"#;
466        let p = parse_policy_or_template(None, src).unwrap();
467
468        let validate = Validator::new(schema);
469        let notes: Vec<ValidationError> =
470            validate.validate_template_action_application(&p).collect();
471
472        expect_err(
473            src,
474            &Report::new(notes.first().unwrap().clone()),
475            &ExpectedErrorMessageBuilder::error(
476                r#"for policy `policy0`, unable to find an applicable action given the policy scope constraints"#,
477            )
478            .help("try replacing `==` with `in` in the principal clause and the resource clause")
479            .exactly_one_underline(src)
480            .build(),
481        );
482        assert_eq!(notes.len(), 1, "{notes:?}");
483    }
484
485    #[test]
486    fn validate_entity_type_in_singleton_schema() {
487        let foo_type = "foo_type";
488        let schema_file = json_schema::NamespaceDefinition::new(
489            [(
490                foo_type.parse().unwrap(),
491                json_schema::StandardEntityType {
492                    member_of_types: vec![],
493                    shape: json_schema::AttributesOrContext::default(),
494                    tags: None,
495                }
496                .into(),
497            )],
498            [],
499        );
500        let singleton_schema = schema_file.try_into().unwrap();
501        let policy = Template::new(
502            PolicyID::from_string("policy0"),
503            None,
504            ast::Annotations::new(),
505            Effect::Permit,
506            PrincipalConstraint::any(),
507            ActionConstraint::any(),
508            ResourceConstraint::is_eq(Arc::new(
509                EntityUID::with_eid_and_type(foo_type, "foo_name")
510                    .expect("should be a valid identifier"),
511            )),
512            None,
513        );
514
515        let validate = Validator::new(singleton_schema);
516        assert!(
517            Validator::validate_entity_types(validate.schema(), &policy)
518                .next()
519                .is_none(),
520            "Did not expect any validation errors."
521        );
522    }
523
524    #[test]
525    fn validate_entity_type_not_in_singleton_schema() {
526        let schema_file = json_schema::NamespaceDefinition::new(
527            [(
528                "foo_type".parse().unwrap(),
529                json_schema::StandardEntityType {
530                    member_of_types: vec![],
531                    shape: json_schema::AttributesOrContext::default(),
532                    tags: None,
533                }
534                .into(),
535            )],
536            [],
537        );
538        let singleton_schema = schema_file.try_into().unwrap();
539
540        let src = r#"permit(principal, action, resource == bar_type::"bar_name");"#;
541        let policy = parse_policy_or_template(None, src).unwrap();
542        let validate = Validator::new(singleton_schema);
543        let notes: Vec<ValidationError> =
544            Validator::validate_entity_types(validate.schema(), &policy).collect();
545        expect_err(
546            src,
547            &Report::new(notes.first().unwrap().clone()),
548            &ExpectedErrorMessageBuilder::error(
549                "for policy `policy0`, unrecognized entity type `bar_type`",
550            )
551            .exactly_one_underline("bar_type")
552            .help("did you mean `foo_type`?")
553            .build(),
554        );
555        assert_eq!(notes.len(), 1, "{notes:?}");
556    }
557
558    #[test]
559    fn validate_action_id_empty_schema() {
560        let src = r#"permit(principal, action == Action::"foo_name", resource);"#;
561        let policy = parse_policy_or_template(None, src).unwrap();
562        let validate = Validator::new(ValidatorSchema::empty());
563        let notes: Vec<ValidationError> =
564            Validator::validate_action_ids(validate.schema(), &policy).collect();
565        expect_err(
566            src,
567            &Report::new(notes.first().unwrap().clone()),
568            &ExpectedErrorMessageBuilder::error(
569                r#"for policy `policy0`, unrecognized action `Action::"foo_name"`"#,
570            )
571            .exactly_one_underline(r#"Action::"foo_name""#)
572            .build(),
573        );
574        assert_eq!(notes.len(), 1, "{notes:?}");
575    }
576
577    #[test]
578    fn validate_action_id_in_singleton_schema() {
579        let foo_name = "foo_name";
580        let schema_file = json_schema::NamespaceDefinition::new(
581            [],
582            [(
583                foo_name.into(),
584                json_schema::ActionType {
585                    applies_to: None,
586                    member_of: None,
587                    attributes: None,
588                    annotations: Annotations::new(),
589                    loc: None,
590                    #[cfg(feature = "extended-schema")]
591                    defn_loc: None,
592                },
593            )],
594        );
595        let singleton_schema = schema_file.try_into().unwrap();
596        let entity =
597            EntityUID::with_eid_and_type("Action", foo_name).expect("should be a valid identifier");
598        let policy = Template::new(
599            PolicyID::from_string("policy0"),
600            None,
601            ast::Annotations::new(),
602            Effect::Permit,
603            PrincipalConstraint::any(),
604            ActionConstraint::is_eq(entity),
605            ResourceConstraint::any(),
606            None,
607        );
608
609        let validate = Validator::new(singleton_schema);
610        assert!(
611            Validator::validate_action_ids(validate.schema(), &policy)
612                .next()
613                .is_none(),
614            "Did not expect any validation errors."
615        );
616    }
617
618    #[test]
619    fn validate_principal_slot_in_singleton_schema() {
620        let p_name = "User";
621        let schema_file = json_schema::NamespaceDefinition::new(
622            [(
623                p_name.parse().unwrap(),
624                json_schema::StandardEntityType {
625                    member_of_types: vec![],
626                    shape: json_schema::AttributesOrContext::default(),
627                    tags: None,
628                }
629                .into(),
630            )],
631            [],
632        );
633        let schema = schema_file.try_into().unwrap();
634        let principal_constraint = PrincipalConstraint::is_eq_slot();
635        let validator = Validator::new(schema);
636        let entities = validator
637            .get_principals_satisfying_constraint(&principal_constraint)
638            .collect::<Vec<_>>();
639        assert_eq!(entities.len(), 1);
640        let name = entities[0];
641        assert_eq!(name, &p_name.parse().expect("Expected valid entity type."));
642    }
643
644    #[test]
645    fn validate_resource_slot_in_singleton_schema() {
646        let p_name = "Package";
647        let schema_file = json_schema::NamespaceDefinition::new(
648            [(
649                p_name.parse().unwrap(),
650                json_schema::StandardEntityType {
651                    member_of_types: vec![],
652                    shape: json_schema::AttributesOrContext::default(),
653                    tags: None,
654                }
655                .into(),
656            )],
657            [],
658        );
659        let schema = schema_file.try_into().unwrap();
660        let principal_constraint = PrincipalConstraint::any();
661        let validator = Validator::new(schema);
662        let entities = validator
663            .get_principals_satisfying_constraint(&principal_constraint)
664            .collect::<Vec<_>>();
665        assert_eq!(entities.len(), 1);
666        let name = entities[0];
667        assert_eq!(name, &p_name.parse().expect("Expected valid entity type."));
668    }
669
670    #[test]
671    fn undefined_entity_type_in_principal_slot() {
672        let p_name = "User";
673        let schema_file = json_schema::NamespaceDefinition::new(
674            [(
675                p_name.parse().unwrap(),
676                json_schema::StandardEntityType {
677                    member_of_types: vec![],
678                    shape: json_schema::AttributesOrContext::default(),
679                    tags: None,
680                }
681                .into(),
682            )],
683            [],
684        );
685        let schema = schema_file.try_into().expect("Invalid schema");
686
687        let undefined_euid: EntityUID = "Undefined::\"foo\""
688            .parse()
689            .expect("Expected entity UID to parse.");
690        let env = HashMap::from([(ast::SlotId::principal(), undefined_euid)]);
691
692        let validator = Validator::new(schema);
693        let notes: Vec<ValidationError> = validator
694            .validate_entity_types_in_slots(&PolicyID::from_string("0"), &env)
695            .collect();
696
697        assert_eq!(1, notes.len());
698        match notes.first() {
699            Some(ValidationError::UnrecognizedEntityType(UnrecognizedEntityType {
700                actual_entity_type,
701                suggested_entity_type,
702                ..
703            })) => {
704                assert_eq!("Undefined", actual_entity_type);
705                assert_eq!(
706                    "User",
707                    suggested_entity_type
708                        .as_ref()
709                        .expect("Expected a suggested entity type")
710                );
711            }
712            _ => panic!("Unexpected variant of ValidationErrorKind."),
713        };
714    }
715
716    #[test]
717    fn validate_action_id_not_in_singleton_schema() {
718        let schema_file = json_schema::NamespaceDefinition::new(
719            [],
720            [(
721                "foo_name".into(),
722                json_schema::ActionType {
723                    applies_to: None,
724                    member_of: None,
725                    attributes: None,
726                    annotations: Annotations::new(),
727                    loc: None,
728                    #[cfg(feature = "extended-schema")]
729                    defn_loc: None,
730                },
731            )],
732        );
733        let singleton_schema = schema_file.try_into().unwrap();
734
735        let src = r#"permit(principal, action == Action::"bar_name", resource);"#;
736        let policy = parse_policy_or_template(None, src).unwrap();
737        let validate = Validator::new(singleton_schema);
738        let notes: Vec<ValidationError> =
739            Validator::validate_action_ids(validate.schema(), &policy).collect();
740        expect_err(
741            src,
742            &Report::new(notes.first().unwrap().clone()),
743            &ExpectedErrorMessageBuilder::error(
744                r#"for policy `policy0`, unrecognized action `Action::"bar_name"`"#,
745            )
746            .exactly_one_underline(r#"Action::"bar_name""#)
747            .help(r#"did you mean `Action::"foo_name"`?"#)
748            .build(),
749        );
750        assert_eq!(notes.len(), 1, "{notes:?}");
751    }
752
753    #[test]
754    fn validate_action_id_with_action_type() {
755        let schema_file = json_schema::NamespaceDefinition::new(
756            [],
757            [(
758                "Action::view".into(),
759                json_schema::ActionType {
760                    applies_to: None,
761                    member_of: None,
762                    attributes: None,
763                    annotations: Annotations::new(),
764                    loc: None,
765                    #[cfg(feature = "extended-schema")]
766                    defn_loc: None,
767                },
768            )],
769        );
770        let singleton_schema = schema_file.try_into().unwrap();
771
772        let src = r#"permit(principal, action == Action::"view", resource);"#;
773        let policy = parse_policy_or_template(None, src).unwrap();
774        let validate = Validator::new(singleton_schema);
775        let notes: Vec<ValidationError> =
776            Validator::validate_action_ids(validate.schema(), &policy).collect();
777        expect_err(
778            src,
779            &Report::new(notes.first().unwrap().clone()),
780            &ExpectedErrorMessageBuilder::error(
781                r#"for policy `policy0`, unrecognized action `Action::"view"`"#,
782            )
783            .exactly_one_underline(r#"Action::"view""#)
784            .help(r#"did you intend to include the type in action `Action::"Action::view"`?"#)
785            .build(),
786        );
787        assert_eq!(notes.len(), 1, "{notes:?}");
788    }
789
790    #[test]
791    fn validate_action_id_with_action_type_namespace() {
792        let schema_src = r#"
793        {
794            "foo::foo::bar::baz": {
795                "entityTypes": {},
796                "actions": {
797                    "Action::view": {}
798                }
799            }
800        }"#;
801
802        let schema_fragment: json_schema::Fragment<RawName> =
803            serde_json::from_str(schema_src).expect("Parse Error");
804        let schema = schema_fragment.try_into().unwrap();
805
806        let src = r#"permit(principal, action == Action::"view", resource);"#;
807        let policy = parse_policy_or_template(None, src).unwrap();
808        let validate = Validator::new(schema);
809        let notes: Vec<ValidationError> =
810            Validator::validate_action_ids(validate.schema(), &policy).collect();
811        expect_err(
812            src,
813            &Report::new(notes.first().unwrap().clone()),
814            &ExpectedErrorMessageBuilder::error(
815                r#"for policy `policy0`, unrecognized action `Action::"view"`"#,
816            )
817            .exactly_one_underline(r#"Action::"view""#)
818            .help(r#"did you intend to include the type in action `foo::foo::bar::baz::Action::"Action::view"`?"#)
819            .build(),
820        );
821        assert_eq!(notes.len(), 1, "{notes:?}");
822    }
823
824    #[test]
825    fn validate_namespaced_action_id_in_schema() {
826        let descriptors = json_schema::Fragment::from_json_str(
827            r#"
828                {
829                    "NS": {
830                        "entityTypes": {},
831                        "actions": { "foo_name": {} }
832                    }
833                }"#,
834        )
835        .expect("Expected schema parse.");
836        let schema = descriptors.try_into().unwrap();
837        let entity: EntityUID = "NS::Action::\"foo_name\""
838            .parse()
839            .expect("Expected entity parse.");
840        let policy = Template::new(
841            PolicyID::from_string("policy0"),
842            None,
843            ast::Annotations::new(),
844            Effect::Permit,
845            PrincipalConstraint::any(),
846            ActionConstraint::is_eq(entity),
847            ResourceConstraint::any(),
848            None,
849        );
850
851        let validate = Validator::new(schema);
852        let notes: Vec<ValidationError> =
853            Validator::validate_action_ids(validate.schema(), &policy).collect();
854        assert_eq!(notes, vec![], "Did not expect any invalid action.");
855    }
856
857    #[test]
858    fn validate_namespaced_invalid_action() {
859        let descriptors = json_schema::Fragment::from_json_str(
860            r#"
861                {
862                    "NS": {
863                        "entityTypes": {},
864                        "actions": { "foo_name": {} }
865                    }
866                }"#,
867        )
868        .expect("Expected schema parse.");
869        let schema = descriptors.try_into().unwrap();
870
871        let src = r#"permit(principal, action == Bogus::Action::"foo_name", resource);"#;
872        let policy = parse_policy_or_template(None, src).unwrap();
873        let validate = Validator::new(schema);
874        let notes: Vec<ValidationError> =
875            Validator::validate_action_ids(validate.schema(), &policy).collect();
876        expect_err(
877            src,
878            &Report::new(notes.first().unwrap().clone()),
879            &ExpectedErrorMessageBuilder::error(
880                r#"for policy `policy0`, unrecognized action `Bogus::Action::"foo_name"`"#,
881            )
882            .exactly_one_underline(r#"Bogus::Action::"foo_name""#)
883            .help(r#"did you mean `NS::Action::"foo_name"`?"#)
884            .build(),
885        );
886        assert_eq!(notes.len(), 1, "{notes:?}");
887    }
888
889    #[test]
890    fn validate_namespaced_entity_type_in_schema() {
891        let descriptors = json_schema::Fragment::from_json_str(
892            r#"
893                {
894                    "NS": {
895                        "entityTypes": {"Foo": {} },
896                        "actions": {}
897                    }
898                }"#,
899        )
900        .expect("Expected schema parse.");
901        let schema = descriptors.try_into().unwrap();
902        let entity_type: ast::EntityType = "NS::Foo".parse().expect("Expected entity type parse.");
903        let policy = Template::new(
904            PolicyID::from_string("policy0"),
905            None,
906            ast::Annotations::new(),
907            Effect::Permit,
908            PrincipalConstraint::is_eq(Arc::new(EntityUID::from_components(
909                entity_type,
910                Eid::new("bar"),
911                None,
912            ))),
913            ActionConstraint::any(),
914            ResourceConstraint::any(),
915            None,
916        );
917
918        let validate = Validator::new(schema);
919        let notes: Vec<ValidationError> =
920            Validator::validate_entity_types(validate.schema(), &policy).collect();
921
922        assert_eq!(notes, vec![], "Did not expect any invalid action.");
923    }
924
925    #[test]
926    fn validate_namespaced_invalid_entity_type() {
927        let descriptors = json_schema::Fragment::from_json_str(
928            r#"
929                {
930                    "NS": {
931                        "entityTypes": {"Foo": {} },
932                        "actions": {}
933                    }
934                }"#,
935        )
936        .expect("Expected schema parse.");
937        let schema = descriptors.try_into().unwrap();
938
939        let src = r#"permit(principal == Bogus::Foo::"bar", action, resource);"#;
940        let policy = parse_policy_or_template(None, src).unwrap();
941        let validate = Validator::new(schema);
942        let notes: Vec<ValidationError> =
943            Validator::validate_entity_types(validate.schema(), &policy).collect();
944        expect_err(
945            src,
946            &Report::new(notes.first().unwrap().clone()),
947            &ExpectedErrorMessageBuilder::error(
948                "for policy `policy0`, unrecognized entity type `Bogus::Foo`",
949            )
950            .exactly_one_underline("Bogus::Foo")
951            .help("did you mean `NS::Foo`?")
952            .build(),
953        );
954        assert_eq!(notes.len(), 1, "{notes:?}");
955    }
956
957    #[test]
958    fn get_possible_actions_eq() {
959        let foo_name = "foo_name";
960        let euid_foo =
961            EntityUID::with_eid_and_type("Action", foo_name).expect("should be a valid identifier");
962        let action_constraint = ActionConstraint::is_eq(euid_foo.clone());
963
964        let schema_file = json_schema::NamespaceDefinition::new(
965            [],
966            [(
967                foo_name.into(),
968                json_schema::ActionType {
969                    applies_to: None,
970                    member_of: None,
971                    attributes: None,
972                    annotations: Annotations::new(),
973                    loc: None,
974                    #[cfg(feature = "extended-schema")]
975                    defn_loc: None,
976                },
977            )],
978        );
979        let singleton_schema = schema_file.try_into().unwrap();
980
981        let validate = Validator::new(singleton_schema);
982        let actions = validate
983            .get_actions_satisfying_constraint(&action_constraint)
984            .collect();
985        assert_eq!(HashSet::from([&euid_foo]), actions);
986    }
987
988    #[test]
989    fn get_possible_actions_in_no_parents() {
990        let foo_name = "foo_name";
991        let euid_foo =
992            EntityUID::with_eid_and_type("Action", foo_name).expect("should be a valid identifier");
993        let action_constraint = ActionConstraint::is_in(vec![euid_foo.clone()]);
994
995        let schema_file = json_schema::NamespaceDefinition::new(
996            [],
997            [(
998                foo_name.into(),
999                json_schema::ActionType {
1000                    applies_to: None,
1001                    member_of: None,
1002                    attributes: None,
1003                    annotations: Annotations::new(),
1004                    loc: None,
1005                    #[cfg(feature = "extended-schema")]
1006                    defn_loc: None,
1007                },
1008            )],
1009        );
1010        let singleton_schema = schema_file.try_into().unwrap();
1011
1012        let validate = Validator::new(singleton_schema);
1013        let actions = validate
1014            .get_actions_satisfying_constraint(&action_constraint)
1015            .collect();
1016        assert_eq!(HashSet::from([&euid_foo]), actions);
1017    }
1018
1019    #[test]
1020    fn get_possible_actions_in_set_no_parents() {
1021        let foo_name = "foo_name";
1022        let euid_foo =
1023            EntityUID::with_eid_and_type("Action", foo_name).expect("should be a valid identifier");
1024        let action_constraint = ActionConstraint::is_in(vec![euid_foo.clone()]);
1025
1026        let schema_file = json_schema::NamespaceDefinition::new(
1027            [],
1028            [(
1029                foo_name.into(),
1030                json_schema::ActionType {
1031                    applies_to: None,
1032                    member_of: None,
1033                    attributes: None,
1034                    annotations: Annotations::new(),
1035                    loc: None,
1036                    #[cfg(feature = "extended-schema")]
1037                    defn_loc: None,
1038                },
1039            )],
1040        );
1041        let singleton_schema = schema_file.try_into().unwrap();
1042
1043        let validate = Validator::new(singleton_schema);
1044        let actions = validate
1045            .get_actions_satisfying_constraint(&action_constraint)
1046            .collect();
1047        assert_eq!(HashSet::from([&euid_foo]), actions);
1048    }
1049
1050    #[test]
1051    fn get_possible_principals_eq() {
1052        let foo_type = "foo_type";
1053        let euid_foo = EntityUID::with_eid_and_type(foo_type, "foo_name")
1054            .expect("should be a valid identifier");
1055        let principal_constraint = PrincipalConstraint::is_eq(Arc::new(euid_foo.clone()));
1056
1057        let schema_file = json_schema::NamespaceDefinition::new(
1058            [(
1059                foo_type.parse().unwrap(),
1060                json_schema::StandardEntityType {
1061                    member_of_types: vec![],
1062                    shape: json_schema::AttributesOrContext::default(),
1063                    tags: None,
1064                }
1065                .into(),
1066            )],
1067            [],
1068        );
1069        let singleton_schema = schema_file.try_into().unwrap();
1070
1071        let validate = Validator::new(singleton_schema);
1072        let principals = validate
1073            .get_principals_satisfying_constraint(&principal_constraint)
1074            .cloned()
1075            .collect::<HashSet<_>>();
1076        assert_eq!(HashSet::from([euid_foo.components().0]), principals);
1077    }
1078
1079    fn schema_with_single_principal_action_resource(
1080    ) -> (EntityUID, EntityUID, EntityUID, ValidatorSchema) {
1081        let action_name = "foo";
1082        let action_euid = EntityUID::with_eid_and_type("Action", action_name)
1083            .expect("should be a valid identifier");
1084        let principal_type = "bar";
1085        let principal_euid = EntityUID::with_eid_and_type(principal_type, "principal")
1086            .expect("should be a valid identifier");
1087        let resource_type = "baz";
1088        let resource_euid = EntityUID::with_eid_and_type(resource_type, "resource")
1089            .expect("should be a valid identifier");
1090
1091        let schema = json_schema::NamespaceDefinition::new(
1092            [
1093                (
1094                    principal_type.parse().unwrap(),
1095                    json_schema::StandardEntityType {
1096                        member_of_types: vec![],
1097                        shape: json_schema::AttributesOrContext::default(),
1098                        tags: None,
1099                    }
1100                    .into(),
1101                ),
1102                (
1103                    resource_type.parse().unwrap(),
1104                    json_schema::StandardEntityType {
1105                        member_of_types: vec![],
1106                        shape: json_schema::AttributesOrContext::default(),
1107                        tags: None,
1108                    }
1109                    .into(),
1110                ),
1111            ],
1112            [(
1113                action_name.into(),
1114                json_schema::ActionType {
1115                    applies_to: Some(json_schema::ApplySpec {
1116                        resource_types: vec![resource_type.parse().unwrap()],
1117                        principal_types: vec![principal_type.parse().unwrap()],
1118                        context: json_schema::AttributesOrContext::default(),
1119                    }),
1120                    member_of: Some(vec![]),
1121                    attributes: None,
1122                    annotations: Annotations::new(),
1123                    loc: None,
1124                    #[cfg(feature = "extended-schema")]
1125                    defn_loc: None,
1126                },
1127            )],
1128        )
1129        .try_into()
1130        .expect("Expected valid schema file.");
1131        (principal_euid, action_euid, resource_euid, schema)
1132    }
1133
1134    #[track_caller] // report the caller's location as the location of the panic, not the location in this function
1135    fn assert_validate_policy_succeeds(validator: &Validator, policy: &Template) {
1136        assert!(
1137            validator
1138                .validate_policy(policy, ValidationMode::default())
1139                .0
1140                .next()
1141                .is_none(),
1142            "Did not expect any validation errors."
1143        );
1144        assert!(
1145            validator
1146                .validate_policy(policy, ValidationMode::default())
1147                .1
1148                .next()
1149                .is_none(),
1150            "Did not expect any validation warnings."
1151        );
1152    }
1153
1154    #[track_caller] // report the caller's location as the location of the panic, not the location in this function
1155    fn assert_validate_policy_fails(
1156        validator: &Validator,
1157        policy: &Template,
1158        expected: &[ValidationError],
1159    ) {
1160        assert_eq!(
1161            validator
1162                .validate_policy(policy, ValidationMode::default())
1163                .0
1164                .collect::<Vec<ValidationError>>(),
1165            expected,
1166            "Unexpected validation errors."
1167        );
1168    }
1169
1170    #[track_caller] // report the caller's location as the location of the panic, not the location in this function
1171    fn assert_validate_policy_flags_impossible_policy(validator: &Validator, policy: &Template) {
1172        assert_eq!(
1173            validator
1174                .validate_policy(policy, ValidationMode::default())
1175                .1
1176                .collect::<Vec<ValidationWarning>>(),
1177            vec![ValidationWarning::impossible_policy(
1178                policy.loc().cloned(),
1179                policy.id().clone()
1180            )],
1181            "Unexpected validation warnings."
1182        );
1183    }
1184
1185    #[test]
1186    fn validate_action_apply_correct() {
1187        let (principal, action, resource, schema) = schema_with_single_principal_action_resource();
1188
1189        let policy = Template::new(
1190            PolicyID::from_string("policy0"),
1191            None,
1192            ast::Annotations::new(),
1193            Effect::Permit,
1194            PrincipalConstraint::is_eq(Arc::new(principal)),
1195            ActionConstraint::is_eq(action),
1196            ResourceConstraint::is_eq(Arc::new(resource)),
1197            None,
1198        );
1199
1200        let validator = Validator::new(schema);
1201        assert_validate_policy_succeeds(&validator, &policy);
1202    }
1203
1204    #[test]
1205    fn validate_action_apply_incorrect_principal() {
1206        let (_, _, _, schema) = schema_with_single_principal_action_resource();
1207
1208        let src =
1209            r#"permit(principal == baz::"p", action == Action::"foo", resource == baz::"r");"#;
1210        let p = parse_policy_or_template(None, src).unwrap();
1211
1212        let validate = Validator::new(schema);
1213        let notes: Vec<ValidationError> =
1214            validate.validate_template_action_application(&p).collect();
1215
1216        expect_err(
1217            src,
1218            &Report::new(notes.first().unwrap().clone()),
1219            &ExpectedErrorMessageBuilder::error(
1220                r#"for policy `policy0`, unable to find an applicable action given the policy scope constraints"#,
1221            )
1222            .exactly_one_underline(src)
1223            .build(),
1224        );
1225        assert_eq!(notes.len(), 1, "{notes:?}");
1226    }
1227
1228    #[test]
1229    fn validate_action_apply_incorrect_resource() {
1230        let (_, _, _, schema) = schema_with_single_principal_action_resource();
1231
1232        let src =
1233            r#"permit(principal == bar::"p", action == Action::"foo", resource == bar::"r");"#;
1234        let p = parse_policy_or_template(None, src).unwrap();
1235
1236        let validate = Validator::new(schema);
1237        let notes: Vec<ValidationError> =
1238            validate.validate_template_action_application(&p).collect();
1239
1240        expect_err(
1241            src,
1242            &Report::new(notes.first().unwrap().clone()),
1243            &ExpectedErrorMessageBuilder::error(
1244                r#"for policy `policy0`, unable to find an applicable action given the policy scope constraints"#,
1245            )
1246            .exactly_one_underline(src)
1247            .build(),
1248        );
1249        assert_eq!(notes.len(), 1, "{notes:?}");
1250    }
1251
1252    #[test]
1253    fn validate_action_apply_incorrect_principal_and_resource() {
1254        let (_, _, _, schema) = schema_with_single_principal_action_resource();
1255
1256        let src =
1257            r#"permit(principal == baz::"p", action == Action::"foo", resource == bar::"r");"#;
1258        let p = parse_policy_or_template(None, src).unwrap();
1259
1260        let validate = Validator::new(schema);
1261        let notes: Vec<ValidationError> =
1262            validate.validate_template_action_application(&p).collect();
1263
1264        expect_err(
1265            src,
1266            &Report::new(notes.first().unwrap().clone()),
1267            &ExpectedErrorMessageBuilder::error(
1268                r#"for policy `policy0`, unable to find an applicable action given the policy scope constraints"#,
1269            )
1270            .exactly_one_underline(src)
1271            .build(),
1272        );
1273        assert_eq!(notes.len(), 1, "{notes:?}");
1274    }
1275
1276    #[test]
1277    fn validate_principal_is() {
1278        let (_, _, _, schema) = schema_with_single_principal_action_resource();
1279
1280        let policy =
1281            parse_policy_or_template(None, "permit(principal is bar, action, resource);").unwrap();
1282
1283        let validator = Validator::new(schema);
1284        assert_validate_policy_succeeds(&validator, &policy);
1285
1286        let policy = parse_policy_or_template(
1287            None,
1288            r#"permit(principal is bar in bar::"baz", action, resource);"#,
1289        )
1290        .unwrap();
1291
1292        assert_validate_policy_succeeds(&validator, &policy);
1293    }
1294
1295    #[test]
1296    fn validate_principal_is_err() {
1297        let (_, _, _, schema) = schema_with_single_principal_action_resource();
1298
1299        let src = "permit(principal is baz, action, resource);";
1300        let policy = parse_policy_or_template(None, src).unwrap();
1301
1302        let validator = Validator::new(schema);
1303        assert_validate_policy_fails(
1304            &validator,
1305            &policy,
1306            &[ValidationError::invalid_action_application(
1307                Some(Loc::new(0..43, Arc::from(src))),
1308                PolicyID::from_string("policy0"),
1309                false,
1310                false,
1311            )],
1312        );
1313        assert_validate_policy_flags_impossible_policy(&validator, &policy);
1314
1315        let src = r#"permit(principal is biz in faz::"a", action, resource);"#;
1316        let policy = parse_policy_or_template(None, src).unwrap();
1317
1318        assert_validate_policy_fails(
1319            &validator,
1320            &policy,
1321            &[
1322                ValidationError::unrecognized_entity_type(
1323                    Some(Loc::new(27..30, Arc::from(src))),
1324                    PolicyID::from_string("policy0"),
1325                    "faz".into(),
1326                    Some("baz".into()),
1327                ),
1328                ValidationError::unrecognized_entity_type(
1329                    Some(Loc::new(20..23, Arc::from(src))),
1330                    PolicyID::from_string("policy0"),
1331                    "biz".into(),
1332                    Some("baz".into()),
1333                ),
1334                ValidationError::invalid_action_application(
1335                    Some(Loc::new(0..55, Arc::from(src))),
1336                    PolicyID::from_string("policy0"),
1337                    false,
1338                    false,
1339                ),
1340            ],
1341        );
1342        assert_validate_policy_flags_impossible_policy(&validator, &policy);
1343
1344        let src = r#"permit(principal is bar in baz::"buz", action, resource);"#;
1345        let policy = parse_policy_or_template(None, src).unwrap();
1346
1347        assert_validate_policy_fails(
1348            &validator,
1349            &policy,
1350            &[ValidationError::invalid_action_application(
1351                Some(Loc::new(0..57, Arc::from(src))),
1352                PolicyID::from_string("policy0"),
1353                false,
1354                false,
1355            )],
1356        );
1357        assert_validate_policy_flags_impossible_policy(&validator, &policy);
1358    }
1359
1360    #[test]
1361    fn validate_resource_is() {
1362        let (_, _, _, schema) = schema_with_single_principal_action_resource();
1363
1364        let policy =
1365            parse_policy_or_template(None, "permit(principal, action, resource is baz);").unwrap();
1366
1367        let validator = Validator::new(schema);
1368        assert_validate_policy_succeeds(&validator, &policy);
1369
1370        let policy = parse_policy_or_template(
1371            None,
1372            r#"permit(principal, action, resource is baz in baz::"bar");"#,
1373        )
1374        .unwrap();
1375
1376        assert_validate_policy_succeeds(&validator, &policy);
1377    }
1378
1379    #[test]
1380    fn validate_resource_is_err() {
1381        let (_, _, _, schema) = schema_with_single_principal_action_resource();
1382
1383        let src = "permit(principal, action, resource is bar);";
1384        let policy = parse_policy_or_template(None, src).unwrap();
1385
1386        let validator = Validator::new(schema);
1387        assert_validate_policy_fails(
1388            &validator,
1389            &policy,
1390            &[ValidationError::invalid_action_application(
1391                Some(Loc::new(0..43, Arc::from(src))),
1392                PolicyID::from_string("policy0"),
1393                false,
1394                false,
1395            )],
1396        );
1397        assert_validate_policy_flags_impossible_policy(&validator, &policy);
1398
1399        let src = r#"permit(principal, action, resource is baz in bar::"buz");"#;
1400        let policy = parse_policy_or_template(None, src).unwrap();
1401
1402        assert_validate_policy_fails(
1403            &validator,
1404            &policy,
1405            &[ValidationError::invalid_action_application(
1406                Some(Loc::new(0..57, Arc::from(src))),
1407                PolicyID::from_string("policy0"),
1408                false,
1409                false,
1410            )],
1411        );
1412        assert_validate_policy_flags_impossible_policy(&validator, &policy);
1413
1414        let src = r#"permit(principal, action, resource is biz in faz::"a");"#;
1415        let policy = parse_policy_or_template(None, src).unwrap();
1416
1417        assert_validate_policy_fails(
1418            &validator,
1419            &policy,
1420            &[
1421                ValidationError::unrecognized_entity_type(
1422                    Some(Loc::new(45..48, Arc::from(src))),
1423                    PolicyID::from_string("policy0"),
1424                    "faz".into(),
1425                    Some("baz".into()),
1426                ),
1427                ValidationError::unrecognized_entity_type(
1428                    Some(Loc::new(38..41, Arc::from(src))),
1429                    PolicyID::from_string("policy0"),
1430                    "biz".into(),
1431                    Some("baz".into()),
1432                ),
1433                ValidationError::invalid_action_application(
1434                    Some(Loc::new(0..55, Arc::from(src))),
1435                    PolicyID::from_string("policy0"),
1436                    false,
1437                    false,
1438                ),
1439            ],
1440        );
1441        assert_validate_policy_flags_impossible_policy(&validator, &policy);
1442    }
1443
1444    #[test]
1445    fn is_unknown_entity_condition() {
1446        let (_, _, _, schema) = schema_with_single_principal_action_resource();
1447        let src = r#"permit(principal, action, resource) when { resource is biz };"#;
1448        let policy = parse_policy_or_template(None, src).unwrap();
1449
1450        let validator = Validator::new(schema);
1451        let err = validator
1452            .validate_policy(&policy, ValidationMode::default())
1453            .0
1454            .next()
1455            .unwrap();
1456        expect_err(
1457            src,
1458            &Report::new(err),
1459            &ExpectedErrorMessageBuilder::error(
1460                "for policy `policy0`, unrecognized entity type `biz`",
1461            )
1462            .exactly_one_underline("biz")
1463            .help("did you mean `baz`?")
1464            .build(),
1465        );
1466
1467        assert_validate_policy_flags_impossible_policy(&validator, &policy);
1468    }
1469
1470    #[test]
1471    fn test_with_tc_computation() {
1472        let action_name = "foo";
1473        let action_parent_name = "foo_parent";
1474        let action_grandparent_name = "foo_grandparent";
1475        let action_grandparent_euid =
1476            EntityUID::with_eid_and_type("Action", action_grandparent_name)
1477                .expect("should be a valid identifier");
1478
1479        let principal_type = "bar";
1480
1481        let resource_type = "baz";
1482        let resource_parent_type = "baz_parent";
1483        let resource_grandparent_type = "baz_grandparent";
1484        let resource_grandparent_euid =
1485            EntityUID::with_eid_and_type(resource_parent_type, "resource")
1486                .expect("should be a valid identifier");
1487
1488        let schema_file = json_schema::NamespaceDefinition::new(
1489            [
1490                (
1491                    principal_type.parse().unwrap(),
1492                    json_schema::StandardEntityType {
1493                        member_of_types: vec![],
1494                        shape: json_schema::AttributesOrContext::default(),
1495                        tags: None,
1496                    }
1497                    .into(),
1498                ),
1499                (
1500                    resource_type.parse().unwrap(),
1501                    json_schema::StandardEntityType {
1502                        member_of_types: vec![resource_parent_type.parse().unwrap()],
1503                        shape: json_schema::AttributesOrContext::default(),
1504                        tags: None,
1505                    }
1506                    .into(),
1507                ),
1508                (
1509                    resource_parent_type.parse().unwrap(),
1510                    json_schema::StandardEntityType {
1511                        member_of_types: vec![resource_grandparent_type.parse().unwrap()],
1512                        shape: json_schema::AttributesOrContext::default(),
1513                        tags: None,
1514                    }
1515                    .into(),
1516                ),
1517                (
1518                    resource_grandparent_type.parse().unwrap(),
1519                    json_schema::StandardEntityType {
1520                        member_of_types: vec![],
1521                        shape: json_schema::AttributesOrContext::default(),
1522                        tags: None,
1523                    }
1524                    .into(),
1525                ),
1526            ],
1527            [
1528                (
1529                    action_name.into(),
1530                    json_schema::ActionType {
1531                        applies_to: Some(json_schema::ApplySpec {
1532                            resource_types: vec![resource_type.parse().unwrap()],
1533                            principal_types: vec![principal_type.parse().unwrap()],
1534                            context: json_schema::AttributesOrContext::default(),
1535                        }),
1536                        member_of: Some(vec![json_schema::ActionEntityUID::new(
1537                            None,
1538                            action_parent_name.into(),
1539                        )]),
1540                        attributes: None,
1541                        annotations: Annotations::new(),
1542                        loc: None,
1543                        #[cfg(feature = "extended-schema")]
1544                        defn_loc: None,
1545                    },
1546                ),
1547                (
1548                    action_parent_name.into(),
1549                    json_schema::ActionType {
1550                        applies_to: None,
1551                        member_of: Some(vec![json_schema::ActionEntityUID::new(
1552                            None,
1553                            action_grandparent_name.into(),
1554                        )]),
1555                        attributes: None,
1556                        annotations: Annotations::new(),
1557                        loc: None,
1558                        #[cfg(feature = "extended-schema")]
1559                        defn_loc: None,
1560                    },
1561                ),
1562                (
1563                    action_grandparent_name.into(),
1564                    json_schema::ActionType {
1565                        applies_to: None,
1566                        member_of: Some(vec![]),
1567                        attributes: None,
1568                        annotations: Annotations::new(),
1569                        loc: None,
1570                        #[cfg(feature = "extended-schema")]
1571                        defn_loc: None,
1572                    },
1573                ),
1574            ],
1575        );
1576        let schema = schema_file.try_into().unwrap();
1577
1578        let policy = Template::new(
1579            PolicyID::from_string("policy0"),
1580            None,
1581            ast::Annotations::new(),
1582            Effect::Permit,
1583            PrincipalConstraint::any(),
1584            ActionConstraint::is_in([action_grandparent_euid]),
1585            ResourceConstraint::is_in(Arc::new(resource_grandparent_euid)),
1586            None,
1587        );
1588
1589        let validator = Validator::new(schema);
1590        assert_validate_policy_succeeds(&validator, &policy);
1591    }
1592
1593    #[test]
1594    fn unspecified_principal_resource_with_scope_conditions() {
1595        let schema = serde_json::from_str::<json_schema::NamespaceDefinition<RawName>>(
1596            r#"
1597        {
1598            "entityTypes": {"a": {}},
1599            "actions": {
1600                "": { }
1601            }
1602        }
1603        "#,
1604        )
1605        .unwrap()
1606        .try_into()
1607        .unwrap();
1608        let policy = parse_policy(
1609            Some(PolicyID::from_string("0")),
1610            r#"permit(principal == a::"p", action, resource == a::"r");"#,
1611        )
1612        .unwrap();
1613
1614        let validator = Validator::new(schema);
1615        let (template, _) = Template::link_static_policy(policy);
1616        assert_validate_policy_flags_impossible_policy(&validator, &template);
1617    }
1618}
1619
1620#[cfg(test)]
1621#[cfg(feature = "partial-validate")]
1622mod partial_schema {
1623    use crate::{
1624        ast::{PolicyID, StaticPolicy, Template},
1625        parser::parse_policy,
1626    };
1627
1628    use crate::validator::{json_schema, RawName, Validator};
1629
1630    #[track_caller] // report the caller's location as the location of the panic, not the location in this function
1631    fn assert_validates_with_empty_schema(policy: StaticPolicy) {
1632        let schema: json_schema::NamespaceDefinition<RawName> = serde_json::from_str(
1633            r#"
1634        {
1635            "entityTypes": { },
1636            "actions": {}
1637        }
1638        "#,
1639        )
1640        .unwrap();
1641        let schema = schema.try_into().unwrap();
1642
1643        let (template, _) = Template::link_static_policy(policy);
1644        let validate = Validator::new(schema);
1645        let errs = validate
1646            .validate_policy(&template, crate::validator::ValidationMode::Partial)
1647            .0
1648            .collect::<Vec<_>>();
1649        assert_eq!(errs, vec![], "Did not expect any validation errors.");
1650    }
1651
1652    #[test]
1653    fn undeclared_entity_type_partial_schema() {
1654        let policy = parse_policy(
1655            Some(PolicyID::from_string("0")),
1656            r#"permit(principal == User::"alice", action, resource);"#,
1657        )
1658        .unwrap();
1659        assert_validates_with_empty_schema(policy);
1660
1661        let policy = parse_policy(
1662            Some(PolicyID::from_string("0")),
1663            r#"permit(principal, action == Action::"view", resource);"#,
1664        )
1665        .unwrap();
1666        assert_validates_with_empty_schema(policy);
1667
1668        let policy = parse_policy(
1669            Some(PolicyID::from_string("0")),
1670            r#"permit(principal, action, resource == Photo::"party.jpg");"#,
1671        )
1672        .unwrap();
1673        assert_validates_with_empty_schema(policy);
1674    }
1675}