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