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