1use crate::{ValidatorEntityType, ValidatorSchema};
17use cedar_policy_core::extensions::{ExtensionFunctionLookupError, Extensions};
18use cedar_policy_core::{ast, entities};
19use miette::Diagnostic;
20use smol_str::SmolStr;
21use std::collections::hash_map::Values;
22use std::collections::HashSet;
23use std::iter::Cloned;
24use std::sync::Arc;
25use thiserror::Error;
26
27#[derive(Debug)]
29pub struct CoreSchema<'a> {
30 schema: &'a ValidatorSchema,
32}
33
34impl<'a> CoreSchema<'a> {
35 pub fn new(schema: &'a ValidatorSchema) -> Self {
37 Self { schema }
38 }
39}
40
41impl<'a> entities::Schema for CoreSchema<'a> {
42 type EntityTypeDescription = EntityTypeDescription;
43 type ActionEntityIterator = Cloned<Values<'a, ast::EntityUID, Arc<ast::Entity>>>;
44
45 fn entity_type(&self, entity_type: &ast::EntityType) -> Option<EntityTypeDescription> {
46 EntityTypeDescription::new(self.schema, entity_type)
47 }
48
49 fn action(&self, action: &ast::EntityUID) -> Option<Arc<ast::Entity>> {
50 self.schema.actions.get(action).cloned()
51 }
52
53 fn entity_types_with_basename<'b>(
54 &'b self,
55 basename: &'b ast::UnreservedId,
56 ) -> Box<dyn Iterator<Item = ast::EntityType> + 'b> {
57 Box::new(
58 self.schema
59 .entity_types()
60 .filter_map(move |(entity_type, _)| {
61 if &entity_type.name().basename() == basename {
62 Some(entity_type.clone())
63 } else {
64 None
65 }
66 }),
67 )
68 }
69
70 fn action_entities(&self) -> Self::ActionEntityIterator {
71 self.schema.actions.values().cloned()
72 }
73}
74
75#[derive(Debug)]
77pub struct EntityTypeDescription {
78 core_type: ast::EntityType,
80 validator_type: ValidatorEntityType,
82 allowed_parent_types: Arc<HashSet<ast::EntityType>>,
85}
86
87impl EntityTypeDescription {
88 pub fn new(schema: &ValidatorSchema, type_name: &ast::EntityType) -> Option<Self> {
91 Some(Self {
92 core_type: type_name.clone(),
93 validator_type: schema.get_entity_type(type_name).cloned()?,
94 allowed_parent_types: {
95 let mut set = HashSet::new();
96 for (possible_parent_typename, possible_parent_et) in schema.entity_types() {
97 if possible_parent_et.descendants.contains(type_name) {
98 set.insert(possible_parent_typename.clone());
99 }
100 }
101 Arc::new(set)
102 },
103 })
104 }
105}
106
107impl entities::EntityTypeDescription for EntityTypeDescription {
108 fn entity_type(&self) -> ast::EntityType {
109 self.core_type.clone()
110 }
111
112 fn attr_type(&self, attr: &str) -> Option<entities::SchemaType> {
113 let attr_type: &crate::types::Type = &self.validator_type.attr(attr)?.attr_type;
114 #[allow(clippy::expect_used)]
119 let core_schema_type: entities::SchemaType = attr_type
120 .clone()
121 .try_into()
122 .expect("failed to convert validator type into Core SchemaType");
123 debug_assert!(attr_type.is_consistent_with(&core_schema_type));
124 Some(core_schema_type)
125 }
126
127 fn tag_type(&self) -> Option<entities::SchemaType> {
128 let tag_type: &crate::types::Type = self.validator_type.tag_type()?;
129 #[allow(clippy::expect_used)]
134 let core_schema_type: entities::SchemaType = tag_type
135 .clone()
136 .try_into()
137 .expect("failed to convert validator type into Core SchemaType");
138 debug_assert!(tag_type.is_consistent_with(&core_schema_type));
139 Some(core_schema_type)
140 }
141
142 fn required_attrs<'s>(&'s self) -> Box<dyn Iterator<Item = SmolStr> + 's> {
143 Box::new(
144 self.validator_type
145 .attributes
146 .iter()
147 .filter(|(_, ty)| ty.is_required)
148 .map(|(attr, _)| attr.clone()),
149 )
150 }
151
152 fn allowed_parent_types(&self) -> Arc<HashSet<ast::EntityType>> {
153 Arc::clone(&self.allowed_parent_types)
154 }
155
156 fn open_attributes(&self) -> bool {
157 self.validator_type.open_attributes.is_open()
158 }
159}
160
161impl ast::RequestSchema for ValidatorSchema {
162 type Error = RequestValidationError;
163 fn validate_request(
164 &self,
165 request: &ast::Request,
166 extensions: &Extensions<'_>,
167 ) -> std::result::Result<(), Self::Error> {
168 use ast::EntityUIDEntry;
169 if let EntityUIDEntry::Known {
172 euid: principal, ..
173 } = request.principal()
174 {
175 if self.get_entity_type(principal.entity_type()).is_none() {
176 return Err(request_validation_errors::UndeclaredPrincipalTypeError {
177 principal_ty: principal.entity_type().clone(),
178 }
179 .into());
180 }
181 }
182 if let EntityUIDEntry::Known { euid: resource, .. } = request.resource() {
183 if self.get_entity_type(resource.entity_type()).is_none() {
184 return Err(request_validation_errors::UndeclaredResourceTypeError {
185 resource_ty: resource.entity_type().clone(),
186 }
187 .into());
188 }
189 }
190
191 match request.action() {
193 EntityUIDEntry::Known { euid: action, .. } => {
194 let validator_action_id = self.get_action_id(action).ok_or_else(|| {
195 request_validation_errors::UndeclaredActionError {
196 action: Arc::clone(action),
197 }
198 })?;
199 if let EntityUIDEntry::Known {
200 euid: principal, ..
201 } = request.principal()
202 {
203 if !validator_action_id.is_applicable_principal_type(principal.entity_type()) {
204 return Err(request_validation_errors::InvalidPrincipalTypeError {
205 principal_ty: principal.entity_type().clone(),
206 action: Arc::clone(action),
207 }
208 .into());
209 }
210 }
211 if let EntityUIDEntry::Known { euid: resource, .. } = request.resource() {
212 if !validator_action_id.is_applicable_resource_type(resource.entity_type()) {
213 return Err(request_validation_errors::InvalidResourceTypeError {
214 resource_ty: resource.entity_type().clone(),
215 action: Arc::clone(action),
216 }
217 .into());
218 }
219 }
220 if let Some(context) = request.context() {
221 let expected_context_ty = validator_action_id.context_type();
222 if !expected_context_ty
223 .typecheck_partial_value(&context.clone().into(), extensions)
224 .map_err(RequestValidationError::TypeOfContext)?
225 {
226 return Err(request_validation_errors::InvalidContextError {
227 context: context.clone(),
228 action: Arc::clone(action),
229 }
230 .into());
231 }
232 }
233 }
234 EntityUIDEntry::Unknown { .. } => {
235 }
242 }
243 Ok(())
244 }
245}
246
247impl<'a> ast::RequestSchema for CoreSchema<'a> {
248 type Error = RequestValidationError;
249 fn validate_request(
250 &self,
251 request: &ast::Request,
252 extensions: &Extensions<'_>,
253 ) -> Result<(), Self::Error> {
254 self.schema.validate_request(request, extensions)
255 }
256}
257
258#[derive(Debug, Diagnostic, Error)]
262pub enum RequestValidationError {
263 #[error(transparent)]
265 #[diagnostic(transparent)]
266 UndeclaredAction(#[from] request_validation_errors::UndeclaredActionError),
267 #[error(transparent)]
269 #[diagnostic(transparent)]
270 UndeclaredPrincipalType(#[from] request_validation_errors::UndeclaredPrincipalTypeError),
271 #[error(transparent)]
273 #[diagnostic(transparent)]
274 UndeclaredResourceType(#[from] request_validation_errors::UndeclaredResourceTypeError),
275 #[error(transparent)]
278 #[diagnostic(transparent)]
279 InvalidPrincipalType(#[from] request_validation_errors::InvalidPrincipalTypeError),
280 #[error(transparent)]
283 #[diagnostic(transparent)]
284 InvalidResourceType(#[from] request_validation_errors::InvalidResourceTypeError),
285 #[error(transparent)]
287 #[diagnostic(transparent)]
288 InvalidContext(#[from] request_validation_errors::InvalidContextError),
289 #[error("context is not valid: {0}")]
292 #[diagnostic(transparent)]
293 TypeOfContext(ExtensionFunctionLookupError),
294}
295
296pub mod request_validation_errors {
298 use cedar_policy_core::ast;
299 use miette::Diagnostic;
300 use std::sync::Arc;
301 use thiserror::Error;
302
303 #[derive(Debug, Error, Diagnostic)]
305 #[error("request's action `{action}` is not declared in the schema")]
306 pub struct UndeclaredActionError {
307 pub(super) action: Arc<ast::EntityUID>,
309 }
310
311 impl UndeclaredActionError {
312 pub fn action(&self) -> &ast::EntityUID {
314 &self.action
315 }
316 }
317
318 #[derive(Debug, Error, Diagnostic)]
320 #[error("principal type `{principal_ty}` is not declared in the schema")]
321 pub struct UndeclaredPrincipalTypeError {
322 pub(super) principal_ty: ast::EntityType,
324 }
325
326 impl UndeclaredPrincipalTypeError {
327 pub fn principal_ty(&self) -> &ast::EntityType {
329 &self.principal_ty
330 }
331 }
332
333 #[derive(Debug, Error, Diagnostic)]
335 #[error("resource type `{resource_ty}` is not declared in the schema")]
336 pub struct UndeclaredResourceTypeError {
337 pub(super) resource_ty: ast::EntityType,
339 }
340
341 impl UndeclaredResourceTypeError {
342 pub fn resource_ty(&self) -> &ast::EntityType {
344 &self.resource_ty
345 }
346 }
347
348 #[derive(Debug, Error, Diagnostic)]
351 #[error("principal type `{principal_ty}` is not valid for `{action}`")]
352 pub struct InvalidPrincipalTypeError {
353 pub(super) principal_ty: ast::EntityType,
355 pub(super) action: Arc<ast::EntityUID>,
357 }
358
359 impl InvalidPrincipalTypeError {
360 pub fn principal_ty(&self) -> &ast::EntityType {
362 &self.principal_ty
363 }
364
365 pub fn action(&self) -> &ast::EntityUID {
367 &self.action
368 }
369 }
370
371 #[derive(Debug, Error, Diagnostic)]
374 #[error("resource type `{resource_ty}` is not valid for `{action}`")]
375 pub struct InvalidResourceTypeError {
376 pub(super) resource_ty: ast::EntityType,
378 pub(super) action: Arc<ast::EntityUID>,
380 }
381
382 impl InvalidResourceTypeError {
383 pub fn resource_ty(&self) -> &ast::EntityType {
385 &self.resource_ty
386 }
387
388 pub fn action(&self) -> &ast::EntityUID {
390 &self.action
391 }
392 }
393
394 #[derive(Debug, Error, Diagnostic)]
396 #[error("context `{context}` is not valid for `{action}`")]
397 pub struct InvalidContextError {
398 pub(super) context: ast::Context,
400 pub(super) action: Arc<ast::EntityUID>,
402 }
403
404 impl InvalidContextError {
405 pub fn context(&self) -> &ast::Context {
407 &self.context
408 }
409
410 pub fn action(&self) -> &ast::EntityUID {
412 &self.action
413 }
414 }
415}
416
417#[derive(Debug, Clone, PartialEq)]
420pub struct ContextSchema(
421 crate::types::Type,
424);
425
426impl entities::ContextSchema for ContextSchema {
428 fn context_type(&self) -> entities::SchemaType {
429 #[allow(clippy::expect_used)]
431 self.0
432 .clone()
433 .try_into()
434 .expect("failed to convert validator type into Core SchemaType")
435 }
436}
437
438pub fn context_schema_for_action(
443 schema: &ValidatorSchema,
444 action: &ast::EntityUID,
445) -> Option<ContextSchema> {
446 schema.context_type(action).cloned().map(ContextSchema)
453}
454
455#[cfg(test)]
456mod test {
457 use super::*;
458 use cedar_policy_core::test_utils::{expect_err, ExpectedErrorMessageBuilder};
459 use cool_asserts::assert_matches;
460 use serde_json::json;
461
462 fn schema() -> ValidatorSchema {
463 let src = json!(
464 { "": {
465 "entityTypes": {
466 "User": {
467 "memberOfTypes": [ "Group" ]
468 },
469 "Group": {
470 "memberOfTypes": []
471 },
472 "Photo": {
473 "memberOfTypes": [ "Album" ]
474 },
475 "Album": {
476 "memberOfTypes": []
477 }
478 },
479 "actions": {
480 "view_photo": {
481 "appliesTo": {
482 "principalTypes": ["User", "Group"],
483 "resourceTypes": ["Photo"]
484 }
485 },
486 "edit_photo": {
487 "appliesTo": {
488 "principalTypes": ["User", "Group"],
489 "resourceTypes": ["Photo"],
490 "context": {
491 "type": "Record",
492 "attributes": {
493 "admin_approval": {
494 "type": "Boolean",
495 "required": true,
496 }
497 }
498 }
499 }
500 }
501 }
502 }});
503 ValidatorSchema::from_json_value(src, Extensions::all_available())
504 .expect("failed to create ValidatorSchema")
505 }
506
507 #[test]
509 fn success_concrete_request_no_context() {
510 assert_matches!(
511 ast::Request::new(
512 (
513 ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
514 None
515 ),
516 (
517 ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(),
518 None
519 ),
520 (
521 ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
522 None
523 ),
524 ast::Context::empty(),
525 Some(&schema()),
526 Extensions::all_available(),
527 ),
528 Ok(_)
529 );
530 }
531
532 #[test]
534 fn success_concrete_request_with_context() {
535 assert_matches!(
536 ast::Request::new(
537 (
538 ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
539 None
540 ),
541 (
542 ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(),
543 None
544 ),
545 (
546 ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
547 None
548 ),
549 ast::Context::from_pairs(
550 [("admin_approval".into(), ast::RestrictedExpr::val(true))],
551 Extensions::all_available()
552 )
553 .unwrap(),
554 Some(&schema()),
555 Extensions::all_available(),
556 ),
557 Ok(_)
558 );
559 }
560
561 #[test]
563 fn success_principal_unknown() {
564 assert_matches!(
565 ast::Request::new_with_unknowns(
566 ast::EntityUIDEntry::Unknown { loc: None },
567 ast::EntityUIDEntry::known(
568 ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(),
569 None,
570 ),
571 ast::EntityUIDEntry::known(
572 ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
573 None,
574 ),
575 Some(ast::Context::empty()),
576 Some(&schema()),
577 Extensions::all_available(),
578 ),
579 Ok(_)
580 );
581 }
582
583 #[test]
585 fn success_action_unknown() {
586 assert_matches!(
587 ast::Request::new_with_unknowns(
588 ast::EntityUIDEntry::known(
589 ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
590 None,
591 ),
592 ast::EntityUIDEntry::Unknown { loc: None },
593 ast::EntityUIDEntry::known(
594 ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
595 None,
596 ),
597 Some(ast::Context::empty()),
598 Some(&schema()),
599 Extensions::all_available(),
600 ),
601 Ok(_)
602 );
603 }
604
605 #[test]
607 fn success_resource_unknown() {
608 assert_matches!(
609 ast::Request::new_with_unknowns(
610 ast::EntityUIDEntry::known(
611 ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
612 None,
613 ),
614 ast::EntityUIDEntry::known(
615 ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(),
616 None,
617 ),
618 ast::EntityUIDEntry::Unknown { loc: None },
619 Some(ast::Context::empty()),
620 Some(&schema()),
621 Extensions::all_available(),
622 ),
623 Ok(_)
624 );
625 }
626
627 #[test]
629 fn success_context_unknown() {
630 assert_matches!(
631 ast::Request::new_with_unknowns(
632 ast::EntityUIDEntry::known(
633 ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
634 None,
635 ),
636 ast::EntityUIDEntry::known(
637 ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(),
638 None,
639 ),
640 ast::EntityUIDEntry::known(
641 ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
642 None,
643 ),
644 None,
645 Some(&schema()),
646 Extensions::all_available(),
647 ),
648 Ok(_)
649 )
650 }
651
652 #[test]
654 fn success_everything_unspecified() {
655 assert_matches!(
656 ast::Request::new_with_unknowns(
657 ast::EntityUIDEntry::Unknown { loc: None },
658 ast::EntityUIDEntry::Unknown { loc: None },
659 ast::EntityUIDEntry::Unknown { loc: None },
660 None,
661 Some(&schema()),
662 Extensions::all_available(),
663 ),
664 Ok(_)
665 );
666 }
667
668 #[test]
672 fn success_unknown_action_but_invalid_types() {
673 assert_matches!(
674 ast::Request::new_with_unknowns(
675 ast::EntityUIDEntry::known(
676 ast::EntityUID::with_eid_and_type("Album", "abc123").unwrap(),
677 None,
678 ),
679 ast::EntityUIDEntry::Unknown { loc: None },
680 ast::EntityUIDEntry::known(
681 ast::EntityUID::with_eid_and_type("User", "alice").unwrap(),
682 None,
683 ),
684 None,
685 Some(&schema()),
686 Extensions::all_available(),
687 ),
688 Ok(_)
689 );
690 }
691
692 #[test]
694 fn action_not_declared() {
695 assert_matches!(
696 ast::Request::new(
697 (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
698 (ast::EntityUID::with_eid_and_type("Action", "destroy").unwrap(), None),
699 (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
700 ast::Context::empty(),
701 Some(&schema()),
702 Extensions::all_available(),
703 ),
704 Err(e) => {
705 expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(r#"request's action `Action::"destroy"` is not declared in the schema"#).build());
706 }
707 );
708 }
709
710 #[test]
712 fn principal_type_not_declared() {
713 assert_matches!(
714 ast::Request::new(
715 (ast::EntityUID::with_eid_and_type("Foo", "abc123").unwrap(), None),
716 (ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(), None),
717 (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
718 ast::Context::empty(),
719 Some(&schema()),
720 Extensions::all_available(),
721 ),
722 Err(e) => {
723 expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error("principal type `Foo` is not declared in the schema").build());
724 }
725 );
726 }
727
728 #[test]
730 fn resource_type_not_declared() {
731 assert_matches!(
732 ast::Request::new(
733 (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
734 (ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(), None),
735 (ast::EntityUID::with_eid_and_type("Foo", "vacationphoto94.jpg").unwrap(), None),
736 ast::Context::empty(),
737 Some(&schema()),
738 Extensions::all_available(),
739 ),
740 Err(e) => {
741 expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error("resource type `Foo` is not declared in the schema").build());
742 }
743 );
744 }
745
746 #[test]
748 fn principal_type_invalid() {
749 assert_matches!(
750 ast::Request::new(
751 (ast::EntityUID::with_eid_and_type("Album", "abc123").unwrap(), None),
752 (ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(), None),
753 (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
754 ast::Context::empty(),
755 Some(&schema()),
756 Extensions::all_available(),
757 ),
758 Err(e) => {
759 expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(r#"principal type `Album` is not valid for `Action::"view_photo"`"#).build());
760 }
761 );
762 }
763
764 #[test]
766 fn resource_type_invalid() {
767 assert_matches!(
768 ast::Request::new(
769 (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
770 (ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(), None),
771 (ast::EntityUID::with_eid_and_type("Group", "coders").unwrap(), None),
772 ast::Context::empty(),
773 Some(&schema()),
774 Extensions::all_available(),
775 ),
776 Err(e) => {
777 expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(r#"resource type `Group` is not valid for `Action::"view_photo"`"#).build());
778 }
779 );
780 }
781
782 #[test]
784 fn context_missing_attribute() {
785 assert_matches!(
786 ast::Request::new(
787 (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
788 (ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(), None),
789 (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
790 ast::Context::empty(),
791 Some(&schema()),
792 Extensions::all_available(),
793 ),
794 Err(e) => {
795 expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(r#"context `<first-class record with 0 fields>` is not valid for `Action::"edit_photo"`"#).build());
796 }
797 );
798 }
799
800 #[test]
802 fn context_extra_attribute() {
803 let context_with_extra_attr = ast::Context::from_pairs(
804 [
805 ("admin_approval".into(), ast::RestrictedExpr::val(true)),
806 ("extra".into(), ast::RestrictedExpr::val(42)),
807 ],
808 Extensions::all_available(),
809 )
810 .unwrap();
811 assert_matches!(
812 ast::Request::new(
813 (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
814 (ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(), None),
815 (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
816 context_with_extra_attr.clone(),
817 Some(&schema()),
818 Extensions::all_available(),
819 ),
820 Err(e) => {
821 expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(r#"context `<first-class record with 2 fields>` is not valid for `Action::"edit_photo"`"#).build());
822 }
823 );
824 }
825
826 #[test]
828 fn context_attribute_wrong_type() {
829 let context_with_wrong_type_attr = ast::Context::from_pairs(
830 [(
831 "admin_approval".into(),
832 ast::RestrictedExpr::set([ast::RestrictedExpr::val(true)]),
833 )],
834 Extensions::all_available(),
835 )
836 .unwrap();
837 assert_matches!(
838 ast::Request::new(
839 (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
840 (ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(), None),
841 (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
842 context_with_wrong_type_attr.clone(),
843 Some(&schema()),
844 Extensions::all_available(),
845 ),
846 Err(e) => {
847 expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(r#"context `<first-class record with 1 fields>` is not valid for `Action::"edit_photo"`"#).build());
848 }
849 );
850 }
851
852 #[test]
854 fn context_attribute_heterogeneous_set() {
855 let context_with_heterogeneous_set = ast::Context::from_pairs(
856 [(
857 "admin_approval".into(),
858 ast::RestrictedExpr::set([
859 ast::RestrictedExpr::val(true),
860 ast::RestrictedExpr::val(-1001),
861 ]),
862 )],
863 Extensions::all_available(),
864 )
865 .unwrap();
866 assert_matches!(
867 ast::Request::new(
868 (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
869 (ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(), None),
870 (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
871 context_with_heterogeneous_set.clone(),
872 Some(&schema()),
873 Extensions::all_available(),
874 ),
875 Err(e) => {
876 expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(r#"context `<first-class record with 1 fields>` is not valid for `Action::"edit_photo"`"#).build());
877 }
878 );
879 }
880}