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),
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 #[expect(
126 clippy::expect_used,
127 reason = "`attr_type` is taken from a `ValidatorEntityType` which was constructed from a schema"
128 )]
129 let core_schema_type: entities::SchemaType = attr_type
130 .clone()
131 .try_into()
132 .expect("failed to convert validator type into Core SchemaType");
133 debug_assert!(attr_type.is_consistent_with(&core_schema_type));
134 Some(core_schema_type)
135 }
136
137 fn tag_type(&self) -> Option<entities::SchemaType> {
138 let tag_type: &crate::validator::types::Type = self.validator_type.tag_type()?;
139 #[expect(
140 clippy::expect_used,
141 reason = "`tag_type` is taken from a `ValidatorEntityType` which was constructed from a schema"
142 )]
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 let action_uid = request.action().uid();
178
179 self.validate_scope_variables(
181 request.principal().uid(),
182 action_uid,
183 request.resource().uid(),
184 )?;
185
186 if let (Some(context), Some(action)) = (request.context(), action_uid) {
187 self.validate_context(context, action, extensions)?;
188 }
189 Ok(())
190 }
191
192 fn validate_context<'a>(
194 &self,
195 context: &ast::Context,
196 action: &ast::EntityUID,
197 extensions: &Extensions<'a>,
198 ) -> std::result::Result<(), RequestValidationError> {
199 let validator_action_id = self.get_action_id(action).ok_or_else(|| {
201 request_validation_errors::UndeclaredActionError {
202 action: Arc::new(action.clone()),
203 }
204 })?;
205
206 validate_euids_in_partial_value(&CoreSchema::new(self), &context.clone().into()).map_err(
208 |e| match e {
209 ValidateEuidError::InvalidEnumEntity(e) => {
210 RequestValidationError::InvalidEnumEntity(e)
211 }
212 ValidateEuidError::UndeclaredAction(e) => {
213 request_validation_errors::UndeclaredActionError {
214 action: Arc::new(e.uid),
215 }
216 .into()
217 }
218 },
219 )?;
220
221 let expected_context_ty = validator_action_id.context_type();
223 if !expected_context_ty
224 .typecheck_partial_value(&context.clone().into(), extensions)
225 .map_err(RequestValidationError::TypeOfContext)?
226 {
227 return Err(request_validation_errors::InvalidContextError {
228 context: context.clone(),
229 action: Arc::new(action.clone()),
230 }
231 .into());
232 }
233 Ok(())
234 }
235
236 fn validate_scope_variables(
238 &self,
239 principal: Option<&EntityUID>,
240 action: Option<&EntityUID>,
241 resource: Option<&EntityUID>,
242 ) -> std::result::Result<(), RequestValidationError> {
243 if let Some(principal) = principal {
245 let principal_type = principal.entity_type();
246 if let Some(et) = self.get_entity_type(principal_type) {
247 if let ValidatorEntityType {
248 kind: ValidatorEntityTypeKind::Enum(choices),
249 ..
250 } = et
251 {
252 is_valid_enumerated_entity(choices, principal)
253 .map_err(RequestValidationError::InvalidEnumEntity)?;
254 }
255 } else {
256 return Err(request_validation_errors::UndeclaredPrincipalTypeError {
257 principal_ty: principal_type.clone(),
258 }
259 .into());
260 }
261 }
262
263 if let Some(resource) = resource {
265 let resource_type = resource.entity_type();
266 if let Some(et) = self.get_entity_type(resource_type) {
267 if let ValidatorEntityType {
268 kind: ValidatorEntityTypeKind::Enum(choices),
269 ..
270 } = et
271 {
272 is_valid_enumerated_entity(choices, resource)
273 .map_err(RequestValidationError::InvalidEnumEntity)?;
274 }
275 } else {
276 return Err(request_validation_errors::UndeclaredResourceTypeError {
277 resource_ty: resource_type.clone(),
278 }
279 .into());
280 }
281 }
282
283 if let Some(action) = action {
285 let validator_action_id = self.get_action_id(action).ok_or_else(|| {
286 request_validation_errors::UndeclaredActionError {
287 action: Arc::new(action.clone()),
288 }
289 })?;
290
291 if let Some(principal) = principal {
293 let principal_type = principal.entity_type();
294 validator_action_id
295 .check_principal_type(principal_type, &Arc::new(action.clone()))?;
296 }
297
298 if let Some(resource) = resource {
300 let resource_type = resource.entity_type();
301 validator_action_id
302 .check_resource_type(resource_type, &Arc::new(action.clone()))?;
303 }
304 }
305
306 Ok(())
307 }
308}
309
310impl ValidatorActionId {
311 pub(crate) fn check_principal_type(
312 &self,
313 principal_type: &EntityType,
314 action: &Arc<EntityUID>,
315 ) -> Result<(), request_validation_errors::InvalidPrincipalTypeError> {
316 if !self.is_applicable_principal_type(principal_type) {
317 Err(request_validation_errors::InvalidPrincipalTypeError {
318 principal_ty: principal_type.clone(),
319 action: Arc::clone(action),
320 valid_principal_tys: self.applies_to_principals().cloned().collect(),
321 })
322 } else {
323 Ok(())
324 }
325 }
326
327 pub(crate) fn check_resource_type(
328 &self,
329 resource_type: &EntityType,
330 action: &Arc<EntityUID>,
331 ) -> Result<(), request_validation_errors::InvalidResourceTypeError> {
332 if !self.is_applicable_resource_type(resource_type) {
333 Err(request_validation_errors::InvalidResourceTypeError {
334 resource_ty: resource_type.clone(),
335 action: Arc::clone(action),
336 valid_resource_tys: self.applies_to_resources().cloned().collect(),
337 })
338 } else {
339 Ok(())
340 }
341 }
342}
343
344impl ast::RequestSchema for CoreSchema<'_> {
345 type Error = RequestValidationError;
346 fn validate_request(
347 &self,
348 request: &ast::Request,
349 extensions: &Extensions<'_>,
350 ) -> Result<(), Self::Error> {
351 self.schema.validate_request(request, extensions)
352 }
353
354 fn validate_context<'a>(
356 &self,
357 context: &ast::Context,
358 action: &EntityUID,
359 extensions: &Extensions<'a>,
360 ) -> std::result::Result<(), Self::Error> {
361 self.schema.validate_context(context, action, extensions)
362 }
363
364 fn validate_scope_variables(
366 &self,
367 principal: Option<&EntityUID>,
368 action: Option<&EntityUID>,
369 resource: Option<&EntityUID>,
370 ) -> std::result::Result<(), RequestValidationError> {
371 self.schema
372 .validate_scope_variables(principal, action, resource)
373 }
374}
375
376#[derive(Debug, Diagnostic, Error)]
380pub enum RequestValidationError {
381 #[error(transparent)]
383 #[diagnostic(transparent)]
384 UndeclaredAction(#[from] request_validation_errors::UndeclaredActionError),
385 #[error(transparent)]
387 #[diagnostic(transparent)]
388 UndeclaredPrincipalType(#[from] request_validation_errors::UndeclaredPrincipalTypeError),
389 #[error(transparent)]
391 #[diagnostic(transparent)]
392 UndeclaredResourceType(#[from] request_validation_errors::UndeclaredResourceTypeError),
393 #[error(transparent)]
396 #[diagnostic(transparent)]
397 InvalidPrincipalType(#[from] request_validation_errors::InvalidPrincipalTypeError),
398 #[error(transparent)]
401 #[diagnostic(transparent)]
402 InvalidResourceType(#[from] request_validation_errors::InvalidResourceTypeError),
403 #[error(transparent)]
405 #[diagnostic(transparent)]
406 InvalidContext(#[from] request_validation_errors::InvalidContextError),
407 #[error("context is not valid: {0}")]
410 #[diagnostic(transparent)]
411 TypeOfContext(ExtensionFunctionLookupError),
412 #[error(transparent)]
415 #[diagnostic(transparent)]
416 InvalidEnumEntity(#[from] InvalidEnumEntityError),
417}
418
419pub mod request_validation_errors {
421 use crate::ast;
422 use itertools::Itertools;
423 use miette::Diagnostic;
424 use std::sync::Arc;
425 use thiserror::Error;
426
427 #[derive(Debug, Error)]
429 #[error("request's action `{action}` is not declared in the schema")]
430 pub struct UndeclaredActionError {
431 pub(crate) action: Arc<ast::EntityUID>,
433 }
434
435 impl Diagnostic for UndeclaredActionError {
436 impl_diagnostic_from_method_on_field!(action, loc);
437 }
438
439 impl UndeclaredActionError {
440 pub fn action(&self) -> &ast::EntityUID {
442 &self.action
443 }
444 }
445
446 #[derive(Debug, Error)]
448 #[error("principal type `{principal_ty}` is not declared in the schema")]
449 pub struct UndeclaredPrincipalTypeError {
450 pub(crate) principal_ty: ast::EntityType,
452 }
453
454 impl Diagnostic for UndeclaredPrincipalTypeError {
455 impl_diagnostic_from_method_on_field!(principal_ty, loc);
456 }
457
458 impl UndeclaredPrincipalTypeError {
459 pub fn principal_ty(&self) -> &ast::EntityType {
461 &self.principal_ty
462 }
463 }
464
465 #[derive(Debug, Error)]
467 #[error("resource type `{resource_ty}` is not declared in the schema")]
468 pub struct UndeclaredResourceTypeError {
469 pub(crate) resource_ty: ast::EntityType,
471 }
472
473 impl Diagnostic for UndeclaredResourceTypeError {
474 impl_diagnostic_from_method_on_field!(resource_ty, loc);
475 }
476
477 impl UndeclaredResourceTypeError {
478 pub fn resource_ty(&self) -> &ast::EntityType {
480 &self.resource_ty
481 }
482 }
483
484 #[derive(Debug, Error)]
487 #[error("principal type `{principal_ty}` is not valid for `{action}`")]
488 pub struct InvalidPrincipalTypeError {
489 pub(crate) principal_ty: ast::EntityType,
491 pub(crate) action: Arc<ast::EntityUID>,
493 pub(crate) valid_principal_tys: Vec<ast::EntityType>,
495 }
496
497 impl Diagnostic for InvalidPrincipalTypeError {
498 fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
499 Some(Box::new(invalid_principal_type_help(
500 &self.valid_principal_tys,
501 &self.action,
502 )))
503 }
504
505 impl_diagnostic_from_method_on_field!(principal_ty, loc);
508 }
509
510 fn invalid_principal_type_help(
511 valid_principal_tys: &[ast::EntityType],
512 action: &ast::EntityUID,
513 ) -> String {
514 if valid_principal_tys.is_empty() {
515 format!("no principal types are valid for `{action}`")
516 } else {
517 format!(
518 "valid principal types for `{action}`: {}",
519 valid_principal_tys
520 .iter()
521 .sorted_unstable()
522 .map(|et| format!("`{et}`"))
523 .join(", ")
524 )
525 }
526 }
527
528 impl InvalidPrincipalTypeError {
529 pub fn principal_ty(&self) -> &ast::EntityType {
531 &self.principal_ty
532 }
533
534 pub fn action(&self) -> &ast::EntityUID {
536 &self.action
537 }
538
539 pub fn valid_principal_tys(&self) -> impl Iterator<Item = &ast::EntityType> {
541 self.valid_principal_tys.iter()
542 }
543 }
544
545 #[derive(Debug, Error)]
548 #[error("resource type `{resource_ty}` is not valid for `{action}`")]
549 pub struct InvalidResourceTypeError {
550 pub(super) resource_ty: ast::EntityType,
552 pub(super) action: Arc<ast::EntityUID>,
554 pub(super) valid_resource_tys: Vec<ast::EntityType>,
556 }
557
558 impl Diagnostic for InvalidResourceTypeError {
559 fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
560 Some(Box::new(invalid_resource_type_help(
561 &self.valid_resource_tys,
562 &self.action,
563 )))
564 }
565
566 impl_diagnostic_from_method_on_field!(resource_ty, loc);
569 }
570
571 fn invalid_resource_type_help(
572 valid_resource_tys: &[ast::EntityType],
573 action: &ast::EntityUID,
574 ) -> String {
575 if valid_resource_tys.is_empty() {
576 format!("no resource types are valid for `{action}`")
577 } else {
578 format!(
579 "valid resource types for `{action}`: {}",
580 valid_resource_tys
581 .iter()
582 .sorted_unstable()
583 .map(|et| format!("`{et}`"))
584 .join(", ")
585 )
586 }
587 }
588
589 impl InvalidResourceTypeError {
590 pub fn resource_ty(&self) -> &ast::EntityType {
592 &self.resource_ty
593 }
594
595 pub fn action(&self) -> &ast::EntityUID {
597 &self.action
598 }
599
600 pub fn valid_resource_tys(&self) -> impl Iterator<Item = &ast::EntityType> {
602 self.valid_resource_tys.iter()
603 }
604 }
605
606 #[derive(Debug, Error, Diagnostic)]
608 #[error("context `{}` is not valid for `{action}`", ast::BoundedToString::to_string_bounded(.context, BOUNDEDDISPLAY_BOUND_FOR_INVALID_CONTEXT_ERROR))]
609 pub struct InvalidContextError {
610 pub(crate) context: ast::Context,
612 pub(crate) action: Arc<ast::EntityUID>,
614 }
615
616 const BOUNDEDDISPLAY_BOUND_FOR_INVALID_CONTEXT_ERROR: usize = 5;
617
618 impl InvalidContextError {
619 pub fn context(&self) -> &ast::Context {
621 &self.context
622 }
623
624 pub fn action(&self) -> &ast::EntityUID {
626 &self.action
627 }
628 }
629}
630
631#[derive(Debug, Clone, PartialEq, Eq)]
634pub struct ContextSchema(
635 crate::validator::types::Type,
638);
639
640impl entities::ContextSchema for ContextSchema {
642 fn context_type(&self) -> entities::SchemaType {
643 #[expect(
644 clippy::expect_used,
645 reason = "By `ContextSchema` invariant, `self.0` is representable as a schema type"
646 )]
647 self.0
648 .clone()
649 .try_into()
650 .expect("failed to convert validator type into Core SchemaType")
651 }
652}
653
654pub fn context_schema_for_action(
659 schema: &ValidatorSchema,
660 action: &ast::EntityUID,
661) -> Option<ContextSchema> {
662 schema.context_type(action).cloned().map(ContextSchema)
669}
670
671#[cfg(test)]
672mod test {
673 use super::*;
674 use crate::test_utils::{expect_err, ExpectedErrorMessageBuilder};
675 use ast::{Context, Value};
676 use cool_asserts::assert_matches;
677 use serde_json::json;
678
679 #[track_caller]
680 fn schema_with_enums() -> ValidatorSchema {
681 let src = r#"
682 entity Fruit enum ["🍉", "🍓", "🍒"];
683 entity People;
684 action "eat" appliesTo {
685 principal: [People],
686 resource: [Fruit],
687 context: {
688 fruit?: Fruit,
689 }
690 };
691 "#;
692 ValidatorSchema::from_cedarschema_str(src, Extensions::none())
693 .expect("should be a valid schema")
694 .0
695 }
696
697 fn schema() -> ValidatorSchema {
698 let src = json!(
699 { "": {
700 "entityTypes": {
701 "User": {
702 "memberOfTypes": [ "Group" ]
703 },
704 "Group": {
705 "memberOfTypes": []
706 },
707 "Photo": {
708 "memberOfTypes": [ "Album" ]
709 },
710 "Album": {
711 "memberOfTypes": []
712 }
713 },
714 "actions": {
715 "view_photo": {
716 "appliesTo": {
717 "principalTypes": ["User", "Group"],
718 "resourceTypes": ["Photo"]
719 }
720 },
721 "edit_photo": {
722 "appliesTo": {
723 "principalTypes": ["User", "Group"],
724 "resourceTypes": ["Photo"],
725 "context": {
726 "type": "Record",
727 "attributes": {
728 "admin_approval": {
729 "type": "Boolean",
730 "required": true,
731 }
732 }
733 }
734 }
735 }
736 }
737 }});
738 ValidatorSchema::from_json_value(src, Extensions::all_available())
739 .expect("failed to create ValidatorSchema")
740 }
741
742 #[test]
744 fn success_concrete_request_no_context() {
745 assert_matches!(
746 ast::Request::new(
747 (
748 ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
749 None
750 ),
751 (
752 ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(),
753 None
754 ),
755 (
756 ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
757 None
758 ),
759 ast::Context::empty(),
760 Some(&schema()),
761 Extensions::all_available(),
762 ),
763 Ok(_)
764 );
765 }
766
767 #[test]
769 fn success_concrete_request_with_context() {
770 assert_matches!(
771 ast::Request::new(
772 (
773 ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
774 None
775 ),
776 (
777 ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(),
778 None
779 ),
780 (
781 ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
782 None
783 ),
784 ast::Context::from_pairs(
785 [("admin_approval".into(), ast::RestrictedExpr::val(true))],
786 Extensions::all_available()
787 )
788 .unwrap(),
789 Some(&schema()),
790 Extensions::all_available(),
791 ),
792 Ok(_)
793 );
794 }
795
796 #[test]
798 fn success_principal_unknown() {
799 assert_matches!(
800 ast::Request::new_with_unknowns(
801 ast::EntityUIDEntry::unknown(),
802 ast::EntityUIDEntry::known(
803 ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(),
804 None,
805 ),
806 ast::EntityUIDEntry::known(
807 ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
808 None,
809 ),
810 Some(ast::Context::empty()),
811 Some(&schema()),
812 Extensions::all_available(),
813 ),
814 Ok(_)
815 );
816 }
817
818 #[test]
820 fn success_action_unknown() {
821 assert_matches!(
822 ast::Request::new_with_unknowns(
823 ast::EntityUIDEntry::known(
824 ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
825 None,
826 ),
827 ast::EntityUIDEntry::unknown(),
828 ast::EntityUIDEntry::known(
829 ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
830 None,
831 ),
832 Some(ast::Context::empty()),
833 Some(&schema()),
834 Extensions::all_available(),
835 ),
836 Ok(_)
837 );
838 }
839
840 #[test]
842 fn success_resource_unknown() {
843 assert_matches!(
844 ast::Request::new_with_unknowns(
845 ast::EntityUIDEntry::known(
846 ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
847 None,
848 ),
849 ast::EntityUIDEntry::known(
850 ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(),
851 None,
852 ),
853 ast::EntityUIDEntry::unknown(),
854 Some(ast::Context::empty()),
855 Some(&schema()),
856 Extensions::all_available(),
857 ),
858 Ok(_)
859 );
860 }
861
862 #[test]
864 fn success_context_unknown() {
865 assert_matches!(
866 ast::Request::new_with_unknowns(
867 ast::EntityUIDEntry::known(
868 ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
869 None,
870 ),
871 ast::EntityUIDEntry::known(
872 ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(),
873 None,
874 ),
875 ast::EntityUIDEntry::known(
876 ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
877 None,
878 ),
879 None,
880 Some(&schema()),
881 Extensions::all_available(),
882 ),
883 Ok(_)
884 )
885 }
886
887 #[test]
889 fn success_everything_unspecified() {
890 assert_matches!(
891 ast::Request::new_with_unknowns(
892 ast::EntityUIDEntry::unknown(),
893 ast::EntityUIDEntry::unknown(),
894 ast::EntityUIDEntry::unknown(),
895 None,
896 Some(&schema()),
897 Extensions::all_available(),
898 ),
899 Ok(_)
900 );
901 }
902
903 #[test]
907 fn success_unknown_action_but_invalid_types() {
908 assert_matches!(
909 ast::Request::new_with_unknowns(
910 ast::EntityUIDEntry::known(
911 ast::EntityUID::with_eid_and_type("Album", "abc123").unwrap(),
912 None,
913 ),
914 ast::EntityUIDEntry::unknown(),
915 ast::EntityUIDEntry::known(
916 ast::EntityUID::with_eid_and_type("User", "alice").unwrap(),
917 None,
918 ),
919 None,
920 Some(&schema()),
921 Extensions::all_available(),
922 ),
923 Ok(_)
924 );
925 }
926
927 #[test]
929 fn action_not_declared() {
930 assert_matches!(
931 ast::Request::new(
932 (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
933 (ast::EntityUID::with_eid_and_type("Action", "destroy").unwrap(), None),
934 (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
935 ast::Context::empty(),
936 Some(&schema()),
937 Extensions::all_available(),
938 ),
939 Err(e) => {
940 expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(r#"request's action `Action::"destroy"` is not declared in the schema"#).build());
941 }
942 );
943 }
944
945 #[test]
947 fn principal_type_not_declared() {
948 assert_matches!(
949 ast::Request::new(
950 (ast::EntityUID::with_eid_and_type("Foo", "abc123").unwrap(), None),
951 (ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(), None),
952 (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
953 ast::Context::empty(),
954 Some(&schema()),
955 Extensions::all_available(),
956 ),
957 Err(e) => {
958 expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error("principal type `Foo` is not declared in the schema").build());
959 }
960 );
961 }
962
963 #[test]
965 fn resource_type_not_declared() {
966 assert_matches!(
967 ast::Request::new(
968 (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
969 (ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(), None),
970 (ast::EntityUID::with_eid_and_type("Foo", "vacationphoto94.jpg").unwrap(), None),
971 ast::Context::empty(),
972 Some(&schema()),
973 Extensions::all_available(),
974 ),
975 Err(e) => {
976 expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error("resource type `Foo` is not declared in the schema").build());
977 }
978 );
979 }
980
981 #[test]
983 fn principal_type_invalid() {
984 assert_matches!(
985 ast::Request::new(
986 (ast::EntityUID::with_eid_and_type("Album", "abc123").unwrap(), None),
987 (ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(), None),
988 (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
989 ast::Context::empty(),
990 Some(&schema()),
991 Extensions::all_available(),
992 ),
993 Err(e) => {
994 expect_err(
995 "",
996 &miette::Report::new(e),
997 &ExpectedErrorMessageBuilder::error(r#"principal type `Album` is not valid for `Action::"view_photo"`"#)
998 .help(r#"valid principal types for `Action::"view_photo"`: `Group`, `User`"#)
999 .build(),
1000 );
1001 }
1002 );
1003 }
1004
1005 #[test]
1007 fn resource_type_invalid() {
1008 assert_matches!(
1009 ast::Request::new(
1010 (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
1011 (ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(), None),
1012 (ast::EntityUID::with_eid_and_type("Group", "coders").unwrap(), None),
1013 ast::Context::empty(),
1014 Some(&schema()),
1015 Extensions::all_available(),
1016 ),
1017 Err(e) => {
1018 expect_err(
1019 "",
1020 &miette::Report::new(e),
1021 &ExpectedErrorMessageBuilder::error(r#"resource type `Group` is not valid for `Action::"view_photo"`"#)
1022 .help(r#"valid resource types for `Action::"view_photo"`: `Photo`"#)
1023 .build(),
1024 );
1025 }
1026 );
1027 }
1028
1029 #[test]
1031 fn context_missing_attribute() {
1032 assert_matches!(
1033 ast::Request::new(
1034 (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
1035 (ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(), None),
1036 (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
1037 ast::Context::empty(),
1038 Some(&schema()),
1039 Extensions::all_available(),
1040 ),
1041 Err(e) => {
1042 expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(r#"context `{}` is not valid for `Action::"edit_photo"`"#).build());
1043 }
1044 );
1045 }
1046
1047 #[test]
1049 fn context_extra_attribute() {
1050 let context_with_extra_attr = ast::Context::from_pairs(
1051 [
1052 ("admin_approval".into(), ast::RestrictedExpr::val(true)),
1053 ("extra".into(), ast::RestrictedExpr::val(42)),
1054 ],
1055 Extensions::all_available(),
1056 )
1057 .unwrap();
1058 assert_matches!(
1059 ast::Request::new(
1060 (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
1061 (ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(), None),
1062 (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
1063 context_with_extra_attr,
1064 Some(&schema()),
1065 Extensions::all_available(),
1066 ),
1067 Err(e) => {
1068 expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(r#"context `{admin_approval: true, extra: 42}` is not valid for `Action::"edit_photo"`"#).build());
1069 }
1070 );
1071 }
1072
1073 #[test]
1075 fn context_attribute_wrong_type() {
1076 let context_with_wrong_type_attr = ast::Context::from_pairs(
1077 [(
1078 "admin_approval".into(),
1079 ast::RestrictedExpr::set([ast::RestrictedExpr::val(true)]),
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 context_with_wrong_type_attr,
1090 Some(&schema()),
1091 Extensions::all_available(),
1092 ),
1093 Err(e) => {
1094 expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(r#"context `{admin_approval: [true]}` is not valid for `Action::"edit_photo"`"#).build());
1095 }
1096 );
1097 }
1098
1099 #[test]
1101 fn context_attribute_heterogeneous_set() {
1102 let context_with_heterogeneous_set = ast::Context::from_pairs(
1103 [(
1104 "admin_approval".into(),
1105 ast::RestrictedExpr::set([
1106 ast::RestrictedExpr::val(true),
1107 ast::RestrictedExpr::val(-1001),
1108 ]),
1109 )],
1110 Extensions::all_available(),
1111 )
1112 .unwrap();
1113 assert_matches!(
1114 ast::Request::new(
1115 (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
1116 (ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(), None),
1117 (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
1118 context_with_heterogeneous_set,
1119 Some(&schema()),
1120 Extensions::all_available(),
1121 ),
1122 Err(e) => {
1123 expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(r#"context `{admin_approval: [true, -1001]}` is not valid for `Action::"edit_photo"`"#).build());
1124 }
1125 );
1126 }
1127
1128 #[test]
1130 fn context_large() {
1131 let large_context_with_extra_attributes = ast::Context::from_pairs(
1132 [
1133 ("admin_approval".into(), ast::RestrictedExpr::val(true)),
1134 ("extra1".into(), ast::RestrictedExpr::val(false)),
1135 ("also extra".into(), ast::RestrictedExpr::val("spam")),
1136 (
1137 "extra2".into(),
1138 ast::RestrictedExpr::set([ast::RestrictedExpr::val(-100)]),
1139 ),
1140 (
1141 "extra3".into(),
1142 ast::RestrictedExpr::val(
1143 ast::EntityUID::with_eid_and_type("User", "alice").unwrap(),
1144 ),
1145 ),
1146 ("extra4".into(), ast::RestrictedExpr::val("foobar")),
1147 ],
1148 Extensions::all_available(),
1149 )
1150 .unwrap();
1151 assert_matches!(
1152 ast::Request::new(
1153 (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
1154 (ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(), None),
1155 (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
1156 large_context_with_extra_attributes,
1157 Some(&schema()),
1158 Extensions::all_available(),
1159 ),
1160 Err(e) => {
1161 expect_err(
1162 "",
1163 &miette::Report::new(e),
1164 &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(),
1165 );
1166 }
1167 );
1168 }
1169
1170 #[test]
1171 fn enumerated_entity_type() {
1172 assert_matches!(
1173 ast::Request::new(
1174 (
1175 ast::EntityUID::with_eid_and_type("People", "😋").unwrap(),
1176 None
1177 ),
1178 (
1179 ast::EntityUID::with_eid_and_type("Action", "eat").unwrap(),
1180 None
1181 ),
1182 (
1183 ast::EntityUID::with_eid_and_type("Fruit", "🍉").unwrap(),
1184 None
1185 ),
1186 Context::empty(),
1187 Some(&schema_with_enums()),
1188 Extensions::none(),
1189 ),
1190 Ok(_)
1191 );
1192 assert_matches!(
1193 ast::Request::new(
1194 (ast::EntityUID::with_eid_and_type("People", "🤔").unwrap(), None),
1195 (ast::EntityUID::with_eid_and_type("Action", "eat").unwrap(), None),
1196 (ast::EntityUID::with_eid_and_type("Fruit", "🥝").unwrap(), None),
1197 Context::empty(),
1198 Some(&schema_with_enums()),
1199 Extensions::none(),
1200 ),
1201 Err(e) => {
1202 expect_err(
1203 "",
1204 &miette::Report::new(e),
1205 &ExpectedErrorMessageBuilder::error(r#"entity `Fruit::"🥝"` is of an enumerated entity type, but `"🥝"` is not declared as a valid eid"#).help(r#"valid entity eids: "🍉", "🍓", "🍒""#)
1206 .build(),
1207 );
1208 }
1209 );
1210 assert_matches!(
1211 ast::Request::new(
1212 (ast::EntityUID::with_eid_and_type("People", "🤔").unwrap(), None),
1213 (ast::EntityUID::with_eid_and_type("Action", "eat").unwrap(), None),
1214 (ast::EntityUID::with_eid_and_type("Fruit", "🍉").unwrap(), None),
1215 Context::from_pairs([("fruit".into(), (Value::from(ast::EntityUID::with_eid_and_type("Fruit", "🥭").unwrap())).into())], Extensions::none()).expect("should be a valid context"),
1216 Some(&schema_with_enums()),
1217 Extensions::none(),
1218 ),
1219 Err(e) => {
1220 expect_err(
1221 "",
1222 &miette::Report::new(e),
1223 &ExpectedErrorMessageBuilder::error(r#"entity `Fruit::"🥭"` is of an enumerated entity type, but `"🥭"` is not declared as a valid eid"#).help(r#"valid entity eids: "🍉", "🍓", "🍒""#)
1224 .build(),
1225 );
1226 }
1227 );
1228 }
1229}