cedar_policy_validator/
coreschema.rs

1/*
2 * Copyright Cedar Contributors
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16use 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/// Struct which carries enough information that it can (efficiently) impl Core's `Schema`
28#[derive(Debug)]
29pub struct CoreSchema<'a> {
30    /// Contains all the information
31    schema: &'a ValidatorSchema,
32}
33
34impl<'a> CoreSchema<'a> {
35    /// Create a new `CoreSchema` for the given `ValidatorSchema`
36    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/// Struct which carries enough information that it can impl Core's `EntityTypeDescription`
76#[derive(Debug)]
77pub struct EntityTypeDescription {
78    /// Core `EntityType` this is describing
79    core_type: ast::EntityType,
80    /// Contains most of the schema information for this entity type
81    validator_type: ValidatorEntityType,
82    /// Allowed parent types for this entity type. (As of this writing, this
83    /// information is not contained in the `validator_type` by itself.)
84    allowed_parent_types: Arc<HashSet<ast::EntityType>>,
85}
86
87impl EntityTypeDescription {
88    /// Create a description of the given type in the given schema.
89    /// Returns `None` if the given type is not in the given schema.
90    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        // This converts a type from a schema into the representation of schema
115        // types used by core. `attr_type` is taken from a `ValidatorEntityType`
116        // which was constructed from a schema.
117        // PANIC SAFETY: see above
118        #[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        // This converts a type from a schema into the representation of schema
130        // types used by core. `tag_type` is taken from a `ValidatorEntityType`
131        // which was constructed from a schema.
132        // PANIC SAFETY: see above
133        #[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        // first check that principal and resource are of types that exist in
170        // the schema, we can do this check even if action is unknown.
171        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        // the remaining checks require knowing about the action.
192        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                // We could hypothetically ensure that the concrete parts of the
236                // request are valid for _some_ action, but this is probably more
237                // expensive than we want for this validation step.
238                // Instead, we just let the above checks (that principal and
239                // resource are of types that at least _exist_ in the schema)
240                // suffice.
241            }
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/// Error when the request does not conform to the schema.
259//
260// This is NOT a publicly exported error type.
261#[derive(Debug, Diagnostic, Error)]
262pub enum RequestValidationError {
263    /// Request action is not declared in the schema
264    #[error(transparent)]
265    #[diagnostic(transparent)]
266    UndeclaredAction(#[from] request_validation_errors::UndeclaredActionError),
267    /// Request principal is of a type not declared in the schema
268    #[error(transparent)]
269    #[diagnostic(transparent)]
270    UndeclaredPrincipalType(#[from] request_validation_errors::UndeclaredPrincipalTypeError),
271    /// Request resource is of a type not declared in the schema
272    #[error(transparent)]
273    #[diagnostic(transparent)]
274    UndeclaredResourceType(#[from] request_validation_errors::UndeclaredResourceTypeError),
275    /// Request principal is of a type that is declared in the schema, but is
276    /// not valid for the request action
277    #[error(transparent)]
278    #[diagnostic(transparent)]
279    InvalidPrincipalType(#[from] request_validation_errors::InvalidPrincipalTypeError),
280    /// Request resource is of a type that is declared in the schema, but is
281    /// not valid for the request action
282    #[error(transparent)]
283    #[diagnostic(transparent)]
284    InvalidResourceType(#[from] request_validation_errors::InvalidResourceTypeError),
285    /// Context does not comply with the shape specified for the request action
286    #[error(transparent)]
287    #[diagnostic(transparent)]
288    InvalidContext(#[from] request_validation_errors::InvalidContextError),
289    /// Error computing the type of the `Context`; see the contained error type
290    /// for details about the kinds of errors that can occur
291    #[error("context is not valid: {0}")]
292    #[diagnostic(transparent)]
293    TypeOfContext(ExtensionFunctionLookupError),
294}
295
296/// Errors related to validation
297pub 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    /// Request action is not declared in the schema
304    #[derive(Debug, Error, Diagnostic)]
305    #[error("request's action `{action}` is not declared in the schema")]
306    pub struct UndeclaredActionError {
307        /// Action which was not declared in the schema
308        pub(super) action: Arc<ast::EntityUID>,
309    }
310
311    impl UndeclaredActionError {
312        /// The action which was not declared in the schema
313        pub fn action(&self) -> &ast::EntityUID {
314            &self.action
315        }
316    }
317
318    /// Request principal is of a type not declared in the schema
319    #[derive(Debug, Error, Diagnostic)]
320    #[error("principal type `{principal_ty}` is not declared in the schema")]
321    pub struct UndeclaredPrincipalTypeError {
322        /// Principal type which was not declared in the schema
323        pub(super) principal_ty: ast::EntityType,
324    }
325
326    impl UndeclaredPrincipalTypeError {
327        /// The principal type which was not declared in the schema
328        pub fn principal_ty(&self) -> &ast::EntityType {
329            &self.principal_ty
330        }
331    }
332
333    /// Request resource is of a type not declared in the schema
334    #[derive(Debug, Error, Diagnostic)]
335    #[error("resource type `{resource_ty}` is not declared in the schema")]
336    pub struct UndeclaredResourceTypeError {
337        /// Resource type which was not declared in the schema
338        pub(super) resource_ty: ast::EntityType,
339    }
340
341    impl UndeclaredResourceTypeError {
342        /// The resource type which was not declared in the schema
343        pub fn resource_ty(&self) -> &ast::EntityType {
344            &self.resource_ty
345        }
346    }
347
348    /// Request principal is of a type that is declared in the schema, but is
349    /// not valid for the request action
350    #[derive(Debug, Error, Diagnostic)]
351    #[error("principal type `{principal_ty}` is not valid for `{action}`")]
352    pub struct InvalidPrincipalTypeError {
353        /// Principal type which is not valid
354        pub(super) principal_ty: ast::EntityType,
355        /// Action which it is not valid for
356        pub(super) action: Arc<ast::EntityUID>,
357    }
358
359    impl InvalidPrincipalTypeError {
360        /// The principal type which is not valid
361        pub fn principal_ty(&self) -> &ast::EntityType {
362            &self.principal_ty
363        }
364
365        /// The action which it is not valid for
366        pub fn action(&self) -> &ast::EntityUID {
367            &self.action
368        }
369    }
370
371    /// Request resource is of a type that is declared in the schema, but is
372    /// not valid for the request action
373    #[derive(Debug, Error, Diagnostic)]
374    #[error("resource type `{resource_ty}` is not valid for `{action}`")]
375    pub struct InvalidResourceTypeError {
376        /// Resource type which is not valid
377        pub(super) resource_ty: ast::EntityType,
378        /// Action which it is not valid for
379        pub(super) action: Arc<ast::EntityUID>,
380    }
381
382    impl InvalidResourceTypeError {
383        /// The resource type which is not valid
384        pub fn resource_ty(&self) -> &ast::EntityType {
385            &self.resource_ty
386        }
387
388        /// The action which it is not valid for
389        pub fn action(&self) -> &ast::EntityUID {
390            &self.action
391        }
392    }
393
394    /// Context does not comply with the shape specified for the request action
395    #[derive(Debug, Error, Diagnostic)]
396    #[error("context `{context}` is not valid for `{action}`")]
397    pub struct InvalidContextError {
398        /// Context which is not valid
399        pub(super) context: ast::Context,
400        /// Action which it is not valid for
401        pub(super) action: Arc<ast::EntityUID>,
402    }
403
404    impl InvalidContextError {
405        /// The context which is not valid
406        pub fn context(&self) -> &ast::Context {
407            &self.context
408        }
409
410        /// The action which it is not valid for
411        pub fn action(&self) -> &ast::EntityUID {
412            &self.action
413        }
414    }
415}
416
417/// Struct which carries enough information that it can impl Core's
418/// `ContextSchema`.
419#[derive(Debug, Clone, PartialEq)]
420pub struct ContextSchema(
421    // INVARIANT: The `Type` stored in this struct must be representable as a
422    // `SchemaType` to avoid panicking in `context_type`.
423    crate::types::Type,
424);
425
426/// A `Type` contains all the information we need for a Core `ContextSchema`.
427impl entities::ContextSchema for ContextSchema {
428    fn context_type(&self) -> entities::SchemaType {
429        // PANIC SAFETY: By `ContextSchema` invariant, `self.0` is representable as a schema type.
430        #[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
438/// Since different Actions have different schemas for `Context`, you must
439/// specify the `Action` in order to get a `ContextSchema`.
440///
441/// Returns `None` if the action is not in the schema.
442pub fn context_schema_for_action(
443    schema: &ValidatorSchema,
444    action: &ast::EntityUID,
445) -> Option<ContextSchema> {
446    // The invariant on `ContextSchema` requires that the inner type is
447    // representable as a schema type. `ValidatorSchema::context_type`
448    // always returns a closed record type, which are representable as long
449    // as their values are representable. The values are representable
450    // because they are taken from the context of a `ValidatorActionId`
451    // which was constructed directly from a schema.
452    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    /// basic success with concrete request and no context
508    #[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    /// basic success with concrete request and a context
533    #[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    /// success leaving principal unknown
562    #[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    /// success leaving action unknown
584    #[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    /// success leaving resource unknown
606    #[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    /// success leaving context unknown
628    #[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    /// success leaving everything unknown
653    #[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    /// this succeeds for now: unknown action, concrete principal and
669    /// resource of valid types, but none of the schema's actions would work
670    /// with this principal and resource type
671    #[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    /// request action not declared in the schema
693    #[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    /// request principal type not declared in the schema (action concrete)
711    #[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    /// request resource type not declared in the schema (action concrete)
729    #[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    /// request principal type declared, but invalid for request's action
747    #[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    /// request resource type declared, but invalid for request's action
765    #[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    /// request context does not comply with specification: missing attribute
783    #[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    /// request context does not comply with specification: extra attribute
801    #[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    /// request context does not comply with specification: attribute is wrong type
827    #[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    /// request context contains heterogeneous set
853    #[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}