1use crate::{ValidatorActionId, ValidatorEntityType, ValidatorSchema};
17use cedar_policy_core::ast::{EntityType, EntityUID};
18use cedar_policy_core::extensions::{ExtensionFunctionLookupError, Extensions};
19use cedar_policy_core::{ast, entities};
20use miette::Diagnostic;
21use smol_str::SmolStr;
22use std::collections::hash_map::Values;
23use std::collections::HashSet;
24use std::iter::Cloned;
25use std::sync::Arc;
26use thiserror::Error;
27
28#[derive(Debug)]
30pub struct CoreSchema<'a> {
31 schema: &'a ValidatorSchema,
33}
34
35impl<'a> CoreSchema<'a> {
36 pub fn new(schema: &'a ValidatorSchema) -> Self {
38 Self { schema }
39 }
40}
41
42impl<'a> entities::Schema for CoreSchema<'a> {
43 type EntityTypeDescription = EntityTypeDescription;
44 type ActionEntityIterator = Cloned<Values<'a, ast::EntityUID, Arc<ast::Entity>>>;
45
46 fn entity_type(&self, entity_type: &ast::EntityType) -> Option<EntityTypeDescription> {
47 EntityTypeDescription::new(self.schema, entity_type)
48 }
49
50 fn action(&self, action: &ast::EntityUID) -> Option<Arc<ast::Entity>> {
51 self.schema.actions.get(action).cloned()
52 }
53
54 fn entity_types_with_basename<'b>(
55 &'b self,
56 basename: &'b ast::UnreservedId,
57 ) -> Box<dyn Iterator<Item = ast::EntityType> + 'b> {
58 Box::new(self.schema.entity_types().filter_map(move |entity_type| {
59 if &entity_type.name().as_ref().basename() == basename {
60 Some(entity_type.name().clone())
61 } else {
62 None
63 }
64 }))
65 }
66
67 fn action_entities(&self) -> Self::ActionEntityIterator {
68 self.schema.actions.values().cloned()
69 }
70}
71
72#[derive(Debug)]
74pub struct EntityTypeDescription {
75 core_type: ast::EntityType,
77 validator_type: ValidatorEntityType,
79 allowed_parent_types: Arc<HashSet<ast::EntityType>>,
82}
83
84impl EntityTypeDescription {
85 pub fn new(schema: &ValidatorSchema, type_name: &ast::EntityType) -> Option<Self> {
88 Some(Self {
89 core_type: type_name.clone(),
90 validator_type: schema.get_entity_type(type_name).cloned()?,
91 allowed_parent_types: {
92 let mut set = HashSet::new();
93 for possible_parent_et in schema.entity_types() {
94 if possible_parent_et.descendants.contains(type_name) {
95 set.insert(possible_parent_et.name().clone());
96 }
97 }
98 Arc::new(set)
99 },
100 })
101 }
102}
103
104impl entities::EntityTypeDescription for EntityTypeDescription {
105 fn entity_type(&self) -> ast::EntityType {
106 self.core_type.clone()
107 }
108
109 fn attr_type(&self, attr: &str) -> Option<entities::SchemaType> {
110 let attr_type: &crate::types::Type = &self.validator_type.attr(attr)?.attr_type;
111 #[allow(clippy::expect_used)]
116 let core_schema_type: entities::SchemaType = attr_type
117 .clone()
118 .try_into()
119 .expect("failed to convert validator type into Core SchemaType");
120 debug_assert!(attr_type.is_consistent_with(&core_schema_type));
121 Some(core_schema_type)
122 }
123
124 fn tag_type(&self) -> Option<entities::SchemaType> {
125 let tag_type: &crate::types::Type = self.validator_type.tag_type()?;
126 #[allow(clippy::expect_used)]
131 let core_schema_type: entities::SchemaType = tag_type
132 .clone()
133 .try_into()
134 .expect("failed to convert validator type into Core SchemaType");
135 debug_assert!(tag_type.is_consistent_with(&core_schema_type));
136 Some(core_schema_type)
137 }
138
139 fn required_attrs<'s>(&'s self) -> Box<dyn Iterator<Item = SmolStr> + 's> {
140 Box::new(
141 self.validator_type
142 .attributes()
143 .iter()
144 .filter(|(_, ty)| ty.is_required)
145 .map(|(attr, _)| attr.clone()),
146 )
147 }
148
149 fn allowed_parent_types(&self) -> Arc<HashSet<ast::EntityType>> {
150 Arc::clone(&self.allowed_parent_types)
151 }
152
153 fn open_attributes(&self) -> bool {
154 self.validator_type.open_attributes.is_open()
155 }
156}
157
158impl ast::RequestSchema for ValidatorSchema {
159 type Error = RequestValidationError;
160 fn validate_request(
161 &self,
162 request: &ast::Request,
163 extensions: &Extensions<'_>,
164 ) -> std::result::Result<(), Self::Error> {
165 use ast::EntityUIDEntry;
166 if let Some(principal_type) = request.principal().get_type() {
169 if self.get_entity_type(principal_type).is_none() {
170 return Err(request_validation_errors::UndeclaredPrincipalTypeError {
171 principal_ty: principal_type.clone(),
172 }
173 .into());
174 }
175 }
176 if let Some(resource_type) = request.resource().get_type() {
177 if self.get_entity_type(resource_type).is_none() {
178 return Err(request_validation_errors::UndeclaredResourceTypeError {
179 resource_ty: resource_type.clone(),
180 }
181 .into());
182 }
183 }
184
185 match request.action() {
187 EntityUIDEntry::Known { euid: action, .. } => {
188 let validator_action_id = self.get_action_id(action).ok_or_else(|| {
189 request_validation_errors::UndeclaredActionError {
190 action: Arc::clone(action),
191 }
192 })?;
193 if let Some(principal_type) = request.principal().get_type() {
194 validator_action_id.check_principal_type(principal_type, action)?;
195 }
196 if let Some(principal_type) = request.resource().get_type() {
197 validator_action_id.check_resource_type(principal_type, action)?;
198 }
199 if let Some(context) = request.context() {
200 let expected_context_ty = validator_action_id.context_type();
201 if !expected_context_ty
202 .typecheck_partial_value(&context.clone().into(), extensions)
203 .map_err(RequestValidationError::TypeOfContext)?
204 {
205 return Err(request_validation_errors::InvalidContextError {
206 context: context.clone(),
207 action: Arc::clone(action),
208 }
209 .into());
210 }
211 }
212 }
213 EntityUIDEntry::Unknown { .. } => {
214 }
221 }
222 Ok(())
223 }
224}
225
226impl ValidatorActionId {
227 fn check_principal_type(
228 &self,
229 principal_type: &EntityType,
230 action: &Arc<EntityUID>,
231 ) -> Result<(), request_validation_errors::InvalidPrincipalTypeError> {
232 if !self.is_applicable_principal_type(principal_type) {
233 Err(request_validation_errors::InvalidPrincipalTypeError {
234 principal_ty: principal_type.clone(),
235 action: Arc::clone(action),
236 valid_principal_tys: self.applies_to_principals().cloned().collect(),
237 })
238 } else {
239 Ok(())
240 }
241 }
242
243 fn check_resource_type(
244 &self,
245 resource_type: &EntityType,
246 action: &Arc<EntityUID>,
247 ) -> Result<(), request_validation_errors::InvalidResourceTypeError> {
248 if !self.is_applicable_resource_type(resource_type) {
249 Err(request_validation_errors::InvalidResourceTypeError {
250 resource_ty: resource_type.clone(),
251 action: Arc::clone(action),
252 valid_resource_tys: self.applies_to_resources().cloned().collect(),
253 })
254 } else {
255 Ok(())
256 }
257 }
258}
259
260impl ast::RequestSchema for CoreSchema<'_> {
261 type Error = RequestValidationError;
262 fn validate_request(
263 &self,
264 request: &ast::Request,
265 extensions: &Extensions<'_>,
266 ) -> Result<(), Self::Error> {
267 self.schema.validate_request(request, extensions)
268 }
269}
270
271#[derive(Debug, Diagnostic, Error)]
275pub enum RequestValidationError {
276 #[error(transparent)]
278 #[diagnostic(transparent)]
279 UndeclaredAction(#[from] request_validation_errors::UndeclaredActionError),
280 #[error(transparent)]
282 #[diagnostic(transparent)]
283 UndeclaredPrincipalType(#[from] request_validation_errors::UndeclaredPrincipalTypeError),
284 #[error(transparent)]
286 #[diagnostic(transparent)]
287 UndeclaredResourceType(#[from] request_validation_errors::UndeclaredResourceTypeError),
288 #[error(transparent)]
291 #[diagnostic(transparent)]
292 InvalidPrincipalType(#[from] request_validation_errors::InvalidPrincipalTypeError),
293 #[error(transparent)]
296 #[diagnostic(transparent)]
297 InvalidResourceType(#[from] request_validation_errors::InvalidResourceTypeError),
298 #[error(transparent)]
300 #[diagnostic(transparent)]
301 InvalidContext(#[from] request_validation_errors::InvalidContextError),
302 #[error("context is not valid: {0}")]
305 #[diagnostic(transparent)]
306 TypeOfContext(ExtensionFunctionLookupError),
307}
308
309pub mod request_validation_errors {
311 use cedar_policy_core::ast;
312 use cedar_policy_core::impl_diagnostic_from_method_on_field;
313 use itertools::Itertools;
314 use miette::Diagnostic;
315 use std::sync::Arc;
316 use thiserror::Error;
317
318 #[derive(Debug, Error)]
320 #[error("request's action `{action}` is not declared in the schema")]
321 pub struct UndeclaredActionError {
322 pub(super) action: Arc<ast::EntityUID>,
324 }
325
326 impl Diagnostic for UndeclaredActionError {
327 impl_diagnostic_from_method_on_field!(action, loc);
328 }
329
330 impl UndeclaredActionError {
331 pub fn action(&self) -> &ast::EntityUID {
333 &self.action
334 }
335 }
336
337 #[derive(Debug, Error)]
339 #[error("principal type `{principal_ty}` is not declared in the schema")]
340 pub struct UndeclaredPrincipalTypeError {
341 pub(super) principal_ty: ast::EntityType,
343 }
344
345 impl Diagnostic for UndeclaredPrincipalTypeError {
346 impl_diagnostic_from_method_on_field!(principal_ty, loc);
347 }
348
349 impl UndeclaredPrincipalTypeError {
350 pub fn principal_ty(&self) -> &ast::EntityType {
352 &self.principal_ty
353 }
354 }
355
356 #[derive(Debug, Error)]
358 #[error("resource type `{resource_ty}` is not declared in the schema")]
359 pub struct UndeclaredResourceTypeError {
360 pub(super) resource_ty: ast::EntityType,
362 }
363
364 impl Diagnostic for UndeclaredResourceTypeError {
365 impl_diagnostic_from_method_on_field!(resource_ty, loc);
366 }
367
368 impl UndeclaredResourceTypeError {
369 pub fn resource_ty(&self) -> &ast::EntityType {
371 &self.resource_ty
372 }
373 }
374
375 #[derive(Debug, Error)]
378 #[error("principal type `{principal_ty}` is not valid for `{action}`")]
379 pub struct InvalidPrincipalTypeError {
380 pub(super) principal_ty: ast::EntityType,
382 pub(super) action: Arc<ast::EntityUID>,
384 pub(super) valid_principal_tys: Vec<ast::EntityType>,
386 }
387
388 impl Diagnostic for InvalidPrincipalTypeError {
389 fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
390 Some(Box::new(invalid_principal_type_help(
391 &self.valid_principal_tys,
392 &self.action,
393 )))
394 }
395
396 impl_diagnostic_from_method_on_field!(principal_ty, loc);
399 }
400
401 fn invalid_principal_type_help(
402 valid_principal_tys: &[ast::EntityType],
403 action: &ast::EntityUID,
404 ) -> String {
405 if valid_principal_tys.is_empty() {
406 format!("no principal types are valid for `{action}`")
407 } else {
408 format!(
409 "valid principal types for `{action}`: {}",
410 valid_principal_tys
411 .iter()
412 .sorted_unstable()
413 .map(|et| format!("`{et}`"))
414 .join(", ")
415 )
416 }
417 }
418
419 impl InvalidPrincipalTypeError {
420 pub fn principal_ty(&self) -> &ast::EntityType {
422 &self.principal_ty
423 }
424
425 pub fn action(&self) -> &ast::EntityUID {
427 &self.action
428 }
429
430 pub fn valid_principal_tys(&self) -> impl Iterator<Item = &ast::EntityType> {
432 self.valid_principal_tys.iter()
433 }
434 }
435
436 #[derive(Debug, Error)]
439 #[error("resource type `{resource_ty}` is not valid for `{action}`")]
440 pub struct InvalidResourceTypeError {
441 pub(super) resource_ty: ast::EntityType,
443 pub(super) action: Arc<ast::EntityUID>,
445 pub(super) valid_resource_tys: Vec<ast::EntityType>,
447 }
448
449 impl Diagnostic for InvalidResourceTypeError {
450 fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
451 Some(Box::new(invalid_resource_type_help(
452 &self.valid_resource_tys,
453 &self.action,
454 )))
455 }
456
457 impl_diagnostic_from_method_on_field!(resource_ty, loc);
460 }
461
462 fn invalid_resource_type_help(
463 valid_resource_tys: &[ast::EntityType],
464 action: &ast::EntityUID,
465 ) -> String {
466 if valid_resource_tys.is_empty() {
467 format!("no resource types are valid for `{action}`")
468 } else {
469 format!(
470 "valid resource types for `{action}`: {}",
471 valid_resource_tys
472 .iter()
473 .sorted_unstable()
474 .map(|et| format!("`{et}`"))
475 .join(", ")
476 )
477 }
478 }
479
480 impl InvalidResourceTypeError {
481 pub fn resource_ty(&self) -> &ast::EntityType {
483 &self.resource_ty
484 }
485
486 pub fn action(&self) -> &ast::EntityUID {
488 &self.action
489 }
490
491 pub fn valid_resource_tys(&self) -> impl Iterator<Item = &ast::EntityType> {
493 self.valid_resource_tys.iter()
494 }
495 }
496
497 #[derive(Debug, Error, Diagnostic)]
499 #[error("context `{}` is not valid for `{action}`", ast::BoundedToString::to_string_bounded(.context, BOUNDEDDISPLAY_BOUND_FOR_INVALID_CONTEXT_ERROR))]
500 pub struct InvalidContextError {
501 pub(super) context: ast::Context,
503 pub(super) action: Arc<ast::EntityUID>,
505 }
506
507 const BOUNDEDDISPLAY_BOUND_FOR_INVALID_CONTEXT_ERROR: usize = 5;
508
509 impl InvalidContextError {
510 pub fn context(&self) -> &ast::Context {
512 &self.context
513 }
514
515 pub fn action(&self) -> &ast::EntityUID {
517 &self.action
518 }
519 }
520}
521
522#[derive(Debug, Clone, PartialEq, Eq)]
525pub struct ContextSchema(
526 crate::types::Type,
529);
530
531impl entities::ContextSchema for ContextSchema {
533 fn context_type(&self) -> entities::SchemaType {
534 #[allow(clippy::expect_used)]
536 self.0
537 .clone()
538 .try_into()
539 .expect("failed to convert validator type into Core SchemaType")
540 }
541}
542
543pub fn context_schema_for_action(
548 schema: &ValidatorSchema,
549 action: &ast::EntityUID,
550) -> Option<ContextSchema> {
551 schema.context_type(action).cloned().map(ContextSchema)
558}
559
560#[cfg(test)]
561mod test {
562 use super::*;
563 use cedar_policy_core::test_utils::{expect_err, ExpectedErrorMessageBuilder};
564 use cool_asserts::assert_matches;
565 use serde_json::json;
566
567 fn schema() -> ValidatorSchema {
568 let src = json!(
569 { "": {
570 "entityTypes": {
571 "User": {
572 "memberOfTypes": [ "Group" ]
573 },
574 "Group": {
575 "memberOfTypes": []
576 },
577 "Photo": {
578 "memberOfTypes": [ "Album" ]
579 },
580 "Album": {
581 "memberOfTypes": []
582 }
583 },
584 "actions": {
585 "view_photo": {
586 "appliesTo": {
587 "principalTypes": ["User", "Group"],
588 "resourceTypes": ["Photo"]
589 }
590 },
591 "edit_photo": {
592 "appliesTo": {
593 "principalTypes": ["User", "Group"],
594 "resourceTypes": ["Photo"],
595 "context": {
596 "type": "Record",
597 "attributes": {
598 "admin_approval": {
599 "type": "Boolean",
600 "required": true,
601 }
602 }
603 }
604 }
605 }
606 }
607 }});
608 ValidatorSchema::from_json_value(src, Extensions::all_available())
609 .expect("failed to create ValidatorSchema")
610 }
611
612 #[test]
614 fn success_concrete_request_no_context() {
615 assert_matches!(
616 ast::Request::new(
617 (
618 ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
619 None
620 ),
621 (
622 ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(),
623 None
624 ),
625 (
626 ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
627 None
628 ),
629 ast::Context::empty(),
630 Some(&schema()),
631 Extensions::all_available(),
632 ),
633 Ok(_)
634 );
635 }
636
637 #[test]
639 fn success_concrete_request_with_context() {
640 assert_matches!(
641 ast::Request::new(
642 (
643 ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
644 None
645 ),
646 (
647 ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(),
648 None
649 ),
650 (
651 ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
652 None
653 ),
654 ast::Context::from_pairs(
655 [("admin_approval".into(), ast::RestrictedExpr::val(true))],
656 Extensions::all_available()
657 )
658 .unwrap(),
659 Some(&schema()),
660 Extensions::all_available(),
661 ),
662 Ok(_)
663 );
664 }
665
666 #[test]
668 fn success_principal_unknown() {
669 assert_matches!(
670 ast::Request::new_with_unknowns(
671 ast::EntityUIDEntry::unknown(),
672 ast::EntityUIDEntry::known(
673 ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(),
674 None,
675 ),
676 ast::EntityUIDEntry::known(
677 ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
678 None,
679 ),
680 Some(ast::Context::empty()),
681 Some(&schema()),
682 Extensions::all_available(),
683 ),
684 Ok(_)
685 );
686 }
687
688 #[test]
690 fn success_action_unknown() {
691 assert_matches!(
692 ast::Request::new_with_unknowns(
693 ast::EntityUIDEntry::known(
694 ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
695 None,
696 ),
697 ast::EntityUIDEntry::unknown(),
698 ast::EntityUIDEntry::known(
699 ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
700 None,
701 ),
702 Some(ast::Context::empty()),
703 Some(&schema()),
704 Extensions::all_available(),
705 ),
706 Ok(_)
707 );
708 }
709
710 #[test]
712 fn success_resource_unknown() {
713 assert_matches!(
714 ast::Request::new_with_unknowns(
715 ast::EntityUIDEntry::known(
716 ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
717 None,
718 ),
719 ast::EntityUIDEntry::known(
720 ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(),
721 None,
722 ),
723 ast::EntityUIDEntry::unknown(),
724 Some(ast::Context::empty()),
725 Some(&schema()),
726 Extensions::all_available(),
727 ),
728 Ok(_)
729 );
730 }
731
732 #[test]
734 fn success_context_unknown() {
735 assert_matches!(
736 ast::Request::new_with_unknowns(
737 ast::EntityUIDEntry::known(
738 ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
739 None,
740 ),
741 ast::EntityUIDEntry::known(
742 ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(),
743 None,
744 ),
745 ast::EntityUIDEntry::known(
746 ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
747 None,
748 ),
749 None,
750 Some(&schema()),
751 Extensions::all_available(),
752 ),
753 Ok(_)
754 )
755 }
756
757 #[test]
759 fn success_everything_unspecified() {
760 assert_matches!(
761 ast::Request::new_with_unknowns(
762 ast::EntityUIDEntry::unknown(),
763 ast::EntityUIDEntry::unknown(),
764 ast::EntityUIDEntry::unknown(),
765 None,
766 Some(&schema()),
767 Extensions::all_available(),
768 ),
769 Ok(_)
770 );
771 }
772
773 #[test]
777 fn success_unknown_action_but_invalid_types() {
778 assert_matches!(
779 ast::Request::new_with_unknowns(
780 ast::EntityUIDEntry::known(
781 ast::EntityUID::with_eid_and_type("Album", "abc123").unwrap(),
782 None,
783 ),
784 ast::EntityUIDEntry::unknown(),
785 ast::EntityUIDEntry::known(
786 ast::EntityUID::with_eid_and_type("User", "alice").unwrap(),
787 None,
788 ),
789 None,
790 Some(&schema()),
791 Extensions::all_available(),
792 ),
793 Ok(_)
794 );
795 }
796
797 #[test]
799 fn action_not_declared() {
800 assert_matches!(
801 ast::Request::new(
802 (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
803 (ast::EntityUID::with_eid_and_type("Action", "destroy").unwrap(), None),
804 (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
805 ast::Context::empty(),
806 Some(&schema()),
807 Extensions::all_available(),
808 ),
809 Err(e) => {
810 expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(r#"request's action `Action::"destroy"` is not declared in the schema"#).build());
811 }
812 );
813 }
814
815 #[test]
817 fn principal_type_not_declared() {
818 assert_matches!(
819 ast::Request::new(
820 (ast::EntityUID::with_eid_and_type("Foo", "abc123").unwrap(), None),
821 (ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(), None),
822 (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
823 ast::Context::empty(),
824 Some(&schema()),
825 Extensions::all_available(),
826 ),
827 Err(e) => {
828 expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error("principal type `Foo` is not declared in the schema").build());
829 }
830 );
831 }
832
833 #[test]
835 fn resource_type_not_declared() {
836 assert_matches!(
837 ast::Request::new(
838 (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
839 (ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(), None),
840 (ast::EntityUID::with_eid_and_type("Foo", "vacationphoto94.jpg").unwrap(), None),
841 ast::Context::empty(),
842 Some(&schema()),
843 Extensions::all_available(),
844 ),
845 Err(e) => {
846 expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error("resource type `Foo` is not declared in the schema").build());
847 }
848 );
849 }
850
851 #[test]
853 fn principal_type_invalid() {
854 assert_matches!(
855 ast::Request::new(
856 (ast::EntityUID::with_eid_and_type("Album", "abc123").unwrap(), None),
857 (ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(), None),
858 (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
859 ast::Context::empty(),
860 Some(&schema()),
861 Extensions::all_available(),
862 ),
863 Err(e) => {
864 expect_err(
865 "",
866 &miette::Report::new(e),
867 &ExpectedErrorMessageBuilder::error(r#"principal type `Album` is not valid for `Action::"view_photo"`"#)
868 .help(r#"valid principal types for `Action::"view_photo"`: `Group`, `User`"#)
869 .build(),
870 );
871 }
872 );
873 }
874
875 #[test]
877 fn resource_type_invalid() {
878 assert_matches!(
879 ast::Request::new(
880 (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
881 (ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(), None),
882 (ast::EntityUID::with_eid_and_type("Group", "coders").unwrap(), None),
883 ast::Context::empty(),
884 Some(&schema()),
885 Extensions::all_available(),
886 ),
887 Err(e) => {
888 expect_err(
889 "",
890 &miette::Report::new(e),
891 &ExpectedErrorMessageBuilder::error(r#"resource type `Group` is not valid for `Action::"view_photo"`"#)
892 .help(r#"valid resource types for `Action::"view_photo"`: `Photo`"#)
893 .build(),
894 );
895 }
896 );
897 }
898
899 #[test]
901 fn context_missing_attribute() {
902 assert_matches!(
903 ast::Request::new(
904 (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
905 (ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(), None),
906 (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
907 ast::Context::empty(),
908 Some(&schema()),
909 Extensions::all_available(),
910 ),
911 Err(e) => {
912 expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(r#"context `{}` is not valid for `Action::"edit_photo"`"#).build());
913 }
914 );
915 }
916
917 #[test]
919 fn context_extra_attribute() {
920 let context_with_extra_attr = ast::Context::from_pairs(
921 [
922 ("admin_approval".into(), ast::RestrictedExpr::val(true)),
923 ("extra".into(), ast::RestrictedExpr::val(42)),
924 ],
925 Extensions::all_available(),
926 )
927 .unwrap();
928 assert_matches!(
929 ast::Request::new(
930 (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
931 (ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(), None),
932 (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
933 context_with_extra_attr,
934 Some(&schema()),
935 Extensions::all_available(),
936 ),
937 Err(e) => {
938 expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(r#"context `{admin_approval: true, extra: 42}` is not valid for `Action::"edit_photo"`"#).build());
939 }
940 );
941 }
942
943 #[test]
945 fn context_attribute_wrong_type() {
946 let context_with_wrong_type_attr = ast::Context::from_pairs(
947 [(
948 "admin_approval".into(),
949 ast::RestrictedExpr::set([ast::RestrictedExpr::val(true)]),
950 )],
951 Extensions::all_available(),
952 )
953 .unwrap();
954 assert_matches!(
955 ast::Request::new(
956 (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
957 (ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(), None),
958 (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
959 context_with_wrong_type_attr,
960 Some(&schema()),
961 Extensions::all_available(),
962 ),
963 Err(e) => {
964 expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(r#"context `{admin_approval: [true]}` is not valid for `Action::"edit_photo"`"#).build());
965 }
966 );
967 }
968
969 #[test]
971 fn context_attribute_heterogeneous_set() {
972 let context_with_heterogeneous_set = ast::Context::from_pairs(
973 [(
974 "admin_approval".into(),
975 ast::RestrictedExpr::set([
976 ast::RestrictedExpr::val(true),
977 ast::RestrictedExpr::val(-1001),
978 ]),
979 )],
980 Extensions::all_available(),
981 )
982 .unwrap();
983 assert_matches!(
984 ast::Request::new(
985 (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
986 (ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(), None),
987 (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
988 context_with_heterogeneous_set,
989 Some(&schema()),
990 Extensions::all_available(),
991 ),
992 Err(e) => {
993 expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(r#"context `{admin_approval: [true, -1001]}` is not valid for `Action::"edit_photo"`"#).build());
994 }
995 );
996 }
997
998 #[test]
1000 fn context_large() {
1001 let large_context_with_extra_attributes = ast::Context::from_pairs(
1002 [
1003 ("admin_approval".into(), ast::RestrictedExpr::val(true)),
1004 ("extra1".into(), ast::RestrictedExpr::val(false)),
1005 ("also extra".into(), ast::RestrictedExpr::val("spam")),
1006 (
1007 "extra2".into(),
1008 ast::RestrictedExpr::set([ast::RestrictedExpr::val(-100)]),
1009 ),
1010 (
1011 "extra3".into(),
1012 ast::RestrictedExpr::val(
1013 ast::EntityUID::with_eid_and_type("User", "alice").unwrap(),
1014 ),
1015 ),
1016 ("extra4".into(), ast::RestrictedExpr::val("foobar")),
1017 ],
1018 Extensions::all_available(),
1019 )
1020 .unwrap();
1021 assert_matches!(
1022 ast::Request::new(
1023 (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
1024 (ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(), None),
1025 (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
1026 large_context_with_extra_attributes,
1027 Some(&schema()),
1028 Extensions::all_available(),
1029 ),
1030 Err(e) => {
1031 expect_err(
1032 "",
1033 &miette::Report::new(e),
1034 &ExpectedErrorMessageBuilder::error(r#"context `{admin_approval: true, "also extra": "spam", extra1: false, extra2: [-100], extra3: User::"alice", .. }` is not valid for `Action::"edit_photo"`"#).build(),
1035 );
1036 }
1037 );
1038 }
1039}