1use crate::{ValidatorActionId, ValidatorEntityType, ValidatorEntityTypeKind, ValidatorSchema};
17use cedar_policy_core::ast::{Eid, EntityType, EntityUID};
18use cedar_policy_core::entities::conformance::err::InvalidEnumEntityError;
19use cedar_policy_core::entities::conformance::{
20 is_valid_enumerated_entity, validate_euids_in_partial_value,
21};
22use cedar_policy_core::extensions::{ExtensionFunctionLookupError, Extensions};
23use cedar_policy_core::{ast, entities};
24use miette::Diagnostic;
25use nonempty::NonEmpty;
26use smol_str::SmolStr;
27use std::collections::hash_map::Values;
28use std::collections::HashSet;
29use std::iter::Cloned;
30use std::sync::Arc;
31use thiserror::Error;
32
33#[derive(Debug)]
35pub struct CoreSchema<'a> {
36 schema: &'a ValidatorSchema,
38}
39
40impl<'a> CoreSchema<'a> {
41 pub fn new(schema: &'a ValidatorSchema) -> Self {
43 Self { schema }
44 }
45}
46
47impl<'a> entities::Schema for CoreSchema<'a> {
48 type EntityTypeDescription = EntityTypeDescription;
49 type ActionEntityIterator = Cloned<Values<'a, ast::EntityUID, Arc<ast::Entity>>>;
50
51 fn entity_type(&self, entity_type: &ast::EntityType) -> Option<EntityTypeDescription> {
52 EntityTypeDescription::new(self.schema, entity_type)
53 }
54
55 fn action(&self, action: &ast::EntityUID) -> Option<Arc<ast::Entity>> {
56 self.schema.actions.get(action).cloned()
57 }
58
59 fn entity_types_with_basename<'b>(
60 &'b self,
61 basename: &'b ast::UnreservedId,
62 ) -> Box<dyn Iterator<Item = ast::EntityType> + 'b> {
63 Box::new(self.schema.entity_types().filter_map(move |entity_type| {
64 if &entity_type.name().as_ref().basename() == basename {
65 Some(entity_type.name().clone())
66 } else {
67 None
68 }
69 }))
70 }
71
72 fn action_entities(&self) -> Self::ActionEntityIterator {
73 self.schema.actions.values().cloned()
74 }
75}
76
77#[derive(Debug)]
79pub struct EntityTypeDescription {
80 core_type: ast::EntityType,
82 validator_type: ValidatorEntityType,
84 allowed_parent_types: Arc<HashSet<ast::EntityType>>,
87}
88
89impl EntityTypeDescription {
90 pub fn new(schema: &ValidatorSchema, type_name: &ast::EntityType) -> Option<Self> {
93 Some(Self {
94 core_type: type_name.clone(),
95 validator_type: schema.get_entity_type(type_name).cloned()?,
96 allowed_parent_types: {
97 let mut set = HashSet::new();
98 for possible_parent_et in schema.entity_types() {
99 if possible_parent_et.descendants.contains(type_name) {
100 set.insert(possible_parent_et.name().clone());
101 }
102 }
103 Arc::new(set)
104 },
105 })
106 }
107}
108
109impl entities::EntityTypeDescription for EntityTypeDescription {
110 fn enum_entity_eids(&self) -> Option<NonEmpty<Eid>> {
111 match &self.validator_type.kind {
112 ValidatorEntityTypeKind::Enum(choices) => Some(choices.clone().map(Eid::new)),
113 _ => None,
114 }
115 }
116
117 fn entity_type(&self) -> ast::EntityType {
118 self.core_type.clone()
119 }
120
121 fn attr_type(&self, attr: &str) -> Option<entities::SchemaType> {
122 let attr_type: &crate::types::Type = &self.validator_type.attr(attr)?.attr_type;
123 #[allow(clippy::expect_used)]
128 let core_schema_type: entities::SchemaType = attr_type
129 .clone()
130 .try_into()
131 .expect("failed to convert validator type into Core SchemaType");
132 debug_assert!(attr_type.is_consistent_with(&core_schema_type));
133 Some(core_schema_type)
134 }
135
136 fn tag_type(&self) -> Option<entities::SchemaType> {
137 let tag_type: &crate::types::Type = self.validator_type.tag_type()?;
138 #[allow(clippy::expect_used)]
143 let core_schema_type: entities::SchemaType = tag_type
144 .clone()
145 .try_into()
146 .expect("failed to convert validator type into Core SchemaType");
147 debug_assert!(tag_type.is_consistent_with(&core_schema_type));
148 Some(core_schema_type)
149 }
150
151 fn required_attrs<'s>(&'s self) -> Box<dyn Iterator<Item = SmolStr> + 's> {
152 Box::new(
153 self.validator_type
154 .attributes()
155 .iter()
156 .filter(|(_, ty)| ty.is_required)
157 .map(|(attr, _)| attr.clone()),
158 )
159 }
160
161 fn allowed_parent_types(&self) -> Arc<HashSet<ast::EntityType>> {
162 Arc::clone(&self.allowed_parent_types)
163 }
164
165 fn open_attributes(&self) -> bool {
166 self.validator_type.open_attributes().is_open()
167 }
168}
169
170impl ast::RequestSchema for ValidatorSchema {
171 type Error = RequestValidationError;
172 fn validate_request(
173 &self,
174 request: &ast::Request,
175 extensions: &Extensions<'_>,
176 ) -> std::result::Result<(), Self::Error> {
177 use ast::EntityUIDEntry;
178 if let Some(principal_type) = request.principal().get_type() {
181 if let Some(et) = self.get_entity_type(principal_type) {
182 if let Some(euid) = request.principal().uid() {
183 if let ValidatorEntityType {
184 kind: ValidatorEntityTypeKind::Enum(choices),
185 ..
186 } = et
187 {
188 is_valid_enumerated_entity(&Vec::from(choices.clone().map(Eid::new)), euid)
189 .map_err(Self::Error::from)?;
190 }
191 }
192 } else {
193 return Err(request_validation_errors::UndeclaredPrincipalTypeError {
194 principal_ty: principal_type.clone(),
195 }
196 .into());
197 }
198 }
199 if let Some(resource_type) = request.resource().get_type() {
200 if let Some(et) = self.get_entity_type(resource_type) {
201 if let Some(euid) = request.resource().uid() {
202 if let ValidatorEntityType {
203 kind: ValidatorEntityTypeKind::Enum(choices),
204 ..
205 } = et
206 {
207 is_valid_enumerated_entity(&Vec::from(choices.clone().map(Eid::new)), euid)
208 .map_err(Self::Error::from)?;
209 }
210 }
211 } else {
212 return Err(request_validation_errors::UndeclaredResourceTypeError {
213 resource_ty: resource_type.clone(),
214 }
215 .into());
216 }
217 }
218
219 match request.action() {
221 EntityUIDEntry::Known { euid: action, .. } => {
222 let validator_action_id = self.get_action_id(action).ok_or_else(|| {
223 request_validation_errors::UndeclaredActionError {
224 action: Arc::clone(action),
225 }
226 })?;
227 if let Some(principal_type) = request.principal().get_type() {
228 validator_action_id.check_principal_type(principal_type, action)?;
229 }
230 if let Some(principal_type) = request.resource().get_type() {
231 validator_action_id.check_resource_type(principal_type, action)?;
232 }
233 if let Some(context) = request.context() {
234 validate_euids_in_partial_value(
235 &CoreSchema::new(self),
236 &context.clone().into(),
237 )
238 .map_err(RequestValidationError::InvalidEnumEntity)?;
239 let expected_context_ty = validator_action_id.context_type();
240 if !expected_context_ty
241 .typecheck_partial_value(&context.clone().into(), extensions)
242 .map_err(RequestValidationError::TypeOfContext)?
243 {
244 return Err(request_validation_errors::InvalidContextError {
245 context: context.clone(),
246 action: Arc::clone(action),
247 }
248 .into());
249 }
250 }
251 }
252 EntityUIDEntry::Unknown { .. } => {
253 }
260 }
261 Ok(())
262 }
263}
264
265impl ValidatorActionId {
266 fn check_principal_type(
267 &self,
268 principal_type: &EntityType,
269 action: &Arc<EntityUID>,
270 ) -> Result<(), request_validation_errors::InvalidPrincipalTypeError> {
271 if !self.is_applicable_principal_type(principal_type) {
272 Err(request_validation_errors::InvalidPrincipalTypeError {
273 principal_ty: principal_type.clone(),
274 action: Arc::clone(action),
275 valid_principal_tys: self.applies_to_principals().cloned().collect(),
276 })
277 } else {
278 Ok(())
279 }
280 }
281
282 fn check_resource_type(
283 &self,
284 resource_type: &EntityType,
285 action: &Arc<EntityUID>,
286 ) -> Result<(), request_validation_errors::InvalidResourceTypeError> {
287 if !self.is_applicable_resource_type(resource_type) {
288 Err(request_validation_errors::InvalidResourceTypeError {
289 resource_ty: resource_type.clone(),
290 action: Arc::clone(action),
291 valid_resource_tys: self.applies_to_resources().cloned().collect(),
292 })
293 } else {
294 Ok(())
295 }
296 }
297}
298
299impl ast::RequestSchema for CoreSchema<'_> {
300 type Error = RequestValidationError;
301 fn validate_request(
302 &self,
303 request: &ast::Request,
304 extensions: &Extensions<'_>,
305 ) -> Result<(), Self::Error> {
306 self.schema.validate_request(request, extensions)
307 }
308}
309
310#[derive(Debug, Diagnostic, Error)]
314pub enum RequestValidationError {
315 #[error(transparent)]
317 #[diagnostic(transparent)]
318 UndeclaredAction(#[from] request_validation_errors::UndeclaredActionError),
319 #[error(transparent)]
321 #[diagnostic(transparent)]
322 UndeclaredPrincipalType(#[from] request_validation_errors::UndeclaredPrincipalTypeError),
323 #[error(transparent)]
325 #[diagnostic(transparent)]
326 UndeclaredResourceType(#[from] request_validation_errors::UndeclaredResourceTypeError),
327 #[error(transparent)]
330 #[diagnostic(transparent)]
331 InvalidPrincipalType(#[from] request_validation_errors::InvalidPrincipalTypeError),
332 #[error(transparent)]
335 #[diagnostic(transparent)]
336 InvalidResourceType(#[from] request_validation_errors::InvalidResourceTypeError),
337 #[error(transparent)]
339 #[diagnostic(transparent)]
340 InvalidContext(#[from] request_validation_errors::InvalidContextError),
341 #[error("context is not valid: {0}")]
344 #[diagnostic(transparent)]
345 TypeOfContext(ExtensionFunctionLookupError),
346 #[error(transparent)]
349 #[diagnostic(transparent)]
350 InvalidEnumEntity(#[from] InvalidEnumEntityError),
351}
352
353pub mod request_validation_errors {
355 use cedar_policy_core::ast;
356 use cedar_policy_core::impl_diagnostic_from_method_on_field;
357 use itertools::Itertools;
358 use miette::Diagnostic;
359 use std::sync::Arc;
360 use thiserror::Error;
361
362 #[derive(Debug, Error)]
364 #[error("request's action `{action}` is not declared in the schema")]
365 pub struct UndeclaredActionError {
366 pub(super) action: Arc<ast::EntityUID>,
368 }
369
370 impl Diagnostic for UndeclaredActionError {
371 impl_diagnostic_from_method_on_field!(action, loc);
372 }
373
374 impl UndeclaredActionError {
375 pub fn action(&self) -> &ast::EntityUID {
377 &self.action
378 }
379 }
380
381 #[derive(Debug, Error)]
383 #[error("principal type `{principal_ty}` is not declared in the schema")]
384 pub struct UndeclaredPrincipalTypeError {
385 pub(super) principal_ty: ast::EntityType,
387 }
388
389 impl Diagnostic for UndeclaredPrincipalTypeError {
390 impl_diagnostic_from_method_on_field!(principal_ty, loc);
391 }
392
393 impl UndeclaredPrincipalTypeError {
394 pub fn principal_ty(&self) -> &ast::EntityType {
396 &self.principal_ty
397 }
398 }
399
400 #[derive(Debug, Error)]
402 #[error("resource type `{resource_ty}` is not declared in the schema")]
403 pub struct UndeclaredResourceTypeError {
404 pub(super) resource_ty: ast::EntityType,
406 }
407
408 impl Diagnostic for UndeclaredResourceTypeError {
409 impl_diagnostic_from_method_on_field!(resource_ty, loc);
410 }
411
412 impl UndeclaredResourceTypeError {
413 pub fn resource_ty(&self) -> &ast::EntityType {
415 &self.resource_ty
416 }
417 }
418
419 #[derive(Debug, Error)]
422 #[error("principal type `{principal_ty}` is not valid for `{action}`")]
423 pub struct InvalidPrincipalTypeError {
424 pub(super) principal_ty: ast::EntityType,
426 pub(super) action: Arc<ast::EntityUID>,
428 pub(super) valid_principal_tys: Vec<ast::EntityType>,
430 }
431
432 impl Diagnostic for InvalidPrincipalTypeError {
433 fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
434 Some(Box::new(invalid_principal_type_help(
435 &self.valid_principal_tys,
436 &self.action,
437 )))
438 }
439
440 impl_diagnostic_from_method_on_field!(principal_ty, loc);
443 }
444
445 fn invalid_principal_type_help(
446 valid_principal_tys: &[ast::EntityType],
447 action: &ast::EntityUID,
448 ) -> String {
449 if valid_principal_tys.is_empty() {
450 format!("no principal types are valid for `{action}`")
451 } else {
452 format!(
453 "valid principal types for `{action}`: {}",
454 valid_principal_tys
455 .iter()
456 .sorted_unstable()
457 .map(|et| format!("`{et}`"))
458 .join(", ")
459 )
460 }
461 }
462
463 impl InvalidPrincipalTypeError {
464 pub fn principal_ty(&self) -> &ast::EntityType {
466 &self.principal_ty
467 }
468
469 pub fn action(&self) -> &ast::EntityUID {
471 &self.action
472 }
473
474 pub fn valid_principal_tys(&self) -> impl Iterator<Item = &ast::EntityType> {
476 self.valid_principal_tys.iter()
477 }
478 }
479
480 #[derive(Debug, Error)]
483 #[error("resource type `{resource_ty}` is not valid for `{action}`")]
484 pub struct InvalidResourceTypeError {
485 pub(super) resource_ty: ast::EntityType,
487 pub(super) action: Arc<ast::EntityUID>,
489 pub(super) valid_resource_tys: Vec<ast::EntityType>,
491 }
492
493 impl Diagnostic for InvalidResourceTypeError {
494 fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
495 Some(Box::new(invalid_resource_type_help(
496 &self.valid_resource_tys,
497 &self.action,
498 )))
499 }
500
501 impl_diagnostic_from_method_on_field!(resource_ty, loc);
504 }
505
506 fn invalid_resource_type_help(
507 valid_resource_tys: &[ast::EntityType],
508 action: &ast::EntityUID,
509 ) -> String {
510 if valid_resource_tys.is_empty() {
511 format!("no resource types are valid for `{action}`")
512 } else {
513 format!(
514 "valid resource types for `{action}`: {}",
515 valid_resource_tys
516 .iter()
517 .sorted_unstable()
518 .map(|et| format!("`{et}`"))
519 .join(", ")
520 )
521 }
522 }
523
524 impl InvalidResourceTypeError {
525 pub fn resource_ty(&self) -> &ast::EntityType {
527 &self.resource_ty
528 }
529
530 pub fn action(&self) -> &ast::EntityUID {
532 &self.action
533 }
534
535 pub fn valid_resource_tys(&self) -> impl Iterator<Item = &ast::EntityType> {
537 self.valid_resource_tys.iter()
538 }
539 }
540
541 #[derive(Debug, Error, Diagnostic)]
543 #[error("context `{}` is not valid for `{action}`", ast::BoundedToString::to_string_bounded(.context, BOUNDEDDISPLAY_BOUND_FOR_INVALID_CONTEXT_ERROR))]
544 pub struct InvalidContextError {
545 pub(super) context: ast::Context,
547 pub(super) action: Arc<ast::EntityUID>,
549 }
550
551 const BOUNDEDDISPLAY_BOUND_FOR_INVALID_CONTEXT_ERROR: usize = 5;
552
553 impl InvalidContextError {
554 pub fn context(&self) -> &ast::Context {
556 &self.context
557 }
558
559 pub fn action(&self) -> &ast::EntityUID {
561 &self.action
562 }
563 }
564}
565
566#[derive(Debug, Clone, PartialEq, Eq)]
569pub struct ContextSchema(
570 crate::types::Type,
573);
574
575impl entities::ContextSchema for ContextSchema {
577 fn context_type(&self) -> entities::SchemaType {
578 #[allow(clippy::expect_used)]
580 self.0
581 .clone()
582 .try_into()
583 .expect("failed to convert validator type into Core SchemaType")
584 }
585}
586
587pub fn context_schema_for_action(
592 schema: &ValidatorSchema,
593 action: &ast::EntityUID,
594) -> Option<ContextSchema> {
595 schema.context_type(action).cloned().map(ContextSchema)
602}
603
604#[cfg(test)]
605mod test {
606 use super::*;
607 use ast::{Context, Value};
608 use cedar_policy_core::test_utils::{expect_err, ExpectedErrorMessageBuilder};
609 use cool_asserts::assert_matches;
610 use serde_json::json;
611
612 #[track_caller]
613 fn schema_with_enums() -> ValidatorSchema {
614 let src = r#"
615 entity Fruit enum ["🍉", "🍓", "🍒"];
616 entity People;
617 action "eat" appliesTo {
618 principal: [People],
619 resource: [Fruit],
620 context: {
621 fruit?: Fruit,
622 }
623 };
624 "#;
625 ValidatorSchema::from_cedarschema_str(src, Extensions::none())
626 .expect("should be a valid schema")
627 .0
628 }
629
630 fn schema() -> ValidatorSchema {
631 let src = json!(
632 { "": {
633 "entityTypes": {
634 "User": {
635 "memberOfTypes": [ "Group" ]
636 },
637 "Group": {
638 "memberOfTypes": []
639 },
640 "Photo": {
641 "memberOfTypes": [ "Album" ]
642 },
643 "Album": {
644 "memberOfTypes": []
645 }
646 },
647 "actions": {
648 "view_photo": {
649 "appliesTo": {
650 "principalTypes": ["User", "Group"],
651 "resourceTypes": ["Photo"]
652 }
653 },
654 "edit_photo": {
655 "appliesTo": {
656 "principalTypes": ["User", "Group"],
657 "resourceTypes": ["Photo"],
658 "context": {
659 "type": "Record",
660 "attributes": {
661 "admin_approval": {
662 "type": "Boolean",
663 "required": true,
664 }
665 }
666 }
667 }
668 }
669 }
670 }});
671 ValidatorSchema::from_json_value(src, Extensions::all_available())
672 .expect("failed to create ValidatorSchema")
673 }
674
675 #[test]
677 fn success_concrete_request_no_context() {
678 assert_matches!(
679 ast::Request::new(
680 (
681 ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
682 None
683 ),
684 (
685 ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(),
686 None
687 ),
688 (
689 ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
690 None
691 ),
692 ast::Context::empty(),
693 Some(&schema()),
694 Extensions::all_available(),
695 ),
696 Ok(_)
697 );
698 }
699
700 #[test]
702 fn success_concrete_request_with_context() {
703 assert_matches!(
704 ast::Request::new(
705 (
706 ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
707 None
708 ),
709 (
710 ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(),
711 None
712 ),
713 (
714 ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
715 None
716 ),
717 ast::Context::from_pairs(
718 [("admin_approval".into(), ast::RestrictedExpr::val(true))],
719 Extensions::all_available()
720 )
721 .unwrap(),
722 Some(&schema()),
723 Extensions::all_available(),
724 ),
725 Ok(_)
726 );
727 }
728
729 #[test]
731 fn success_principal_unknown() {
732 assert_matches!(
733 ast::Request::new_with_unknowns(
734 ast::EntityUIDEntry::unknown(),
735 ast::EntityUIDEntry::known(
736 ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(),
737 None,
738 ),
739 ast::EntityUIDEntry::known(
740 ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
741 None,
742 ),
743 Some(ast::Context::empty()),
744 Some(&schema()),
745 Extensions::all_available(),
746 ),
747 Ok(_)
748 );
749 }
750
751 #[test]
753 fn success_action_unknown() {
754 assert_matches!(
755 ast::Request::new_with_unknowns(
756 ast::EntityUIDEntry::known(
757 ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
758 None,
759 ),
760 ast::EntityUIDEntry::unknown(),
761 ast::EntityUIDEntry::known(
762 ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
763 None,
764 ),
765 Some(ast::Context::empty()),
766 Some(&schema()),
767 Extensions::all_available(),
768 ),
769 Ok(_)
770 );
771 }
772
773 #[test]
775 fn success_resource_unknown() {
776 assert_matches!(
777 ast::Request::new_with_unknowns(
778 ast::EntityUIDEntry::known(
779 ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
780 None,
781 ),
782 ast::EntityUIDEntry::known(
783 ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(),
784 None,
785 ),
786 ast::EntityUIDEntry::unknown(),
787 Some(ast::Context::empty()),
788 Some(&schema()),
789 Extensions::all_available(),
790 ),
791 Ok(_)
792 );
793 }
794
795 #[test]
797 fn success_context_unknown() {
798 assert_matches!(
799 ast::Request::new_with_unknowns(
800 ast::EntityUIDEntry::known(
801 ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
802 None,
803 ),
804 ast::EntityUIDEntry::known(
805 ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(),
806 None,
807 ),
808 ast::EntityUIDEntry::known(
809 ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
810 None,
811 ),
812 None,
813 Some(&schema()),
814 Extensions::all_available(),
815 ),
816 Ok(_)
817 )
818 }
819
820 #[test]
822 fn success_everything_unspecified() {
823 assert_matches!(
824 ast::Request::new_with_unknowns(
825 ast::EntityUIDEntry::unknown(),
826 ast::EntityUIDEntry::unknown(),
827 ast::EntityUIDEntry::unknown(),
828 None,
829 Some(&schema()),
830 Extensions::all_available(),
831 ),
832 Ok(_)
833 );
834 }
835
836 #[test]
840 fn success_unknown_action_but_invalid_types() {
841 assert_matches!(
842 ast::Request::new_with_unknowns(
843 ast::EntityUIDEntry::known(
844 ast::EntityUID::with_eid_and_type("Album", "abc123").unwrap(),
845 None,
846 ),
847 ast::EntityUIDEntry::unknown(),
848 ast::EntityUIDEntry::known(
849 ast::EntityUID::with_eid_and_type("User", "alice").unwrap(),
850 None,
851 ),
852 None,
853 Some(&schema()),
854 Extensions::all_available(),
855 ),
856 Ok(_)
857 );
858 }
859
860 #[test]
862 fn action_not_declared() {
863 assert_matches!(
864 ast::Request::new(
865 (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
866 (ast::EntityUID::with_eid_and_type("Action", "destroy").unwrap(), None),
867 (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
868 ast::Context::empty(),
869 Some(&schema()),
870 Extensions::all_available(),
871 ),
872 Err(e) => {
873 expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(r#"request's action `Action::"destroy"` is not declared in the schema"#).build());
874 }
875 );
876 }
877
878 #[test]
880 fn principal_type_not_declared() {
881 assert_matches!(
882 ast::Request::new(
883 (ast::EntityUID::with_eid_and_type("Foo", "abc123").unwrap(), None),
884 (ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(), None),
885 (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
886 ast::Context::empty(),
887 Some(&schema()),
888 Extensions::all_available(),
889 ),
890 Err(e) => {
891 expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error("principal type `Foo` is not declared in the schema").build());
892 }
893 );
894 }
895
896 #[test]
898 fn resource_type_not_declared() {
899 assert_matches!(
900 ast::Request::new(
901 (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
902 (ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(), None),
903 (ast::EntityUID::with_eid_and_type("Foo", "vacationphoto94.jpg").unwrap(), None),
904 ast::Context::empty(),
905 Some(&schema()),
906 Extensions::all_available(),
907 ),
908 Err(e) => {
909 expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error("resource type `Foo` is not declared in the schema").build());
910 }
911 );
912 }
913
914 #[test]
916 fn principal_type_invalid() {
917 assert_matches!(
918 ast::Request::new(
919 (ast::EntityUID::with_eid_and_type("Album", "abc123").unwrap(), None),
920 (ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(), None),
921 (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
922 ast::Context::empty(),
923 Some(&schema()),
924 Extensions::all_available(),
925 ),
926 Err(e) => {
927 expect_err(
928 "",
929 &miette::Report::new(e),
930 &ExpectedErrorMessageBuilder::error(r#"principal type `Album` is not valid for `Action::"view_photo"`"#)
931 .help(r#"valid principal types for `Action::"view_photo"`: `Group`, `User`"#)
932 .build(),
933 );
934 }
935 );
936 }
937
938 #[test]
940 fn resource_type_invalid() {
941 assert_matches!(
942 ast::Request::new(
943 (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
944 (ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(), None),
945 (ast::EntityUID::with_eid_and_type("Group", "coders").unwrap(), None),
946 ast::Context::empty(),
947 Some(&schema()),
948 Extensions::all_available(),
949 ),
950 Err(e) => {
951 expect_err(
952 "",
953 &miette::Report::new(e),
954 &ExpectedErrorMessageBuilder::error(r#"resource type `Group` is not valid for `Action::"view_photo"`"#)
955 .help(r#"valid resource types for `Action::"view_photo"`: `Photo`"#)
956 .build(),
957 );
958 }
959 );
960 }
961
962 #[test]
964 fn context_missing_attribute() {
965 assert_matches!(
966 ast::Request::new(
967 (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
968 (ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(), None),
969 (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
970 ast::Context::empty(),
971 Some(&schema()),
972 Extensions::all_available(),
973 ),
974 Err(e) => {
975 expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(r#"context `{}` is not valid for `Action::"edit_photo"`"#).build());
976 }
977 );
978 }
979
980 #[test]
982 fn context_extra_attribute() {
983 let context_with_extra_attr = ast::Context::from_pairs(
984 [
985 ("admin_approval".into(), ast::RestrictedExpr::val(true)),
986 ("extra".into(), ast::RestrictedExpr::val(42)),
987 ],
988 Extensions::all_available(),
989 )
990 .unwrap();
991 assert_matches!(
992 ast::Request::new(
993 (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
994 (ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(), None),
995 (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
996 context_with_extra_attr,
997 Some(&schema()),
998 Extensions::all_available(),
999 ),
1000 Err(e) => {
1001 expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(r#"context `{admin_approval: true, extra: 42}` is not valid for `Action::"edit_photo"`"#).build());
1002 }
1003 );
1004 }
1005
1006 #[test]
1008 fn context_attribute_wrong_type() {
1009 let context_with_wrong_type_attr = ast::Context::from_pairs(
1010 [(
1011 "admin_approval".into(),
1012 ast::RestrictedExpr::set([ast::RestrictedExpr::val(true)]),
1013 )],
1014 Extensions::all_available(),
1015 )
1016 .unwrap();
1017 assert_matches!(
1018 ast::Request::new(
1019 (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
1020 (ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(), None),
1021 (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
1022 context_with_wrong_type_attr,
1023 Some(&schema()),
1024 Extensions::all_available(),
1025 ),
1026 Err(e) => {
1027 expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(r#"context `{admin_approval: [true]}` is not valid for `Action::"edit_photo"`"#).build());
1028 }
1029 );
1030 }
1031
1032 #[test]
1034 fn context_attribute_heterogeneous_set() {
1035 let context_with_heterogeneous_set = ast::Context::from_pairs(
1036 [(
1037 "admin_approval".into(),
1038 ast::RestrictedExpr::set([
1039 ast::RestrictedExpr::val(true),
1040 ast::RestrictedExpr::val(-1001),
1041 ]),
1042 )],
1043 Extensions::all_available(),
1044 )
1045 .unwrap();
1046 assert_matches!(
1047 ast::Request::new(
1048 (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
1049 (ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(), None),
1050 (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
1051 context_with_heterogeneous_set,
1052 Some(&schema()),
1053 Extensions::all_available(),
1054 ),
1055 Err(e) => {
1056 expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(r#"context `{admin_approval: [true, -1001]}` is not valid for `Action::"edit_photo"`"#).build());
1057 }
1058 );
1059 }
1060
1061 #[test]
1063 fn context_large() {
1064 let large_context_with_extra_attributes = ast::Context::from_pairs(
1065 [
1066 ("admin_approval".into(), ast::RestrictedExpr::val(true)),
1067 ("extra1".into(), ast::RestrictedExpr::val(false)),
1068 ("also extra".into(), ast::RestrictedExpr::val("spam")),
1069 (
1070 "extra2".into(),
1071 ast::RestrictedExpr::set([ast::RestrictedExpr::val(-100)]),
1072 ),
1073 (
1074 "extra3".into(),
1075 ast::RestrictedExpr::val(
1076 ast::EntityUID::with_eid_and_type("User", "alice").unwrap(),
1077 ),
1078 ),
1079 ("extra4".into(), ast::RestrictedExpr::val("foobar")),
1080 ],
1081 Extensions::all_available(),
1082 )
1083 .unwrap();
1084 assert_matches!(
1085 ast::Request::new(
1086 (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
1087 (ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(), None),
1088 (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
1089 large_context_with_extra_attributes,
1090 Some(&schema()),
1091 Extensions::all_available(),
1092 ),
1093 Err(e) => {
1094 expect_err(
1095 "",
1096 &miette::Report::new(e),
1097 &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(),
1098 );
1099 }
1100 );
1101 }
1102
1103 #[test]
1104 fn enumerated_entity_type() {
1105 assert_matches!(
1106 ast::Request::new(
1107 (
1108 ast::EntityUID::with_eid_and_type("People", "😋").unwrap(),
1109 None
1110 ),
1111 (
1112 ast::EntityUID::with_eid_and_type("Action", "eat").unwrap(),
1113 None
1114 ),
1115 (
1116 ast::EntityUID::with_eid_and_type("Fruit", "🍉").unwrap(),
1117 None
1118 ),
1119 Context::empty(),
1120 Some(&schema_with_enums()),
1121 Extensions::none(),
1122 ),
1123 Ok(_)
1124 );
1125 assert_matches!(
1126 ast::Request::new(
1127 (ast::EntityUID::with_eid_and_type("People", "🤔").unwrap(), None),
1128 (ast::EntityUID::with_eid_and_type("Action", "eat").unwrap(), None),
1129 (ast::EntityUID::with_eid_and_type("Fruit", "🥝").unwrap(), None),
1130 Context::empty(),
1131 Some(&schema_with_enums()),
1132 Extensions::none(),
1133 ),
1134 Err(e) => {
1135 expect_err(
1136 "",
1137 &miette::Report::new(e),
1138 &ExpectedErrorMessageBuilder::error(r#"entity `Fruit::"🥝"` is of an enumerated entity type, but `"🥝"` is not declared as a valid eid"#).help(r#"valid entity eids: "🍉", "🍓", "🍒""#)
1139 .build(),
1140 );
1141 }
1142 );
1143 assert_matches!(
1144 ast::Request::new(
1145 (ast::EntityUID::with_eid_and_type("People", "🤔").unwrap(), None),
1146 (ast::EntityUID::with_eid_and_type("Action", "eat").unwrap(), None),
1147 (ast::EntityUID::with_eid_and_type("Fruit", "🍉").unwrap(), None),
1148 Context::from_pairs([("fruit".into(), (Value::from(ast::EntityUID::with_eid_and_type("Fruit", "🥭").unwrap())).into())], Extensions::none()).expect("should be a valid context"),
1149 Some(&schema_with_enums()),
1150 Extensions::none(),
1151 ),
1152 Err(e) => {
1153 expect_err(
1154 "",
1155 &miette::Report::new(e),
1156 &ExpectedErrorMessageBuilder::error(r#"entity `Fruit::"🥭"` is of an enumerated entity type, but `"🥭"` is not declared as a valid eid"#).help(r#"valid entity eids: "🍉", "🍓", "🍒""#)
1157 .build(),
1158 );
1159 }
1160 );
1161 }
1162}