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 #[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(
253 &Vec::from(choices.clone().map(Eid::new)),
254 principal,
255 )
256 .map_err(RequestValidationError::InvalidEnumEntity)?;
257 }
258 } else {
259 return Err(request_validation_errors::UndeclaredPrincipalTypeError {
260 principal_ty: principal_type.clone(),
261 }
262 .into());
263 }
264 }
265
266 if let Some(resource) = resource {
268 let resource_type = resource.entity_type();
269 if let Some(et) = self.get_entity_type(resource_type) {
270 if let ValidatorEntityType {
271 kind: ValidatorEntityTypeKind::Enum(choices),
272 ..
273 } = et
274 {
275 is_valid_enumerated_entity(&Vec::from(choices.clone().map(Eid::new)), resource)
276 .map_err(RequestValidationError::InvalidEnumEntity)?;
277 }
278 } else {
279 return Err(request_validation_errors::UndeclaredResourceTypeError {
280 resource_ty: resource_type.clone(),
281 }
282 .into());
283 }
284 }
285
286 if let Some(action) = action {
288 let validator_action_id = self.get_action_id(action).ok_or_else(|| {
289 request_validation_errors::UndeclaredActionError {
290 action: Arc::new(action.clone()),
291 }
292 })?;
293
294 if let Some(principal) = principal {
296 let principal_type = principal.entity_type();
297 validator_action_id
298 .check_principal_type(principal_type, &Arc::new(action.clone()))?;
299 }
300
301 if let Some(resource) = resource {
303 let resource_type = resource.entity_type();
304 validator_action_id
305 .check_resource_type(resource_type, &Arc::new(action.clone()))?;
306 }
307 }
308
309 Ok(())
310 }
311}
312
313impl ValidatorActionId {
314 pub(crate) fn check_principal_type(
315 &self,
316 principal_type: &EntityType,
317 action: &Arc<EntityUID>,
318 ) -> Result<(), request_validation_errors::InvalidPrincipalTypeError> {
319 if !self.is_applicable_principal_type(principal_type) {
320 Err(request_validation_errors::InvalidPrincipalTypeError {
321 principal_ty: principal_type.clone(),
322 action: Arc::clone(action),
323 valid_principal_tys: self.applies_to_principals().cloned().collect(),
324 })
325 } else {
326 Ok(())
327 }
328 }
329
330 pub(crate) fn check_resource_type(
331 &self,
332 resource_type: &EntityType,
333 action: &Arc<EntityUID>,
334 ) -> Result<(), request_validation_errors::InvalidResourceTypeError> {
335 if !self.is_applicable_resource_type(resource_type) {
336 Err(request_validation_errors::InvalidResourceTypeError {
337 resource_ty: resource_type.clone(),
338 action: Arc::clone(action),
339 valid_resource_tys: self.applies_to_resources().cloned().collect(),
340 })
341 } else {
342 Ok(())
343 }
344 }
345}
346
347impl ast::RequestSchema for CoreSchema<'_> {
348 type Error = RequestValidationError;
349 fn validate_request(
350 &self,
351 request: &ast::Request,
352 extensions: &Extensions<'_>,
353 ) -> Result<(), Self::Error> {
354 self.schema.validate_request(request, extensions)
355 }
356
357 fn validate_context<'a>(
359 &self,
360 context: &ast::Context,
361 action: &EntityUID,
362 extensions: &Extensions<'a>,
363 ) -> std::result::Result<(), Self::Error> {
364 self.schema.validate_context(context, action, extensions)
365 }
366
367 fn validate_scope_variables(
369 &self,
370 principal: Option<&EntityUID>,
371 action: Option<&EntityUID>,
372 resource: Option<&EntityUID>,
373 ) -> std::result::Result<(), RequestValidationError> {
374 self.schema
375 .validate_scope_variables(principal, action, resource)
376 }
377}
378
379#[derive(Debug, Diagnostic, Error)]
383pub enum RequestValidationError {
384 #[error(transparent)]
386 #[diagnostic(transparent)]
387 UndeclaredAction(#[from] request_validation_errors::UndeclaredActionError),
388 #[error(transparent)]
390 #[diagnostic(transparent)]
391 UndeclaredPrincipalType(#[from] request_validation_errors::UndeclaredPrincipalTypeError),
392 #[error(transparent)]
394 #[diagnostic(transparent)]
395 UndeclaredResourceType(#[from] request_validation_errors::UndeclaredResourceTypeError),
396 #[error(transparent)]
399 #[diagnostic(transparent)]
400 InvalidPrincipalType(#[from] request_validation_errors::InvalidPrincipalTypeError),
401 #[error(transparent)]
404 #[diagnostic(transparent)]
405 InvalidResourceType(#[from] request_validation_errors::InvalidResourceTypeError),
406 #[error(transparent)]
408 #[diagnostic(transparent)]
409 InvalidContext(#[from] request_validation_errors::InvalidContextError),
410 #[error("context is not valid: {0}")]
413 #[diagnostic(transparent)]
414 TypeOfContext(ExtensionFunctionLookupError),
415 #[error(transparent)]
418 #[diagnostic(transparent)]
419 InvalidEnumEntity(#[from] InvalidEnumEntityError),
420}
421
422pub mod request_validation_errors {
424 use crate::ast;
425 use itertools::Itertools;
426 use miette::Diagnostic;
427 use std::sync::Arc;
428 use thiserror::Error;
429
430 #[derive(Debug, Error)]
432 #[error("request's action `{action}` is not declared in the schema")]
433 pub struct UndeclaredActionError {
434 pub(crate) action: Arc<ast::EntityUID>,
436 }
437
438 impl Diagnostic for UndeclaredActionError {
439 impl_diagnostic_from_method_on_field!(action, loc);
440 }
441
442 impl UndeclaredActionError {
443 pub fn action(&self) -> &ast::EntityUID {
445 &self.action
446 }
447 }
448
449 #[derive(Debug, Error)]
451 #[error("principal type `{principal_ty}` is not declared in the schema")]
452 pub struct UndeclaredPrincipalTypeError {
453 pub(crate) principal_ty: ast::EntityType,
455 }
456
457 impl Diagnostic for UndeclaredPrincipalTypeError {
458 impl_diagnostic_from_method_on_field!(principal_ty, loc);
459 }
460
461 impl UndeclaredPrincipalTypeError {
462 pub fn principal_ty(&self) -> &ast::EntityType {
464 &self.principal_ty
465 }
466 }
467
468 #[derive(Debug, Error)]
470 #[error("resource type `{resource_ty}` is not declared in the schema")]
471 pub struct UndeclaredResourceTypeError {
472 pub(crate) resource_ty: ast::EntityType,
474 }
475
476 impl Diagnostic for UndeclaredResourceTypeError {
477 impl_diagnostic_from_method_on_field!(resource_ty, loc);
478 }
479
480 impl UndeclaredResourceTypeError {
481 pub fn resource_ty(&self) -> &ast::EntityType {
483 &self.resource_ty
484 }
485 }
486
487 #[derive(Debug, Error)]
490 #[error("principal type `{principal_ty}` is not valid for `{action}`")]
491 pub struct InvalidPrincipalTypeError {
492 pub(crate) principal_ty: ast::EntityType,
494 pub(crate) action: Arc<ast::EntityUID>,
496 pub(crate) valid_principal_tys: Vec<ast::EntityType>,
498 }
499
500 impl Diagnostic for InvalidPrincipalTypeError {
501 fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
502 Some(Box::new(invalid_principal_type_help(
503 &self.valid_principal_tys,
504 &self.action,
505 )))
506 }
507
508 impl_diagnostic_from_method_on_field!(principal_ty, loc);
511 }
512
513 fn invalid_principal_type_help(
514 valid_principal_tys: &[ast::EntityType],
515 action: &ast::EntityUID,
516 ) -> String {
517 if valid_principal_tys.is_empty() {
518 format!("no principal types are valid for `{action}`")
519 } else {
520 format!(
521 "valid principal types for `{action}`: {}",
522 valid_principal_tys
523 .iter()
524 .sorted_unstable()
525 .map(|et| format!("`{et}`"))
526 .join(", ")
527 )
528 }
529 }
530
531 impl InvalidPrincipalTypeError {
532 pub fn principal_ty(&self) -> &ast::EntityType {
534 &self.principal_ty
535 }
536
537 pub fn action(&self) -> &ast::EntityUID {
539 &self.action
540 }
541
542 pub fn valid_principal_tys(&self) -> impl Iterator<Item = &ast::EntityType> {
544 self.valid_principal_tys.iter()
545 }
546 }
547
548 #[derive(Debug, Error)]
551 #[error("resource type `{resource_ty}` is not valid for `{action}`")]
552 pub struct InvalidResourceTypeError {
553 pub(super) resource_ty: ast::EntityType,
555 pub(super) action: Arc<ast::EntityUID>,
557 pub(super) valid_resource_tys: Vec<ast::EntityType>,
559 }
560
561 impl Diagnostic for InvalidResourceTypeError {
562 fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
563 Some(Box::new(invalid_resource_type_help(
564 &self.valid_resource_tys,
565 &self.action,
566 )))
567 }
568
569 impl_diagnostic_from_method_on_field!(resource_ty, loc);
572 }
573
574 fn invalid_resource_type_help(
575 valid_resource_tys: &[ast::EntityType],
576 action: &ast::EntityUID,
577 ) -> String {
578 if valid_resource_tys.is_empty() {
579 format!("no resource types are valid for `{action}`")
580 } else {
581 format!(
582 "valid resource types for `{action}`: {}",
583 valid_resource_tys
584 .iter()
585 .sorted_unstable()
586 .map(|et| format!("`{et}`"))
587 .join(", ")
588 )
589 }
590 }
591
592 impl InvalidResourceTypeError {
593 pub fn resource_ty(&self) -> &ast::EntityType {
595 &self.resource_ty
596 }
597
598 pub fn action(&self) -> &ast::EntityUID {
600 &self.action
601 }
602
603 pub fn valid_resource_tys(&self) -> impl Iterator<Item = &ast::EntityType> {
605 self.valid_resource_tys.iter()
606 }
607 }
608
609 #[derive(Debug, Error, Diagnostic)]
611 #[error("context `{}` is not valid for `{action}`", ast::BoundedToString::to_string_bounded(.context, BOUNDEDDISPLAY_BOUND_FOR_INVALID_CONTEXT_ERROR))]
612 pub struct InvalidContextError {
613 pub(crate) context: ast::Context,
615 pub(crate) action: Arc<ast::EntityUID>,
617 }
618
619 const BOUNDEDDISPLAY_BOUND_FOR_INVALID_CONTEXT_ERROR: usize = 5;
620
621 impl InvalidContextError {
622 pub fn context(&self) -> &ast::Context {
624 &self.context
625 }
626
627 pub fn action(&self) -> &ast::EntityUID {
629 &self.action
630 }
631 }
632}
633
634#[derive(Debug, Clone, PartialEq, Eq)]
637pub struct ContextSchema(
638 crate::validator::types::Type,
641);
642
643impl entities::ContextSchema for ContextSchema {
645 fn context_type(&self) -> entities::SchemaType {
646 #[expect(
647 clippy::expect_used,
648 reason = "By `ContextSchema` invariant, `self.0` is representable as a schema type"
649 )]
650 self.0
651 .clone()
652 .try_into()
653 .expect("failed to convert validator type into Core SchemaType")
654 }
655}
656
657pub fn context_schema_for_action(
662 schema: &ValidatorSchema,
663 action: &ast::EntityUID,
664) -> Option<ContextSchema> {
665 schema.context_type(action).cloned().map(ContextSchema)
672}
673
674#[cfg(test)]
675mod test {
676 use super::*;
677 use crate::test_utils::{expect_err, ExpectedErrorMessageBuilder};
678 use ast::{Context, Value};
679 use cool_asserts::assert_matches;
680 use serde_json::json;
681
682 #[track_caller]
683 fn schema_with_enums() -> ValidatorSchema {
684 let src = r#"
685 entity Fruit enum ["🍉", "🍓", "🍒"];
686 entity People;
687 action "eat" appliesTo {
688 principal: [People],
689 resource: [Fruit],
690 context: {
691 fruit?: Fruit,
692 }
693 };
694 "#;
695 ValidatorSchema::from_cedarschema_str(src, Extensions::none())
696 .expect("should be a valid schema")
697 .0
698 }
699
700 fn schema() -> ValidatorSchema {
701 let src = json!(
702 { "": {
703 "entityTypes": {
704 "User": {
705 "memberOfTypes": [ "Group" ]
706 },
707 "Group": {
708 "memberOfTypes": []
709 },
710 "Photo": {
711 "memberOfTypes": [ "Album" ]
712 },
713 "Album": {
714 "memberOfTypes": []
715 }
716 },
717 "actions": {
718 "view_photo": {
719 "appliesTo": {
720 "principalTypes": ["User", "Group"],
721 "resourceTypes": ["Photo"]
722 }
723 },
724 "edit_photo": {
725 "appliesTo": {
726 "principalTypes": ["User", "Group"],
727 "resourceTypes": ["Photo"],
728 "context": {
729 "type": "Record",
730 "attributes": {
731 "admin_approval": {
732 "type": "Boolean",
733 "required": true,
734 }
735 }
736 }
737 }
738 }
739 }
740 }});
741 ValidatorSchema::from_json_value(src, Extensions::all_available())
742 .expect("failed to create ValidatorSchema")
743 }
744
745 #[test]
747 fn success_concrete_request_no_context() {
748 assert_matches!(
749 ast::Request::new(
750 (
751 ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
752 None
753 ),
754 (
755 ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(),
756 None
757 ),
758 (
759 ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
760 None
761 ),
762 ast::Context::empty(),
763 Some(&schema()),
764 Extensions::all_available(),
765 ),
766 Ok(_)
767 );
768 }
769
770 #[test]
772 fn success_concrete_request_with_context() {
773 assert_matches!(
774 ast::Request::new(
775 (
776 ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
777 None
778 ),
779 (
780 ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(),
781 None
782 ),
783 (
784 ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
785 None
786 ),
787 ast::Context::from_pairs(
788 [("admin_approval".into(), ast::RestrictedExpr::val(true))],
789 Extensions::all_available()
790 )
791 .unwrap(),
792 Some(&schema()),
793 Extensions::all_available(),
794 ),
795 Ok(_)
796 );
797 }
798
799 #[test]
801 fn success_principal_unknown() {
802 assert_matches!(
803 ast::Request::new_with_unknowns(
804 ast::EntityUIDEntry::unknown(),
805 ast::EntityUIDEntry::known(
806 ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(),
807 None,
808 ),
809 ast::EntityUIDEntry::known(
810 ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
811 None,
812 ),
813 Some(ast::Context::empty()),
814 Some(&schema()),
815 Extensions::all_available(),
816 ),
817 Ok(_)
818 );
819 }
820
821 #[test]
823 fn success_action_unknown() {
824 assert_matches!(
825 ast::Request::new_with_unknowns(
826 ast::EntityUIDEntry::known(
827 ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
828 None,
829 ),
830 ast::EntityUIDEntry::unknown(),
831 ast::EntityUIDEntry::known(
832 ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
833 None,
834 ),
835 Some(ast::Context::empty()),
836 Some(&schema()),
837 Extensions::all_available(),
838 ),
839 Ok(_)
840 );
841 }
842
843 #[test]
845 fn success_resource_unknown() {
846 assert_matches!(
847 ast::Request::new_with_unknowns(
848 ast::EntityUIDEntry::known(
849 ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
850 None,
851 ),
852 ast::EntityUIDEntry::known(
853 ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(),
854 None,
855 ),
856 ast::EntityUIDEntry::unknown(),
857 Some(ast::Context::empty()),
858 Some(&schema()),
859 Extensions::all_available(),
860 ),
861 Ok(_)
862 );
863 }
864
865 #[test]
867 fn success_context_unknown() {
868 assert_matches!(
869 ast::Request::new_with_unknowns(
870 ast::EntityUIDEntry::known(
871 ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
872 None,
873 ),
874 ast::EntityUIDEntry::known(
875 ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(),
876 None,
877 ),
878 ast::EntityUIDEntry::known(
879 ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
880 None,
881 ),
882 None,
883 Some(&schema()),
884 Extensions::all_available(),
885 ),
886 Ok(_)
887 )
888 }
889
890 #[test]
892 fn success_everything_unspecified() {
893 assert_matches!(
894 ast::Request::new_with_unknowns(
895 ast::EntityUIDEntry::unknown(),
896 ast::EntityUIDEntry::unknown(),
897 ast::EntityUIDEntry::unknown(),
898 None,
899 Some(&schema()),
900 Extensions::all_available(),
901 ),
902 Ok(_)
903 );
904 }
905
906 #[test]
910 fn success_unknown_action_but_invalid_types() {
911 assert_matches!(
912 ast::Request::new_with_unknowns(
913 ast::EntityUIDEntry::known(
914 ast::EntityUID::with_eid_and_type("Album", "abc123").unwrap(),
915 None,
916 ),
917 ast::EntityUIDEntry::unknown(),
918 ast::EntityUIDEntry::known(
919 ast::EntityUID::with_eid_and_type("User", "alice").unwrap(),
920 None,
921 ),
922 None,
923 Some(&schema()),
924 Extensions::all_available(),
925 ),
926 Ok(_)
927 );
928 }
929
930 #[test]
932 fn action_not_declared() {
933 assert_matches!(
934 ast::Request::new(
935 (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
936 (ast::EntityUID::with_eid_and_type("Action", "destroy").unwrap(), None),
937 (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
938 ast::Context::empty(),
939 Some(&schema()),
940 Extensions::all_available(),
941 ),
942 Err(e) => {
943 expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(r#"request's action `Action::"destroy"` is not declared in the schema"#).build());
944 }
945 );
946 }
947
948 #[test]
950 fn principal_type_not_declared() {
951 assert_matches!(
952 ast::Request::new(
953 (ast::EntityUID::with_eid_and_type("Foo", "abc123").unwrap(), None),
954 (ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(), None),
955 (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
956 ast::Context::empty(),
957 Some(&schema()),
958 Extensions::all_available(),
959 ),
960 Err(e) => {
961 expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error("principal type `Foo` is not declared in the schema").build());
962 }
963 );
964 }
965
966 #[test]
968 fn resource_type_not_declared() {
969 assert_matches!(
970 ast::Request::new(
971 (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
972 (ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(), None),
973 (ast::EntityUID::with_eid_and_type("Foo", "vacationphoto94.jpg").unwrap(), None),
974 ast::Context::empty(),
975 Some(&schema()),
976 Extensions::all_available(),
977 ),
978 Err(e) => {
979 expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error("resource type `Foo` is not declared in the schema").build());
980 }
981 );
982 }
983
984 #[test]
986 fn principal_type_invalid() {
987 assert_matches!(
988 ast::Request::new(
989 (ast::EntityUID::with_eid_and_type("Album", "abc123").unwrap(), None),
990 (ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(), None),
991 (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
992 ast::Context::empty(),
993 Some(&schema()),
994 Extensions::all_available(),
995 ),
996 Err(e) => {
997 expect_err(
998 "",
999 &miette::Report::new(e),
1000 &ExpectedErrorMessageBuilder::error(r#"principal type `Album` is not valid for `Action::"view_photo"`"#)
1001 .help(r#"valid principal types for `Action::"view_photo"`: `Group`, `User`"#)
1002 .build(),
1003 );
1004 }
1005 );
1006 }
1007
1008 #[test]
1010 fn resource_type_invalid() {
1011 assert_matches!(
1012 ast::Request::new(
1013 (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
1014 (ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(), None),
1015 (ast::EntityUID::with_eid_and_type("Group", "coders").unwrap(), None),
1016 ast::Context::empty(),
1017 Some(&schema()),
1018 Extensions::all_available(),
1019 ),
1020 Err(e) => {
1021 expect_err(
1022 "",
1023 &miette::Report::new(e),
1024 &ExpectedErrorMessageBuilder::error(r#"resource type `Group` is not valid for `Action::"view_photo"`"#)
1025 .help(r#"valid resource types for `Action::"view_photo"`: `Photo`"#)
1026 .build(),
1027 );
1028 }
1029 );
1030 }
1031
1032 #[test]
1034 fn context_missing_attribute() {
1035 assert_matches!(
1036 ast::Request::new(
1037 (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
1038 (ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(), None),
1039 (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
1040 ast::Context::empty(),
1041 Some(&schema()),
1042 Extensions::all_available(),
1043 ),
1044 Err(e) => {
1045 expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(r#"context `{}` is not valid for `Action::"edit_photo"`"#).build());
1046 }
1047 );
1048 }
1049
1050 #[test]
1052 fn context_extra_attribute() {
1053 let context_with_extra_attr = ast::Context::from_pairs(
1054 [
1055 ("admin_approval".into(), ast::RestrictedExpr::val(true)),
1056 ("extra".into(), ast::RestrictedExpr::val(42)),
1057 ],
1058 Extensions::all_available(),
1059 )
1060 .unwrap();
1061 assert_matches!(
1062 ast::Request::new(
1063 (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
1064 (ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(), None),
1065 (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
1066 context_with_extra_attr,
1067 Some(&schema()),
1068 Extensions::all_available(),
1069 ),
1070 Err(e) => {
1071 expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(r#"context `{admin_approval: true, extra: 42}` is not valid for `Action::"edit_photo"`"#).build());
1072 }
1073 );
1074 }
1075
1076 #[test]
1078 fn context_attribute_wrong_type() {
1079 let context_with_wrong_type_attr = ast::Context::from_pairs(
1080 [(
1081 "admin_approval".into(),
1082 ast::RestrictedExpr::set([ast::RestrictedExpr::val(true)]),
1083 )],
1084 Extensions::all_available(),
1085 )
1086 .unwrap();
1087 assert_matches!(
1088 ast::Request::new(
1089 (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
1090 (ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(), None),
1091 (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
1092 context_with_wrong_type_attr,
1093 Some(&schema()),
1094 Extensions::all_available(),
1095 ),
1096 Err(e) => {
1097 expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(r#"context `{admin_approval: [true]}` is not valid for `Action::"edit_photo"`"#).build());
1098 }
1099 );
1100 }
1101
1102 #[test]
1104 fn context_attribute_heterogeneous_set() {
1105 let context_with_heterogeneous_set = ast::Context::from_pairs(
1106 [(
1107 "admin_approval".into(),
1108 ast::RestrictedExpr::set([
1109 ast::RestrictedExpr::val(true),
1110 ast::RestrictedExpr::val(-1001),
1111 ]),
1112 )],
1113 Extensions::all_available(),
1114 )
1115 .unwrap();
1116 assert_matches!(
1117 ast::Request::new(
1118 (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
1119 (ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(), None),
1120 (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
1121 context_with_heterogeneous_set,
1122 Some(&schema()),
1123 Extensions::all_available(),
1124 ),
1125 Err(e) => {
1126 expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(r#"context `{admin_approval: [true, -1001]}` is not valid for `Action::"edit_photo"`"#).build());
1127 }
1128 );
1129 }
1130
1131 #[test]
1133 fn context_large() {
1134 let large_context_with_extra_attributes = ast::Context::from_pairs(
1135 [
1136 ("admin_approval".into(), ast::RestrictedExpr::val(true)),
1137 ("extra1".into(), ast::RestrictedExpr::val(false)),
1138 ("also extra".into(), ast::RestrictedExpr::val("spam")),
1139 (
1140 "extra2".into(),
1141 ast::RestrictedExpr::set([ast::RestrictedExpr::val(-100)]),
1142 ),
1143 (
1144 "extra3".into(),
1145 ast::RestrictedExpr::val(
1146 ast::EntityUID::with_eid_and_type("User", "alice").unwrap(),
1147 ),
1148 ),
1149 ("extra4".into(), ast::RestrictedExpr::val("foobar")),
1150 ],
1151 Extensions::all_available(),
1152 )
1153 .unwrap();
1154 assert_matches!(
1155 ast::Request::new(
1156 (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
1157 (ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(), None),
1158 (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
1159 large_context_with_extra_attributes,
1160 Some(&schema()),
1161 Extensions::all_available(),
1162 ),
1163 Err(e) => {
1164 expect_err(
1165 "",
1166 &miette::Report::new(e),
1167 &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(),
1168 );
1169 }
1170 );
1171 }
1172
1173 #[test]
1174 fn enumerated_entity_type() {
1175 assert_matches!(
1176 ast::Request::new(
1177 (
1178 ast::EntityUID::with_eid_and_type("People", "😋").unwrap(),
1179 None
1180 ),
1181 (
1182 ast::EntityUID::with_eid_and_type("Action", "eat").unwrap(),
1183 None
1184 ),
1185 (
1186 ast::EntityUID::with_eid_and_type("Fruit", "🍉").unwrap(),
1187 None
1188 ),
1189 Context::empty(),
1190 Some(&schema_with_enums()),
1191 Extensions::none(),
1192 ),
1193 Ok(_)
1194 );
1195 assert_matches!(
1196 ast::Request::new(
1197 (ast::EntityUID::with_eid_and_type("People", "🤔").unwrap(), None),
1198 (ast::EntityUID::with_eid_and_type("Action", "eat").unwrap(), None),
1199 (ast::EntityUID::with_eid_and_type("Fruit", "🥝").unwrap(), None),
1200 Context::empty(),
1201 Some(&schema_with_enums()),
1202 Extensions::none(),
1203 ),
1204 Err(e) => {
1205 expect_err(
1206 "",
1207 &miette::Report::new(e),
1208 &ExpectedErrorMessageBuilder::error(r#"entity `Fruit::"🥝"` is of an enumerated entity type, but `"🥝"` is not declared as a valid eid"#).help(r#"valid entity eids: "🍉", "🍓", "🍒""#)
1209 .build(),
1210 );
1211 }
1212 );
1213 assert_matches!(
1214 ast::Request::new(
1215 (ast::EntityUID::with_eid_and_type("People", "🤔").unwrap(), None),
1216 (ast::EntityUID::with_eid_and_type("Action", "eat").unwrap(), None),
1217 (ast::EntityUID::with_eid_and_type("Fruit", "🍉").unwrap(), None),
1218 Context::from_pairs([("fruit".into(), (Value::from(ast::EntityUID::with_eid_and_type("Fruit", "🥭").unwrap())).into())], Extensions::none()).expect("should be a valid context"),
1219 Some(&schema_with_enums()),
1220 Extensions::none(),
1221 ),
1222 Err(e) => {
1223 expect_err(
1224 "",
1225 &miette::Report::new(e),
1226 &ExpectedErrorMessageBuilder::error(r#"entity `Fruit::"🥭"` is of an enumerated entity type, but `"🥭"` is not declared as a valid eid"#).help(r#"valid entity eids: "🍉", "🍓", "🍒""#)
1227 .build(),
1228 );
1229 }
1230 );
1231 }
1232}