1#![deny(
19 missing_docs,
20 rustdoc::broken_intra_doc_links,
21 rustdoc::private_intra_doc_links,
22 rustdoc::invalid_codeblock_attributes,
23 rustdoc::invalid_html_tags,
24 rustdoc::invalid_rust_codeblocks,
25 rustdoc::bare_urls,
26 clippy::doc_markdown
27)]
28#![cfg_attr(
29 feature = "wasm",
30 allow(
31 non_snake_case,
32 reason = "Wasm/TypeScript doesn't use snake case identifiers by convention"
33 )
34)]
35
36use crate::ast::{Policy, PolicySet, Template};
37use std::collections::HashSet;
38mod level_validate;
39
40mod coreschema;
41#[cfg(feature = "entity-manifest")]
42pub mod entity_manifest;
43pub use coreschema::*;
44mod diagnostics;
45pub use diagnostics::*;
46mod expr_iterator;
47mod extension_schema;
48mod extensions;
49mod rbac;
50mod schema;
51pub use schema::err::*;
52pub use schema::*;
53mod deprecated_schema_compat;
54pub mod json_schema;
55mod str_checks;
56pub use str_checks::confusable_string_checks;
57pub mod cedar_schema;
58pub mod typecheck;
59use typecheck::Typechecker;
60mod partition_nonempty;
61pub mod types;
62
63#[derive(Default, Eq, PartialEq, Copy, Clone, Debug)]
65pub enum ValidationMode {
66 #[default]
68 Strict,
69 Permissive,
71 #[cfg(feature = "partial-validate")]
74 Partial,
75}
76
77impl ValidationMode {
78 fn is_partial(self) -> bool {
81 match self {
82 ValidationMode::Strict | ValidationMode::Permissive => false,
83 #[cfg(feature = "partial-validate")]
84 ValidationMode::Partial => true,
85 }
86 }
87
88 fn is_strict(self) -> bool {
90 match self {
91 ValidationMode::Strict => true,
92 ValidationMode::Permissive => false,
93 #[cfg(feature = "partial-validate")]
94 ValidationMode::Partial => false,
95 }
96 }
97}
98
99#[derive(Debug, Clone)]
102pub struct Validator {
103 schema: ValidatorSchema,
104}
105
106impl Validator {
107 pub fn new(schema: ValidatorSchema) -> Validator {
109 Self { schema }
110 }
111
112 pub fn schema(&self) -> &ValidatorSchema {
114 &self.schema
115 }
116
117 pub fn validate(&self, policies: &PolicySet, mode: ValidationMode) -> ValidationResult {
120 let validate_policy_results: (Vec<_>, Vec<_>) = policies
121 .all_templates()
122 .map(|p| self.validate_policy(p, mode))
123 .unzip();
124 let template_and_static_policy_errs = validate_policy_results.0.into_iter().flatten();
125 let template_and_static_policy_warnings = validate_policy_results.1.into_iter().flatten();
126 let link_errs = policies
127 .policies()
128 .filter_map(|p| self.validate_slots(p, mode))
129 .flatten();
130 ValidationResult::new(
131 template_and_static_policy_errs.chain(link_errs),
132 template_and_static_policy_warnings
133 .chain(confusable_string_checks(policies.all_templates())),
134 )
135 }
136
137 pub fn validate_with_level(
142 &self,
143 policies: &PolicySet,
144 mode: ValidationMode,
145 max_deref_level: u32,
146 ) -> ValidationResult {
147 let validate_policy_results: (Vec<_>, Vec<_>) = policies
148 .all_templates()
149 .map(|p| self.validate_policy_with_level(p, mode, max_deref_level))
150 .unzip();
151 let template_and_static_policy_errs = validate_policy_results.0.into_iter().flatten();
152 let template_and_static_policy_warnings = validate_policy_results.1.into_iter().flatten();
153 let link_errs = policies
154 .policies()
155 .filter_map(|p| self.validate_slots(p, mode))
156 .flatten();
157 ValidationResult::new(
158 template_and_static_policy_errs.chain(link_errs),
159 template_and_static_policy_warnings
160 .chain(confusable_string_checks(policies.all_templates())),
161 )
162 }
163
164 fn validate_policy<'a>(
168 &'a self,
169 p: &'a Template,
170 mode: ValidationMode,
171 ) -> (
172 impl Iterator<Item = ValidationError> + 'a,
173 impl Iterator<Item = ValidationWarning> + 'a,
174 ) {
175 let validation_errors = if mode.is_partial() {
176 None
181 } else {
182 Some(
183 Validator::validate_entity_types(&self.schema, p)
184 .chain(Validator::validate_enum_entity(&self.schema, p))
185 .chain(Validator::validate_action_ids(&self.schema, p))
186 .chain(self.validate_template_action_application(p)),
191 )
192 }
193 .into_iter()
194 .flatten();
195 let (errors, warnings) = self.typecheck_policy(p, mode);
196 (validation_errors.chain(errors), warnings)
197 }
198
199 pub fn validate_entity_types_and_literals<'a>(
204 schema: &'a ValidatorSchema,
205 p: &'a Template,
206 ) -> impl Iterator<Item = ValidationError> + 'a {
207 Validator::validate_entity_types(schema, p)
208 .chain(Validator::validate_enum_entity(schema, p))
209 .chain(Validator::validate_action_ids(schema, p))
210 }
211
212 fn validate_slots<'a>(
215 &'a self,
216 p: &'a Policy,
217 mode: ValidationMode,
218 ) -> Option<impl Iterator<Item = ValidationError> + 'a> {
219 if p.is_static() {
221 return None;
222 }
223 if mode.is_partial() {
227 return None;
228 }
229 Some(
233 self.validate_entity_types_in_slots(p.id(), p.env())
234 .chain(self.validate_linked_action_application(p)),
235 )
236 }
237
238 fn typecheck_policy<'a>(
244 &'a self,
245 t: &'a Template,
246 mode: ValidationMode,
247 ) -> (
248 impl Iterator<Item = ValidationError> + 'a,
249 impl Iterator<Item = ValidationWarning> + 'a,
250 ) {
251 let typecheck = Typechecker::new(&self.schema, mode);
252 let mut errors = HashSet::new();
253 let mut warnings = HashSet::new();
254 typecheck.typecheck_policy(t, &mut errors, &mut warnings);
255 (errors.into_iter(), warnings.into_iter())
256 }
257}
258
259#[cfg(test)]
260mod test {
261 use itertools::Itertools;
262 use std::{collections::HashMap, sync::Arc};
263
264 use crate::validator::types::Type;
265 use crate::validator::validation_errors::UnrecognizedActionIdHelp;
266 use crate::validator::Result;
267
268 use super::*;
269 use crate::{
270 ast::{self, PolicyID},
271 est::Annotations,
272 parser::{self, Loc},
273 };
274
275 use similar_asserts::assert_eq;
276
277 #[test]
278 fn top_level_validate() -> Result<()> {
279 let mut set = PolicySet::new();
280 let foo_type = "foo_type";
281 let bar_type = "bar_type";
282 let action_name = "action";
283 let schema_file = json_schema::NamespaceDefinition::new(
284 [
285 (
286 foo_type.parse().unwrap(),
287 json_schema::StandardEntityType {
288 member_of_types: vec![],
289 shape: json_schema::AttributesOrContext::default(),
290 tags: None,
291 }
292 .into(),
293 ),
294 (
295 bar_type.parse().unwrap(),
296 json_schema::StandardEntityType {
297 member_of_types: vec![],
298 shape: json_schema::AttributesOrContext::default(),
299 tags: None,
300 }
301 .into(),
302 ),
303 ],
304 [(
305 action_name.into(),
306 json_schema::ActionType {
307 applies_to: Some(json_schema::ApplySpec {
308 principal_types: vec!["foo_type".parse().unwrap()],
309 resource_types: vec!["bar_type".parse().unwrap()],
310 context: json_schema::AttributesOrContext::default(),
311 }),
312 member_of: None,
313 attributes: None,
314 annotations: Annotations::new(),
315 loc: None,
316 #[cfg(feature = "extended-schema")]
317 defn_loc: None,
318 },
319 )],
320 );
321 let schema = schema_file.try_into().unwrap();
322 let validator = Validator::new(schema);
323
324 let policy_a_src = r#"permit(principal in foo_type::"a", action == Action::"actin", resource == bar_type::"b");"#;
325 let policy_a = parser::parse_policy(Some(PolicyID::from_string("pola")), policy_a_src)
326 .expect("Test Policy Should Parse");
327 set.add_static(policy_a)
328 .expect("Policy already present in PolicySet");
329
330 let policy_b_src = r#"permit(principal in foo_tye::"a", action == Action::"action", resource == br_type::"b");"#;
331 let policy_b = parser::parse_policy(Some(PolicyID::from_string("polb")), policy_b_src)
332 .expect("Test Policy Should Parse");
333 set.add_static(policy_b)
334 .expect("Policy already present in PolicySet");
335
336 let result = validator.validate(&set, ValidationMode::default());
337 let principal_err = ValidationError::unrecognized_entity_type(
338 Some(Loc::new(20..27, Arc::from(policy_b_src))),
339 PolicyID::from_string("polb"),
340 "foo_tye".to_string(),
341 Some("foo_type".to_string()),
342 );
343 let resource_err = ValidationError::unrecognized_entity_type(
344 Some(Loc::new(74..81, Arc::from(policy_b_src))),
345 PolicyID::from_string("polb"),
346 "br_type".to_string(),
347 Some("bar_type".to_string()),
348 );
349 let action_err = ValidationError::unrecognized_action_id(
350 Some(Loc::new(45..60, Arc::from(policy_a_src))),
351 PolicyID::from_string("pola"),
352 "Action::\"actin\"".to_string(),
353 Some(UnrecognizedActionIdHelp::SuggestAlternative(
354 "Action::\"action\"".to_string(),
355 )),
356 );
357
358 assert!(!result.validation_passed());
359 assert!(
360 result.validation_errors().contains(&principal_err),
361 "{result:?}"
362 );
363 assert!(
364 result.validation_errors().contains(&resource_err),
365 "{result:?}"
366 );
367 assert!(
368 result.validation_errors().contains(&action_err),
369 "{result:?}"
370 );
371 Ok(())
372 }
373
374 #[test]
375 fn top_level_validate_with_links() -> Result<()> {
376 let mut set = PolicySet::new();
377 let schema: ValidatorSchema = json_schema::Fragment::from_json_str(
378 r#"
379 {
380 "some_namespace": {
381 "entityTypes": {
382 "User": {
383 "shape": {
384 "type": "Record",
385 "attributes": {
386 "department": {
387 "type": "String"
388 },
389 "jobLevel": {
390 "type": "Long"
391 }
392 }
393 },
394 "memberOfTypes": [
395 "UserGroup"
396 ]
397 },
398 "UserGroup": {},
399 "Photo" : {}
400 },
401 "actions": {
402 "view": {
403 "appliesTo": {
404 "resourceTypes": [
405 "Photo"
406 ],
407 "principalTypes": [
408 "User"
409 ]
410 }
411 }
412 }
413 }
414 }
415 "#,
416 )
417 .expect("Schema parse error.")
418 .try_into()
419 .expect("Expected valid schema.");
420 let validator = Validator::new(schema);
421
422 let t = parser::parse_policy_or_template(
423 Some(PolicyID::from_string("template")),
424 r#"permit(principal == some_namespace::User::"Alice", action, resource in ?resource);"#,
425 )
426 .expect("Parse Error");
427 let loc = t.loc().cloned();
428 set.add_template(t)
429 .expect("Template already present in PolicySet");
430
431 let result = validator.validate(&set, ValidationMode::default());
433 assert_eq!(
434 result.validation_errors().collect::<Vec<_>>(),
435 Vec::<&ValidationError>::new()
436 );
437
438 let mut values = HashMap::new();
440 values.insert(
441 ast::SlotId::resource(),
442 ast::EntityUID::from_components(
443 "some_namespace::Photo".parse().unwrap(),
444 ast::Eid::new("foo"),
445 None,
446 ),
447 );
448 set.link(
449 ast::PolicyID::from_string("template"),
450 ast::PolicyID::from_string("link1"),
451 values,
452 )
453 .expect("Linking failed!");
454 let result = validator.validate(&set, ValidationMode::default());
455 assert!(result.validation_passed());
456
457 let mut values = HashMap::new();
459 values.insert(
460 ast::SlotId::resource(),
461 ast::EntityUID::from_components(
462 "some_namespace::Undefined".parse().unwrap(),
463 ast::Eid::new("foo"),
464 None,
465 ),
466 );
467 set.link(
468 ast::PolicyID::from_string("template"),
469 ast::PolicyID::from_string("link2"),
470 values,
471 )
472 .expect("Linking failed!");
473 let result = validator.validate(&set, ValidationMode::default());
474 assert!(!result.validation_passed());
475 assert_eq!(result.validation_errors().count(), 2);
476 let undefined_err = ValidationError::unrecognized_entity_type(
477 None,
478 PolicyID::from_string("link2"),
479 "some_namespace::Undefined".to_string(),
480 Some("some_namespace::User".to_string()),
481 );
482 let invalid_action_err = ValidationError::invalid_action_application(
483 loc.clone(),
484 PolicyID::from_string("link2"),
485 false,
486 false,
487 );
488
489 let actual_undef_error = result
490 .validation_errors()
491 .find(|e| matches!(e, ValidationError::UnrecognizedEntityType(_)))
492 .unwrap();
493 assert_eq!(actual_undef_error, &undefined_err);
494
495 let actual_action_error = result
496 .validation_errors()
497 .find(|e| matches!(e, ValidationError::InvalidActionApplication(_)))
498 .unwrap();
499 assert_eq!(actual_action_error, &invalid_action_err);
500
501 let mut values = HashMap::new();
503 values.insert(
504 ast::SlotId::resource(),
505 ast::EntityUID::from_components(
506 "some_namespace::User".parse().unwrap(),
507 ast::Eid::new("foo"),
508 None,
509 ),
510 );
511 set.link(
512 ast::PolicyID::from_string("template"),
513 ast::PolicyID::from_string("link3"),
514 values,
515 )
516 .expect("Linking failed!");
517 let result = validator.validate(&set, ValidationMode::default());
518 assert!(!result.validation_passed());
519 assert_eq!(result.validation_errors().count(), 3);
521 let invalid_action_err = ValidationError::invalid_action_application(
522 loc,
523 PolicyID::from_string("link3"),
524 false,
525 false,
526 );
527 assert!(result.validation_errors().contains(&invalid_action_err));
528
529 Ok(())
530 }
531
532 #[test]
533 fn validate_finds_warning_and_error() {
534 let schema: ValidatorSchema = json_schema::Fragment::from_json_str(
535 r#"
536 {
537 "": {
538 "entityTypes": {
539 "User": { }
540 },
541 "actions": {
542 "view": {
543 "appliesTo": {
544 "resourceTypes": [ "User" ],
545 "principalTypes": [ "User" ]
546 }
547 }
548 }
549 }
550 }
551 "#,
552 )
553 .expect("Schema parse error.")
554 .try_into()
555 .expect("Expected valid schema.");
556 let validator = Validator::new(schema);
557
558 let mut set = PolicySet::new();
559 let src = r#"permit(principal == User::"าปenry", action, resource) when {1 > true};"#;
560 let p = parser::parse_policy(None, src).unwrap();
561 set.add_static(p).unwrap();
562
563 let result = validator.validate(&set, ValidationMode::default());
564 assert_eq!(
565 result.validation_errors().collect::<Vec<_>>(),
566 vec![&ValidationError::expected_type(
567 typecheck::test::test_utils::get_loc(src, "true"),
568 PolicyID::from_string("policy0"),
569 Type::primitive_long(),
570 Type::singleton_boolean(true),
571 None,
572 )]
573 );
574 assert_eq!(
575 result.validation_warnings().collect::<Vec<_>>(),
576 vec![&ValidationWarning::mixed_script_identifier(
577 None,
578 PolicyID::from_string("policy0"),
579 "าปenry"
580 )]
581 );
582 }
583}
584
585#[cfg(test)]
586#[expect(clippy::cognitive_complexity, reason = "unit test code")]
587mod enumerated_entity_types {
588 use std::collections::HashMap;
589
590 use crate::{
591 ast::{Eid, EntityUID, ExprBuilder, PolicyID, PolicySet, SlotId, Template},
592 expr_builder::ExprBuilder as _,
593 extensions::Extensions,
594 parser::parse_policy_or_template,
595 };
596 use cool_asserts::assert_matches;
597 use itertools::Itertools;
598 use nonempty::nonempty;
599
600 use crate::validator::{
601 typecheck::test::test_utils::get_loc,
602 types::{EntityLUB, Type},
603 validation_errors::AttributeAccess,
604 ValidationError, ValidationWarning, Validator, ValidatorSchema,
605 };
606
607 #[track_caller]
608 fn schema() -> ValidatorSchema {
609 ValidatorSchema::from_json_value(
610 serde_json::json!(
611 {
612 "": { "entityTypes": {
613 "Foo": {
614 "enum": [ "foo" ],
615 },
616 "Bar": {
617 "memberOfTypes": ["Foo"],
618 },
619 "Other": { },
620 },
621 "actions": {
622 "a": {
623 "appliesTo": {
624 "principalTypes": ["Foo"],
625 "resourceTypes": ["Bar"],
626 }
627 }
628 }
629 }
630 }
631 ),
632 Extensions::none(),
633 )
634 .unwrap()
635 }
636
637 #[test]
638 fn basic() {
639 let schema = schema();
640 let template = parse_policy_or_template(None, r#"permit(principal, action == Action::"a", resource) when { principal == Foo::"foo" };"#).unwrap();
641 let validator = Validator::new(schema);
642 let (errors, warnings) =
643 validator.validate_policy(&template, crate::validator::ValidationMode::Strict);
644 assert!(warnings.collect_vec().is_empty());
645 assert!(errors.collect_vec().is_empty());
646 }
647
648 #[test]
649 fn link() {
650 let schema = schema();
651 let template = parse_policy_or_template(
652 None,
653 r#"permit(principal in ?principal, action == Action::"a", resource);"#,
654 )
655 .unwrap();
656 let policy = Template::link(
657 std::sync::Arc::new(template),
658 PolicyID::from_string("test"),
659 HashMap::from_iter([(SlotId::principal(), r#"Other::"foo""#.parse().unwrap())]),
660 )
661 .unwrap();
662 let mut policy_set = PolicySet::new();
663 let _ = policy_set.add(policy);
664 let validator = Validator::new(schema);
665 let result = validator.validate(&policy_set, crate::validator::ValidationMode::Strict);
666
667 assert_eq!(result.validation_errors().collect_vec().len(), 1);
668 }
669
670 #[test]
671 fn basic_invalid() {
672 let schema = schema();
673 let template = parse_policy_or_template(None, r#"permit(principal, action == Action::"a", resource) when { principal == Foo::"fo" };"#).unwrap();
674 let validator = Validator::new(schema.clone());
675 let (errors, warnings) =
676 validator.validate_policy(&template, crate::validator::ValidationMode::Strict);
677 assert!(warnings.collect_vec().is_empty());
678 assert_matches!(&errors.collect_vec(), [ValidationError::InvalidEnumEntity(err)] => {
679 assert_eq!(err.err.choices, nonempty![Eid::new("foo")]);
680 assert_eq!(err.err.uid, EntityUID::with_eid_and_type("Foo", "fo").unwrap());
681 });
682
683 let template = parse_policy_or_template(
684 None,
685 r#"permit(principal == Foo::"๐", action == Action::"a", resource);"#,
686 )
687 .unwrap();
688 let validator = Validator::new(schema.clone());
689 let (errors, warnings) =
690 validator.validate_policy(&template, crate::validator::ValidationMode::Strict);
691 assert!(warnings.collect_vec().is_empty());
692 assert_matches!(&errors.collect_vec(), [ValidationError::InvalidEnumEntity(err)] => {
693 assert_eq!(err.err.choices, nonempty![Eid::new("foo")]);
694 assert_eq!(err.err.uid, EntityUID::with_eid_and_type("Foo", "๐").unwrap());
695 });
696
697 let template = parse_policy_or_template(
698 None,
699 r#"permit(principal in Foo::"๐", action == Action::"a", resource);"#,
700 )
701 .unwrap();
702 let validator = Validator::new(schema.clone());
703 let (errors, warnings) =
704 validator.validate_policy(&template, crate::validator::ValidationMode::Strict);
705 assert!(warnings.collect_vec().is_empty());
706 assert_matches!(&errors.collect_vec(), [ValidationError::InvalidEnumEntity(err)] => {
707 assert_eq!(err.err.choices, nonempty![Eid::new("foo")]);
708 assert_eq!(err.err.uid, EntityUID::with_eid_and_type("Foo", "๐").unwrap());
709 });
710
711 let template = parse_policy_or_template(
712 None,
713 r#"permit(principal, action == Action::"a", resource)
714 when { {"๐": Foo::"๐"} has "๐" };
715 "#,
716 )
717 .unwrap();
718 let validator = Validator::new(schema.clone());
719 let (errors, warnings) =
720 validator.validate_policy(&template, crate::validator::ValidationMode::Strict);
721 assert!(warnings.collect_vec().is_empty());
722 assert_matches!(&errors.collect_vec(), [ValidationError::InvalidEnumEntity(err)] => {
723 assert_eq!(err.err.choices, nonempty![Eid::new("foo")]);
724 assert_eq!(err.err.uid, EntityUID::with_eid_and_type("Foo", "๐").unwrap());
725 });
726
727 let template = parse_policy_or_template(
728 None,
729 r#"permit(principal, action == Action::"a", resource)
730 when { [Foo::"๐"].isEmpty() };
731 "#,
732 )
733 .unwrap();
734 let validator = Validator::new(schema.clone());
735 let (errors, warnings) =
736 validator.validate_policy(&template, crate::validator::ValidationMode::Strict);
737 assert!(warnings.collect_vec().is_empty());
738 assert_matches!(&errors.collect_vec(), [ValidationError::InvalidEnumEntity(err)] => {
739 assert_eq!(err.err.choices, nonempty![Eid::new("foo")]);
740 assert_eq!(err.err.uid, EntityUID::with_eid_and_type("Foo", "๐").unwrap());
741 });
742
743 let template = parse_policy_or_template(
744 None,
745 r#"permit(principal, action == Action::"a", resource)
746 when { [{"๐": Foo::"๐"}].isEmpty() };
747 "#,
748 )
749 .unwrap();
750 let validator = Validator::new(schema);
751 let (errors, warnings) =
752 validator.validate_policy(&template, crate::validator::ValidationMode::Strict);
753 assert!(warnings.collect_vec().is_empty());
754 assert_matches!(&errors.collect_vec(), [ValidationError::InvalidEnumEntity(err)] => {
755 assert_eq!(err.err.choices, nonempty![Eid::new("foo")]);
756 assert_eq!(err.err.uid, EntityUID::with_eid_and_type("Foo", "๐").unwrap());
757 });
758 }
759
760 #[test]
761 fn no_attrs_allowed() {
762 let schema = schema();
763 let src = r#"permit(principal, action == Action::"a", resource) when { principal.foo == "foo" };"#;
764 let template = parse_policy_or_template(None, src).unwrap();
765 let validator = Validator::new(schema);
766 let (errors, warnings) =
767 validator.validate_policy(&template, crate::validator::ValidationMode::Strict);
768 assert!(warnings.collect_vec().is_empty());
769 assert_eq!(
770 errors.collect_vec(),
771 [ValidationError::unsafe_attribute_access(
772 get_loc(src, "principal.foo"),
773 PolicyID::from_string("policy0"),
774 AttributeAccess::EntityLUB(
775 EntityLUB::single_entity("Foo".parse().unwrap()),
776 vec!["foo".into()],
777 ),
778 None,
779 false,
780 )]
781 );
782 }
783
784 #[test]
785 fn no_ancestors() {
786 let schema = schema();
787 let src = r#"permit(principal, action == Action::"a", resource) when { principal in Bar::"bar" };"#;
788 let template = parse_policy_or_template(None, src).unwrap();
789 let validator = Validator::new(schema);
790 let (errors, warnings) =
791 validator.validate_policy(&template, crate::validator::ValidationMode::Strict);
792 assert_eq!(
793 warnings.collect_vec(),
794 [ValidationWarning::impossible_policy(
795 get_loc(src, src),
796 PolicyID::from_string("policy0")
797 )]
798 );
799 assert!(errors.collect_vec().is_empty());
800 }
801
802 #[test]
803 fn no_tags_allowed() {
804 let schema = schema();
805 let src = r#"permit(principal, action == Action::"a", resource) when { principal.getTag("foo") == "foo" };"#;
806 let template = parse_policy_or_template(None, src).unwrap();
807 let validator = Validator::new(schema);
808 let (errors, warnings) =
809 validator.validate_policy(&template, crate::validator::ValidationMode::Strict);
810 assert!(warnings.collect_vec().is_empty());
811 assert_eq!(
812 errors.collect_vec(),
813 [ValidationError::unsafe_tag_access(
814 get_loc(src, r#"principal.getTag("foo")"#),
815 PolicyID::from_string("policy0"),
816 Some(EntityLUB::single_entity("Foo".parse().unwrap()),),
817 {
818 let builder = ExprBuilder::new();
819 let mut expr = builder.val("foo");
820 expr.set_data(Some(Type::primitive_string()));
821 expr
822 },
823 )]
824 );
825 }
826}