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