1use crate::{ValidatorEntityType, ValidatorSchema};
2use cedar_policy_core::entities::GetSchemaTypeError;
3use cedar_policy_core::extensions::Extensions;
4use cedar_policy_core::{ast, entities};
5use smol_str::SmolStr;
6use std::collections::{HashMap, HashSet};
7use std::sync::Arc;
8use thiserror::Error;
9
10pub struct CoreSchema<'a> {
12 schema: &'a ValidatorSchema,
14 actions: HashMap<ast::EntityUID, Arc<ast::Entity>>,
20}
21
22impl<'a> CoreSchema<'a> {
23 pub fn new(schema: &'a ValidatorSchema) -> Self {
24 Self {
25 actions: schema
26 .action_entities_iter()
27 .map(|e| (e.uid(), Arc::new(e)))
28 .collect(),
29 schema,
30 }
31 }
32}
33
34impl<'a> entities::Schema for CoreSchema<'a> {
35 type EntityTypeDescription = EntityTypeDescription;
36 type ActionEntityIterator = Vec<Arc<ast::Entity>>;
37
38 fn entity_type(&self, entity_type: &ast::EntityType) -> Option<EntityTypeDescription> {
39 match entity_type {
40 ast::EntityType::Unspecified => None, ast::EntityType::Specified(name) => EntityTypeDescription::new(self.schema, name),
42 }
43 }
44
45 fn action(&self, action: &ast::EntityUID) -> Option<Arc<ast::Entity>> {
46 self.actions.get(action).map(Arc::clone)
47 }
48
49 fn entity_types_with_basename<'b>(
50 &'b self,
51 basename: &'b ast::Id,
52 ) -> Box<dyn Iterator<Item = ast::EntityType> + 'b> {
53 Box::new(self.schema.entity_types().filter_map(move |(name, _)| {
54 if name.basename() == basename {
55 Some(ast::EntityType::Specified(name.clone()))
56 } else {
57 None
58 }
59 }))
60 }
61
62 fn action_entities(&self) -> Self::ActionEntityIterator {
63 self.actions.values().map(Arc::clone).collect()
64 }
65}
66
67pub struct EntityTypeDescription {
69 core_type: ast::EntityType,
71 validator_type: ValidatorEntityType,
73 allowed_parent_types: Arc<HashSet<ast::EntityType>>,
76}
77
78impl EntityTypeDescription {
79 pub fn new(schema: &ValidatorSchema, type_name: &ast::Name) -> Option<Self> {
82 Some(Self {
83 core_type: ast::EntityType::Specified(type_name.clone()),
84 validator_type: schema.get_entity_type(type_name).cloned()?,
85 allowed_parent_types: {
86 let mut set = HashSet::new();
87 for (possible_parent_typename, possible_parent_et) in schema.entity_types() {
88 if possible_parent_et.descendants.contains(type_name) {
89 set.insert(ast::EntityType::Specified(possible_parent_typename.clone()));
90 }
91 }
92 Arc::new(set)
93 },
94 })
95 }
96}
97
98impl entities::EntityTypeDescription for EntityTypeDescription {
99 fn entity_type(&self) -> ast::EntityType {
100 self.core_type.clone()
101 }
102
103 fn attr_type(&self, attr: &str) -> Option<entities::SchemaType> {
104 let attr_type: &crate::types::Type = &self.validator_type.attr(attr)?.attr_type;
105 #[allow(clippy::expect_used)]
110 let core_schema_type: entities::SchemaType = attr_type
111 .clone()
112 .try_into()
113 .expect("failed to convert validator type into Core SchemaType");
114 debug_assert!(attr_type.is_consistent_with(&core_schema_type));
115 Some(core_schema_type)
116 }
117
118 fn required_attrs<'s>(&'s self) -> Box<dyn Iterator<Item = SmolStr> + 's> {
119 Box::new(
120 self.validator_type
121 .attributes
122 .iter()
123 .filter(|(_, ty)| ty.is_required)
124 .map(|(attr, _)| attr.clone()),
125 )
126 }
127
128 fn allowed_parent_types(&self) -> Arc<HashSet<ast::EntityType>> {
129 Arc::clone(&self.allowed_parent_types)
130 }
131
132 fn open_attributes(&self) -> bool {
133 self.validator_type.open_attributes.is_open()
134 }
135}
136
137impl ast::RequestSchema for ValidatorSchema {
138 type Error = RequestValidationError;
139 fn validate_request(
140 &self,
141 request: &ast::Request,
142 extensions: Extensions<'_>,
143 ) -> std::result::Result<(), Self::Error> {
144 use ast::EntityUIDEntry;
145 if let EntityUIDEntry::Known(principal) = request.principal() {
149 match principal.entity_type() {
150 ast::EntityType::Specified(name) => {
151 if self.get_entity_type(name).is_none() {
152 return Err(RequestValidationError::UndeclaredPrincipalType {
153 principal_ty: principal.entity_type().clone(),
154 });
155 }
156 }
157 ast::EntityType::Unspecified => {} }
159 }
160 if let EntityUIDEntry::Known(resource) = request.resource() {
161 match resource.entity_type() {
162 ast::EntityType::Specified(name) => {
163 if self.get_entity_type(name).is_none() {
164 return Err(RequestValidationError::UndeclaredResourceType {
165 resource_ty: resource.entity_type().clone(),
166 });
167 }
168 }
169 ast::EntityType::Unspecified => {} }
171 }
172
173 match request.action() {
175 EntityUIDEntry::Known(action) => {
176 let validator_action_id = self.get_action_id(action).ok_or_else(|| {
177 RequestValidationError::UndeclaredAction {
178 action: Arc::clone(action),
179 }
180 })?;
181 if let EntityUIDEntry::Known(principal) = request.principal() {
182 if !validator_action_id
183 .applies_to
184 .is_applicable_principal_type(principal.entity_type())
185 {
186 return Err(RequestValidationError::InvalidPrincipalType {
187 principal_ty: principal.entity_type().clone(),
188 action: Arc::clone(action),
189 });
190 }
191 }
192 if let EntityUIDEntry::Known(resource) = request.resource() {
193 if !validator_action_id
194 .applies_to
195 .is_applicable_resource_type(resource.entity_type())
196 {
197 return Err(RequestValidationError::InvalidResourceType {
198 resource_ty: resource.entity_type().clone(),
199 action: Arc::clone(action),
200 });
201 }
202 }
203 if let Some(context) = request.context() {
204 let expected_context_ty = validator_action_id.context_type();
205 if !expected_context_ty
206 .typecheck_partial_value(context.as_ref(), extensions)
207 .map_err(RequestValidationError::TypeOfContext)?
208 {
209 return Err(RequestValidationError::InvalidContext {
210 context: context.clone(),
211 action: Arc::clone(action),
212 });
213 }
214 }
215 }
216 EntityUIDEntry::Unknown => {
217 }
224 }
225 Ok(())
226 }
227}
228
229impl<'a> ast::RequestSchema for CoreSchema<'a> {
230 type Error = RequestValidationError;
231 fn validate_request(
232 &self,
233 request: &ast::Request,
234 extensions: Extensions<'_>,
235 ) -> Result<(), Self::Error> {
236 self.schema.validate_request(request, extensions)
237 }
238}
239
240#[derive(Debug, Error)]
241pub enum RequestValidationError {
242 #[error("request's action `{action}` is not declared in the schema")]
244 UndeclaredAction {
245 action: Arc<ast::EntityUID>,
247 },
248 #[error("principal type `{principal_ty}` is not declared in the schema")]
250 UndeclaredPrincipalType {
251 principal_ty: ast::EntityType,
253 },
254 #[error("resource type `{resource_ty}` is not declared in the schema")]
256 UndeclaredResourceType {
257 resource_ty: ast::EntityType,
259 },
260 #[error("principal type `{principal_ty}` is not valid for `{action}`")]
263 InvalidPrincipalType {
264 principal_ty: ast::EntityType,
266 action: Arc<ast::EntityUID>,
268 },
269 #[error("resource type `{resource_ty}` is not valid for `{action}`")]
272 InvalidResourceType {
273 resource_ty: ast::EntityType,
275 action: Arc<ast::EntityUID>,
277 },
278 #[error("context `{context}` is not valid for `{action}`")]
280 InvalidContext {
281 context: ast::Context,
283 action: Arc<ast::EntityUID>,
285 },
286 #[error("context is not valid: {0}")]
289 TypeOfContext(GetSchemaTypeError),
290}
291
292pub struct ContextSchema(
295 crate::types::Type,
298);
299
300impl entities::ContextSchema for ContextSchema {
302 fn context_type(&self) -> entities::SchemaType {
303 #[allow(clippy::expect_used)]
305 self.0
306 .clone()
307 .try_into()
308 .expect("failed to convert validator type into Core SchemaType")
309 }
310}
311
312pub fn context_schema_for_action(
317 schema: &ValidatorSchema,
318 action: &ast::EntityUID,
319) -> Option<ContextSchema> {
320 schema.context_type(action).map(ContextSchema)
327}
328
329#[cfg(test)]
330mod test {
331 use super::*;
332 use cool_asserts::assert_matches;
333 use serde_json::json;
334
335 fn schema() -> ValidatorSchema {
336 let src = json!(
337 { "": {
338 "entityTypes": {
339 "User": {
340 "memberOfTypes": [ "Group" ]
341 },
342 "Group": {
343 "memberOfTypes": []
344 },
345 "Photo": {
346 "memberOfTypes": [ "Album" ]
347 },
348 "Album": {
349 "memberOfTypes": []
350 }
351 },
352 "actions": {
353 "view_photo": {
354 "appliesTo": {
355 "principalTypes": ["User", "Group"],
356 "resourceTypes": ["Photo"]
357 }
358 },
359 "edit_photo": {
360 "appliesTo": {
361 "principalTypes": ["User", "Group"],
362 "resourceTypes": ["Photo"],
363 "context": {
364 "type": "Record",
365 "attributes": {
366 "admin_approval": {
367 "type": "Boolean",
368 "required": true,
369 }
370 }
371 }
372 }
373 }
374 }
375 }});
376 ValidatorSchema::from_json_value(src, Extensions::all_available())
377 .expect("failed to create ValidatorSchema")
378 }
379
380 #[test]
382 fn success_concrete_request_no_context() {
383 assert_matches!(
384 ast::Request::new(
385 ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
386 ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(),
387 ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
388 ast::Context::empty(),
389 Some(&schema()),
390 Extensions::all_available(),
391 ),
392 Ok(_)
393 );
394 }
395
396 #[test]
398 fn success_concrete_request_with_context() {
399 assert_matches!(
400 ast::Request::new(
401 ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
402 ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(),
403 ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
404 ast::Context::from_pairs(
405 [("admin_approval".into(), ast::RestrictedExpr::val(true))],
406 Extensions::all_available()
407 )
408 .unwrap(),
409 Some(&schema()),
410 Extensions::all_available(),
411 ),
412 Ok(_)
413 );
414 }
415
416 #[test]
418 fn success_principal_unknown() {
419 assert_matches!(
420 ast::Request::new_with_unknowns(
421 ast::EntityUIDEntry::Unknown,
422 ast::EntityUIDEntry::concrete(
423 ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap()
424 ),
425 ast::EntityUIDEntry::concrete(
426 ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap()
427 ),
428 Some(ast::Context::empty()),
429 Some(&schema()),
430 Extensions::all_available(),
431 ),
432 Ok(_)
433 );
434 }
435
436 #[test]
438 fn success_action_unknown() {
439 assert_matches!(
440 ast::Request::new_with_unknowns(
441 ast::EntityUIDEntry::concrete(
442 ast::EntityUID::with_eid_and_type("User", "abc123").unwrap()
443 ),
444 ast::EntityUIDEntry::Unknown,
445 ast::EntityUIDEntry::concrete(
446 ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap()
447 ),
448 Some(ast::Context::empty()),
449 Some(&schema()),
450 Extensions::all_available(),
451 ),
452 Ok(_)
453 );
454 }
455
456 #[test]
458 fn success_resource_unknown() {
459 assert_matches!(
460 ast::Request::new_with_unknowns(
461 ast::EntityUIDEntry::concrete(
462 ast::EntityUID::with_eid_and_type("User", "abc123").unwrap()
463 ),
464 ast::EntityUIDEntry::concrete(
465 ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap()
466 ),
467 ast::EntityUIDEntry::Unknown,
468 Some(ast::Context::empty()),
469 Some(&schema()),
470 Extensions::all_available(),
471 ),
472 Ok(_)
473 );
474 }
475
476 #[test]
478 fn success_context_unknown() {
479 assert_matches!(
480 ast::Request::new_with_unknowns(
481 ast::EntityUIDEntry::concrete(
482 ast::EntityUID::with_eid_and_type("User", "abc123").unwrap()
483 ),
484 ast::EntityUIDEntry::concrete(
485 ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap()
486 ),
487 ast::EntityUIDEntry::concrete(
488 ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap()
489 ),
490 None,
491 Some(&schema()),
492 Extensions::all_available(),
493 ),
494 Ok(_)
495 )
496 }
497
498 #[test]
500 fn success_everything_unspecified() {
501 assert_matches!(
502 ast::Request::new_with_unknowns(
503 ast::EntityUIDEntry::Unknown,
504 ast::EntityUIDEntry::Unknown,
505 ast::EntityUIDEntry::Unknown,
506 None,
507 Some(&schema()),
508 Extensions::all_available(),
509 ),
510 Ok(_)
511 );
512 }
513
514 #[test]
518 fn success_unknown_action_but_invalid_types() {
519 assert_matches!(
520 ast::Request::new_with_unknowns(
521 ast::EntityUIDEntry::concrete(
522 ast::EntityUID::with_eid_and_type("Album", "abc123").unwrap()
523 ),
524 ast::EntityUIDEntry::Unknown,
525 ast::EntityUIDEntry::concrete(
526 ast::EntityUID::with_eid_and_type("User", "alice").unwrap()
527 ),
528 None,
529 Some(&schema()),
530 Extensions::all_available(),
531 ),
532 Ok(_)
533 );
534 }
535
536 #[test]
538 fn action_not_declared() {
539 assert_matches!(
540 ast::Request::new(
541 ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
542 ast::EntityUID::with_eid_and_type("Action", "destroy").unwrap(),
543 ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
544 ast::Context::empty(),
545 Some(&schema()),
546 Extensions::all_available(),
547 ),
548 Err(RequestValidationError::UndeclaredAction { action }) => {
549 assert_eq!(&*action, &ast::EntityUID::with_eid_and_type("Action", "destroy").unwrap());
550 }
551 );
552 }
553
554 #[test]
556 fn action_unspecified() {
557 assert_matches!(
558 ast::Request::new(
559 ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
560 ast::EntityUID::unspecified_from_eid(ast::Eid::new("blahblah")),
561 ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
562 ast::Context::empty(),
563 Some(&schema()),
564 Extensions::all_available(),
565 ),
566 Err(RequestValidationError::UndeclaredAction { action }) => {
567 assert_eq!(&*action, &ast::EntityUID::unspecified_from_eid(ast::Eid::new("blahblah")));
568 }
569 );
570 }
571
572 #[test]
574 fn principal_type_not_declared() {
575 assert_matches!(
576 ast::Request::new(
577 ast::EntityUID::with_eid_and_type("Foo", "abc123").unwrap(),
578 ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(),
579 ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
580 ast::Context::empty(),
581 Some(&schema()),
582 Extensions::all_available(),
583 ),
584 Err(RequestValidationError::UndeclaredPrincipalType { principal_ty }) => {
585 assert_eq!(principal_ty, ast::EntityType::Specified(ast::Name::parse_unqualified_name("Foo").unwrap()));
586 }
587 );
588 }
589
590 #[test]
592 fn principal_type_not_declared_action_unspecified() {
593 assert_matches!(
594 ast::Request::new(
595 ast::EntityUID::with_eid_and_type("Foo", "abc123").unwrap(),
596 ast::EntityUID::unspecified_from_eid(ast::Eid::new("blahblah")),
597 ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
598 ast::Context::empty(),
599 Some(&schema()),
600 Extensions::all_available(),
601 ),
602 Err(RequestValidationError::UndeclaredPrincipalType { principal_ty }) => {
603 assert_eq!(principal_ty, ast::EntityType::Specified(ast::Name::parse_unqualified_name("Foo").unwrap()));
604 }
605 );
606 }
607
608 #[test]
610 fn principal_unspecified() {
611 assert_matches!(
612 ast::Request::new(
613 ast::EntityUID::unspecified_from_eid(ast::Eid::new("principal")),
614 ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(),
615 ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
616 ast::Context::empty(),
617 Some(&schema()),
618 Extensions::all_available(),
619 ),
620 Err(RequestValidationError::InvalidPrincipalType { principal_ty, .. }) => {
621 assert_eq!(principal_ty, ast::EntityType::Unspecified);
622 }
623 );
624 }
625
626 #[test]
628 fn resource_type_not_declared() {
629 assert_matches!(
630 ast::Request::new(
631 ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
632 ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(),
633 ast::EntityUID::with_eid_and_type("Foo", "vacationphoto94.jpg").unwrap(),
634 ast::Context::empty(),
635 Some(&schema()),
636 Extensions::all_available(),
637 ),
638 Err(RequestValidationError::UndeclaredResourceType { resource_ty }) => {
639 assert_eq!(resource_ty, ast::EntityType::Specified(ast::Name::parse_unqualified_name("Foo").unwrap()));
640 }
641 );
642 }
643
644 #[test]
646 fn resource_type_not_declared_action_unspecified() {
647 assert_matches!(
648 ast::Request::new(
649 ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
650 ast::EntityUID::unspecified_from_eid(ast::Eid::new("blahblah")),
651 ast::EntityUID::with_eid_and_type("Foo", "vacationphoto94.jpg").unwrap(),
652 ast::Context::empty(),
653 Some(&schema()),
654 Extensions::all_available(),
655 ),
656 Err(RequestValidationError::UndeclaredResourceType { resource_ty }) => {
657 assert_eq!(resource_ty, ast::EntityType::Specified(ast::Name::parse_unqualified_name("Foo").unwrap()));
658 }
659 );
660 }
661
662 #[test]
664 fn resource_unspecified() {
665 assert_matches!(
666 ast::Request::new(
667 ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
668 ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(),
669 ast::EntityUID::unspecified_from_eid(ast::Eid::new("resource")),
670 ast::Context::empty(),
671 Some(&schema()),
672 Extensions::all_available(),
673 ),
674 Err(RequestValidationError::InvalidResourceType { resource_ty, .. }) => {
675 assert_eq!(resource_ty, ast::EntityType::Unspecified);
676 }
677 );
678 }
679
680 #[test]
682 fn principal_type_invalid() {
683 assert_matches!(
684 ast::Request::new(
685 ast::EntityUID::with_eid_and_type("Album", "abc123").unwrap(),
686 ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(),
687 ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
688 ast::Context::empty(),
689 Some(&schema()),
690 Extensions::all_available(),
691 ),
692 Err(RequestValidationError::InvalidPrincipalType { principal_ty, action }) => {
693 assert_eq!(principal_ty, ast::EntityType::Specified(ast::Name::parse_unqualified_name("Album").unwrap()));
694 assert_eq!(&*action, &ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap());
695 }
696 );
697 }
698
699 #[test]
701 fn resource_type_invalid() {
702 assert_matches!(
703 ast::Request::new(
704 ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
705 ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(),
706 ast::EntityUID::with_eid_and_type("Group", "coders").unwrap(),
707 ast::Context::empty(),
708 Some(&schema()),
709 Extensions::all_available(),
710 ),
711 Err(RequestValidationError::InvalidResourceType { resource_ty, action }) => {
712 assert_eq!(resource_ty, ast::EntityType::Specified(ast::Name::parse_unqualified_name("Group").unwrap()));
713 assert_eq!(&*action, &ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap());
714 }
715 );
716 }
717
718 #[test]
720 fn context_missing_attribute() {
721 assert_matches!(
722 ast::Request::new(
723 ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
724 ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(),
725 ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
726 ast::Context::empty(),
727 Some(&schema()),
728 Extensions::all_available(),
729 ),
730 Err(RequestValidationError::InvalidContext { context, action }) => {
731 assert_eq!(context, ast::Context::empty());
732 assert_eq!(&*action, &ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap());
733 }
734 );
735 }
736
737 #[test]
739 fn context_extra_attribute() {
740 let context_with_extra_attr = ast::Context::from_pairs(
741 [
742 ("admin_approval".into(), ast::RestrictedExpr::val(true)),
743 ("extra".into(), ast::RestrictedExpr::val(42)),
744 ],
745 Extensions::all_available(),
746 )
747 .unwrap();
748 assert_matches!(
749 ast::Request::new(
750 ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
751 ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(),
752 ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
753 context_with_extra_attr.clone(),
754 Some(&schema()),
755 Extensions::all_available(),
756 ),
757 Err(RequestValidationError::InvalidContext { context, action }) => {
758 assert_eq!(context, context_with_extra_attr);
759 assert_eq!(&*action, &ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap());
760 }
761 );
762 }
763
764 #[test]
766 fn context_attribute_wrong_type() {
767 let context_with_wrong_type_attr = ast::Context::from_pairs(
768 [(
769 "admin_approval".into(),
770 ast::RestrictedExpr::set([ast::RestrictedExpr::val(true)]),
771 )],
772 Extensions::all_available(),
773 )
774 .unwrap();
775 assert_matches!(
776 ast::Request::new(
777 ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
778 ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(),
779 ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
780 context_with_wrong_type_attr.clone(),
781 Some(&schema()),
782 Extensions::all_available(),
783 ),
784 Err(RequestValidationError::InvalidContext { context, action }) => {
785 assert_eq!(context, context_with_wrong_type_attr);
786 assert_eq!(&*action, &ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap());
787 }
788 );
789 }
790
791 #[test]
793 fn context_attribute_heterogeneous_set() {
794 let context_with_heterogeneous_set = ast::Context::from_pairs(
795 [(
796 "admin_approval".into(),
797 ast::RestrictedExpr::set([
798 ast::RestrictedExpr::val(true),
799 ast::RestrictedExpr::val(-1001),
800 ]),
801 )],
802 Extensions::all_available(),
803 )
804 .unwrap();
805 assert_matches!(
806 ast::Request::new(
807 ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
808 ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(),
809 ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
810 context_with_heterogeneous_set.clone(),
811 Some(&schema()),
812 Extensions::all_available(),
813 ),
814 Err(RequestValidationError::InvalidContext { context, action }) => {
815 assert_eq!(context, context_with_heterogeneous_set);
816 assert_eq!(&*action, &ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap());
817 }
818 );
819 }
820}