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::{ValidatorActionId, ValidatorEntityType, ValidatorSchema};
17use cedar_policy_core::ast::{EntityType, EntityUID};
18use cedar_policy_core::extensions::{ExtensionFunctionLookupError, Extensions};
19use cedar_policy_core::{ast, entities};
20use miette::Diagnostic;
21use smol_str::SmolStr;
22use std::collections::hash_map::Values;
23use std::collections::HashSet;
24use std::iter::Cloned;
25use std::sync::Arc;
26use thiserror::Error;
27
28/// Struct which carries enough information that it can (efficiently) impl Core's `Schema`
29#[derive(Debug)]
30pub struct CoreSchema<'a> {
31    /// Contains all the information
32    schema: &'a ValidatorSchema,
33}
34
35impl<'a> CoreSchema<'a> {
36    /// Create a new `CoreSchema` for the given `ValidatorSchema`
37    pub fn new(schema: &'a ValidatorSchema) -> Self {
38        Self { schema }
39    }
40}
41
42impl<'a> entities::Schema for CoreSchema<'a> {
43    type EntityTypeDescription = EntityTypeDescription;
44    type ActionEntityIterator = Cloned<Values<'a, ast::EntityUID, Arc<ast::Entity>>>;
45
46    fn entity_type(&self, entity_type: &ast::EntityType) -> Option<EntityTypeDescription> {
47        EntityTypeDescription::new(self.schema, entity_type)
48    }
49
50    fn action(&self, action: &ast::EntityUID) -> Option<Arc<ast::Entity>> {
51        self.schema.actions.get(action).cloned()
52    }
53
54    fn entity_types_with_basename<'b>(
55        &'b self,
56        basename: &'b ast::UnreservedId,
57    ) -> Box<dyn Iterator<Item = ast::EntityType> + 'b> {
58        Box::new(self.schema.entity_types().filter_map(move |entity_type| {
59            if &entity_type.name().as_ref().basename() == basename {
60                Some(entity_type.name().clone())
61            } else {
62                None
63            }
64        }))
65    }
66
67    fn action_entities(&self) -> Self::ActionEntityIterator {
68        self.schema.actions.values().cloned()
69    }
70}
71
72/// Struct which carries enough information that it can impl Core's `EntityTypeDescription`
73#[derive(Debug)]
74pub struct EntityTypeDescription {
75    /// Core `EntityType` this is describing
76    core_type: ast::EntityType,
77    /// Contains most of the schema information for this entity type
78    validator_type: ValidatorEntityType,
79    /// Allowed parent types for this entity type. (As of this writing, this
80    /// information is not contained in the `validator_type` by itself.)
81    allowed_parent_types: Arc<HashSet<ast::EntityType>>,
82}
83
84impl EntityTypeDescription {
85    /// Create a description of the given type in the given schema.
86    /// Returns `None` if the given type is not in the given schema.
87    pub fn new(schema: &ValidatorSchema, type_name: &ast::EntityType) -> Option<Self> {
88        Some(Self {
89            core_type: type_name.clone(),
90            validator_type: schema.get_entity_type(type_name).cloned()?,
91            allowed_parent_types: {
92                let mut set = HashSet::new();
93                for possible_parent_et in schema.entity_types() {
94                    if possible_parent_et.descendants.contains(type_name) {
95                        set.insert(possible_parent_et.name().clone());
96                    }
97                }
98                Arc::new(set)
99            },
100        })
101    }
102}
103
104impl entities::EntityTypeDescription for EntityTypeDescription {
105    fn entity_type(&self) -> ast::EntityType {
106        self.core_type.clone()
107    }
108
109    fn attr_type(&self, attr: &str) -> Option<entities::SchemaType> {
110        let attr_type: &crate::types::Type = &self.validator_type.attr(attr)?.attr_type;
111        // This converts a type from a schema into the representation of schema
112        // types used by core. `attr_type` is taken from a `ValidatorEntityType`
113        // which was constructed from a schema.
114        // PANIC SAFETY: see above
115        #[allow(clippy::expect_used)]
116        let core_schema_type: entities::SchemaType = attr_type
117            .clone()
118            .try_into()
119            .expect("failed to convert validator type into Core SchemaType");
120        debug_assert!(attr_type.is_consistent_with(&core_schema_type));
121        Some(core_schema_type)
122    }
123
124    fn tag_type(&self) -> Option<entities::SchemaType> {
125        let tag_type: &crate::types::Type = self.validator_type.tag_type()?;
126        // This converts a type from a schema into the representation of schema
127        // types used by core. `tag_type` is taken from a `ValidatorEntityType`
128        // which was constructed from a schema.
129        // PANIC SAFETY: see above
130        #[allow(clippy::expect_used)]
131        let core_schema_type: entities::SchemaType = tag_type
132            .clone()
133            .try_into()
134            .expect("failed to convert validator type into Core SchemaType");
135        debug_assert!(tag_type.is_consistent_with(&core_schema_type));
136        Some(core_schema_type)
137    }
138
139    fn required_attrs<'s>(&'s self) -> Box<dyn Iterator<Item = SmolStr> + 's> {
140        Box::new(
141            self.validator_type
142                .attributes()
143                .iter()
144                .filter(|(_, ty)| ty.is_required)
145                .map(|(attr, _)| attr.clone()),
146        )
147    }
148
149    fn allowed_parent_types(&self) -> Arc<HashSet<ast::EntityType>> {
150        Arc::clone(&self.allowed_parent_types)
151    }
152
153    fn open_attributes(&self) -> bool {
154        self.validator_type.open_attributes.is_open()
155    }
156}
157
158impl ast::RequestSchema for ValidatorSchema {
159    type Error = RequestValidationError;
160    fn validate_request(
161        &self,
162        request: &ast::Request,
163        extensions: &Extensions<'_>,
164    ) -> std::result::Result<(), Self::Error> {
165        use ast::EntityUIDEntry;
166        // first check that principal and resource are of types that exist in
167        // the schema, we can do this check even if action is unknown.
168        if let Some(principal_type) = request.principal().get_type() {
169            if self.get_entity_type(principal_type).is_none() {
170                return Err(request_validation_errors::UndeclaredPrincipalTypeError {
171                    principal_ty: principal_type.clone(),
172                }
173                .into());
174            }
175        }
176        if let Some(resource_type) = request.resource().get_type() {
177            if self.get_entity_type(resource_type).is_none() {
178                return Err(request_validation_errors::UndeclaredResourceTypeError {
179                    resource_ty: resource_type.clone(),
180                }
181                .into());
182            }
183        }
184
185        // the remaining checks require knowing about the action.
186        match request.action() {
187            EntityUIDEntry::Known { euid: action, .. } => {
188                let validator_action_id = self.get_action_id(action).ok_or_else(|| {
189                    request_validation_errors::UndeclaredActionError {
190                        action: Arc::clone(action),
191                    }
192                })?;
193                if let Some(principal_type) = request.principal().get_type() {
194                    validator_action_id.check_principal_type(principal_type, action)?;
195                }
196                if let Some(principal_type) = request.resource().get_type() {
197                    validator_action_id.check_resource_type(principal_type, action)?;
198                }
199                if let Some(context) = request.context() {
200                    let expected_context_ty = validator_action_id.context_type();
201                    if !expected_context_ty
202                        .typecheck_partial_value(&context.clone().into(), extensions)
203                        .map_err(RequestValidationError::TypeOfContext)?
204                    {
205                        return Err(request_validation_errors::InvalidContextError {
206                            context: context.clone(),
207                            action: Arc::clone(action),
208                        }
209                        .into());
210                    }
211                }
212            }
213            EntityUIDEntry::Unknown { .. } => {
214                // We could hypothetically ensure that the concrete parts of the
215                // request are valid for _some_ action, but this is probably more
216                // expensive than we want for this validation step.
217                // Instead, we just let the above checks (that principal and
218                // resource are of types that at least _exist_ in the schema)
219                // suffice.
220            }
221        }
222        Ok(())
223    }
224}
225
226impl ValidatorActionId {
227    fn check_principal_type(
228        &self,
229        principal_type: &EntityType,
230        action: &Arc<EntityUID>,
231    ) -> Result<(), request_validation_errors::InvalidPrincipalTypeError> {
232        if !self.is_applicable_principal_type(principal_type) {
233            Err(request_validation_errors::InvalidPrincipalTypeError {
234                principal_ty: principal_type.clone(),
235                action: Arc::clone(action),
236                valid_principal_tys: self.applies_to_principals().cloned().collect(),
237            })
238        } else {
239            Ok(())
240        }
241    }
242
243    fn check_resource_type(
244        &self,
245        resource_type: &EntityType,
246        action: &Arc<EntityUID>,
247    ) -> Result<(), request_validation_errors::InvalidResourceTypeError> {
248        if !self.is_applicable_resource_type(resource_type) {
249            Err(request_validation_errors::InvalidResourceTypeError {
250                resource_ty: resource_type.clone(),
251                action: Arc::clone(action),
252                valid_resource_tys: self.applies_to_resources().cloned().collect(),
253            })
254        } else {
255            Ok(())
256        }
257    }
258}
259
260impl ast::RequestSchema for CoreSchema<'_> {
261    type Error = RequestValidationError;
262    fn validate_request(
263        &self,
264        request: &ast::Request,
265        extensions: &Extensions<'_>,
266    ) -> Result<(), Self::Error> {
267        self.schema.validate_request(request, extensions)
268    }
269}
270
271/// Error when the request does not conform to the schema.
272//
273// This is NOT a publicly exported error type.
274#[derive(Debug, Diagnostic, Error)]
275pub enum RequestValidationError {
276    /// Request action is not declared in the schema
277    #[error(transparent)]
278    #[diagnostic(transparent)]
279    UndeclaredAction(#[from] request_validation_errors::UndeclaredActionError),
280    /// Request principal is of a type not declared in the schema
281    #[error(transparent)]
282    #[diagnostic(transparent)]
283    UndeclaredPrincipalType(#[from] request_validation_errors::UndeclaredPrincipalTypeError),
284    /// Request resource is of a type not declared in the schema
285    #[error(transparent)]
286    #[diagnostic(transparent)]
287    UndeclaredResourceType(#[from] request_validation_errors::UndeclaredResourceTypeError),
288    /// Request principal is of a type that is declared in the schema, but is
289    /// not valid for the request action
290    #[error(transparent)]
291    #[diagnostic(transparent)]
292    InvalidPrincipalType(#[from] request_validation_errors::InvalidPrincipalTypeError),
293    /// Request resource is of a type that is declared in the schema, but is
294    /// not valid for the request action
295    #[error(transparent)]
296    #[diagnostic(transparent)]
297    InvalidResourceType(#[from] request_validation_errors::InvalidResourceTypeError),
298    /// Context does not comply with the shape specified for the request action
299    #[error(transparent)]
300    #[diagnostic(transparent)]
301    InvalidContext(#[from] request_validation_errors::InvalidContextError),
302    /// Error computing the type of the `Context`; see the contained error type
303    /// for details about the kinds of errors that can occur
304    #[error("context is not valid: {0}")]
305    #[diagnostic(transparent)]
306    TypeOfContext(ExtensionFunctionLookupError),
307}
308
309/// Errors related to validation
310pub mod request_validation_errors {
311    use cedar_policy_core::ast;
312    use cedar_policy_core::impl_diagnostic_from_method_on_field;
313    use itertools::Itertools;
314    use miette::Diagnostic;
315    use std::sync::Arc;
316    use thiserror::Error;
317
318    /// Request action is not declared in the schema
319    #[derive(Debug, Error)]
320    #[error("request's action `{action}` is not declared in the schema")]
321    pub struct UndeclaredActionError {
322        /// Action which was not declared in the schema
323        pub(super) action: Arc<ast::EntityUID>,
324    }
325
326    impl Diagnostic for UndeclaredActionError {
327        impl_diagnostic_from_method_on_field!(action, loc);
328    }
329
330    impl UndeclaredActionError {
331        /// The action which was not declared in the schema
332        pub fn action(&self) -> &ast::EntityUID {
333            &self.action
334        }
335    }
336
337    /// Request principal is of a type not declared in the schema
338    #[derive(Debug, Error)]
339    #[error("principal type `{principal_ty}` is not declared in the schema")]
340    pub struct UndeclaredPrincipalTypeError {
341        /// Principal type which was not declared in the schema
342        pub(super) principal_ty: ast::EntityType,
343    }
344
345    impl Diagnostic for UndeclaredPrincipalTypeError {
346        impl_diagnostic_from_method_on_field!(principal_ty, loc);
347    }
348
349    impl UndeclaredPrincipalTypeError {
350        /// The principal type which was not declared in the schema
351        pub fn principal_ty(&self) -> &ast::EntityType {
352            &self.principal_ty
353        }
354    }
355
356    /// Request resource is of a type not declared in the schema
357    #[derive(Debug, Error)]
358    #[error("resource type `{resource_ty}` is not declared in the schema")]
359    pub struct UndeclaredResourceTypeError {
360        /// Resource type which was not declared in the schema
361        pub(super) resource_ty: ast::EntityType,
362    }
363
364    impl Diagnostic for UndeclaredResourceTypeError {
365        impl_diagnostic_from_method_on_field!(resource_ty, loc);
366    }
367
368    impl UndeclaredResourceTypeError {
369        /// The resource type which was not declared in the schema
370        pub fn resource_ty(&self) -> &ast::EntityType {
371            &self.resource_ty
372        }
373    }
374
375    /// Request principal is of a type that is declared in the schema, but is
376    /// not valid for the request action
377    #[derive(Debug, Error)]
378    #[error("principal type `{principal_ty}` is not valid for `{action}`")]
379    pub struct InvalidPrincipalTypeError {
380        /// Principal type which is not valid
381        pub(super) principal_ty: ast::EntityType,
382        /// Action which it is not valid for
383        pub(super) action: Arc<ast::EntityUID>,
384        /// Principal types which actually are valid for that `action`
385        pub(super) valid_principal_tys: Vec<ast::EntityType>,
386    }
387
388    impl Diagnostic for InvalidPrincipalTypeError {
389        fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
390            Some(Box::new(invalid_principal_type_help(
391                &self.valid_principal_tys,
392                &self.action,
393            )))
394        }
395
396        // possible future improvement: provide two labels, one for the source
397        // loc on `principal_ty` and the other for the source loc on `action`
398        impl_diagnostic_from_method_on_field!(principal_ty, loc);
399    }
400
401    fn invalid_principal_type_help(
402        valid_principal_tys: &[ast::EntityType],
403        action: &ast::EntityUID,
404    ) -> String {
405        if valid_principal_tys.is_empty() {
406            format!("no principal types are valid for `{action}`")
407        } else {
408            format!(
409                "valid principal types for `{action}`: {}",
410                valid_principal_tys
411                    .iter()
412                    .sorted_unstable()
413                    .map(|et| format!("`{et}`"))
414                    .join(", ")
415            )
416        }
417    }
418
419    impl InvalidPrincipalTypeError {
420        /// The principal type which is not valid
421        pub fn principal_ty(&self) -> &ast::EntityType {
422            &self.principal_ty
423        }
424
425        /// The action which it is not valid for
426        pub fn action(&self) -> &ast::EntityUID {
427            &self.action
428        }
429
430        /// Principal types which actually are valid for that action
431        pub fn valid_principal_tys(&self) -> impl Iterator<Item = &ast::EntityType> {
432            self.valid_principal_tys.iter()
433        }
434    }
435
436    /// Request resource is of a type that is declared in the schema, but is
437    /// not valid for the request action
438    #[derive(Debug, Error)]
439    #[error("resource type `{resource_ty}` is not valid for `{action}`")]
440    pub struct InvalidResourceTypeError {
441        /// Resource type which is not valid
442        pub(super) resource_ty: ast::EntityType,
443        /// Action which it is not valid for
444        pub(super) action: Arc<ast::EntityUID>,
445        /// Resource types which actually are valid for that `action`
446        pub(super) valid_resource_tys: Vec<ast::EntityType>,
447    }
448
449    impl Diagnostic for InvalidResourceTypeError {
450        fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
451            Some(Box::new(invalid_resource_type_help(
452                &self.valid_resource_tys,
453                &self.action,
454            )))
455        }
456
457        // possible future improvement: provide two labels, one for the source
458        // loc on `resource_ty` and the other for the source loc on `action`
459        impl_diagnostic_from_method_on_field!(resource_ty, loc);
460    }
461
462    fn invalid_resource_type_help(
463        valid_resource_tys: &[ast::EntityType],
464        action: &ast::EntityUID,
465    ) -> String {
466        if valid_resource_tys.is_empty() {
467            format!("no resource types are valid for `{action}`")
468        } else {
469            format!(
470                "valid resource types for `{action}`: {}",
471                valid_resource_tys
472                    .iter()
473                    .sorted_unstable()
474                    .map(|et| format!("`{et}`"))
475                    .join(", ")
476            )
477        }
478    }
479
480    impl InvalidResourceTypeError {
481        /// The resource type which is not valid
482        pub fn resource_ty(&self) -> &ast::EntityType {
483            &self.resource_ty
484        }
485
486        /// The action which it is not valid for
487        pub fn action(&self) -> &ast::EntityUID {
488            &self.action
489        }
490
491        /// Resource types which actually are valid for that action
492        pub fn valid_resource_tys(&self) -> impl Iterator<Item = &ast::EntityType> {
493            self.valid_resource_tys.iter()
494        }
495    }
496
497    /// Context does not comply with the shape specified for the request action
498    #[derive(Debug, Error, Diagnostic)]
499    #[error("context `{}` is not valid for `{action}`", ast::BoundedToString::to_string_bounded(.context, BOUNDEDDISPLAY_BOUND_FOR_INVALID_CONTEXT_ERROR))]
500    pub struct InvalidContextError {
501        /// Context which is not valid
502        pub(super) context: ast::Context,
503        /// Action which it is not valid for
504        pub(super) action: Arc<ast::EntityUID>,
505    }
506
507    const BOUNDEDDISPLAY_BOUND_FOR_INVALID_CONTEXT_ERROR: usize = 5;
508
509    impl InvalidContextError {
510        /// The context which is not valid
511        pub fn context(&self) -> &ast::Context {
512            &self.context
513        }
514
515        /// The action which it is not valid for
516        pub fn action(&self) -> &ast::EntityUID {
517            &self.action
518        }
519    }
520}
521
522/// Struct which carries enough information that it can impl Core's
523/// `ContextSchema`.
524#[derive(Debug, Clone, PartialEq, Eq)]
525pub struct ContextSchema(
526    // INVARIANT: The `Type` stored in this struct must be representable as a
527    // `SchemaType` to avoid panicking in `context_type`.
528    crate::types::Type,
529);
530
531/// A `Type` contains all the information we need for a Core `ContextSchema`.
532impl entities::ContextSchema for ContextSchema {
533    fn context_type(&self) -> entities::SchemaType {
534        // PANIC SAFETY: By `ContextSchema` invariant, `self.0` is representable as a schema type.
535        #[allow(clippy::expect_used)]
536        self.0
537            .clone()
538            .try_into()
539            .expect("failed to convert validator type into Core SchemaType")
540    }
541}
542
543/// Since different Actions have different schemas for `Context`, you must
544/// specify the `Action` in order to get a `ContextSchema`.
545///
546/// Returns `None` if the action is not in the schema.
547pub fn context_schema_for_action(
548    schema: &ValidatorSchema,
549    action: &ast::EntityUID,
550) -> Option<ContextSchema> {
551    // The invariant on `ContextSchema` requires that the inner type is
552    // representable as a schema type. `ValidatorSchema::context_type`
553    // always returns a closed record type, which are representable as long
554    // as their values are representable. The values are representable
555    // because they are taken from the context of a `ValidatorActionId`
556    // which was constructed directly from a schema.
557    schema.context_type(action).cloned().map(ContextSchema)
558}
559
560#[cfg(test)]
561mod test {
562    use super::*;
563    use cedar_policy_core::test_utils::{expect_err, ExpectedErrorMessageBuilder};
564    use cool_asserts::assert_matches;
565    use serde_json::json;
566
567    fn schema() -> ValidatorSchema {
568        let src = json!(
569        { "": {
570            "entityTypes": {
571                "User": {
572                    "memberOfTypes": [ "Group" ]
573                },
574                "Group": {
575                    "memberOfTypes": []
576                },
577                "Photo": {
578                    "memberOfTypes": [ "Album" ]
579                },
580                "Album": {
581                    "memberOfTypes": []
582                }
583            },
584            "actions": {
585                "view_photo": {
586                    "appliesTo": {
587                        "principalTypes": ["User", "Group"],
588                        "resourceTypes": ["Photo"]
589                    }
590                },
591                "edit_photo": {
592                    "appliesTo": {
593                        "principalTypes": ["User", "Group"],
594                        "resourceTypes": ["Photo"],
595                        "context": {
596                            "type": "Record",
597                            "attributes": {
598                                "admin_approval": {
599                                    "type": "Boolean",
600                                    "required": true,
601                                }
602                            }
603                        }
604                    }
605                }
606            }
607        }});
608        ValidatorSchema::from_json_value(src, Extensions::all_available())
609            .expect("failed to create ValidatorSchema")
610    }
611
612    /// basic success with concrete request and no context
613    #[test]
614    fn success_concrete_request_no_context() {
615        assert_matches!(
616            ast::Request::new(
617                (
618                    ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
619                    None
620                ),
621                (
622                    ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(),
623                    None
624                ),
625                (
626                    ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
627                    None
628                ),
629                ast::Context::empty(),
630                Some(&schema()),
631                Extensions::all_available(),
632            ),
633            Ok(_)
634        );
635    }
636
637    /// basic success with concrete request and a context
638    #[test]
639    fn success_concrete_request_with_context() {
640        assert_matches!(
641            ast::Request::new(
642                (
643                    ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
644                    None
645                ),
646                (
647                    ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(),
648                    None
649                ),
650                (
651                    ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
652                    None
653                ),
654                ast::Context::from_pairs(
655                    [("admin_approval".into(), ast::RestrictedExpr::val(true))],
656                    Extensions::all_available()
657                )
658                .unwrap(),
659                Some(&schema()),
660                Extensions::all_available(),
661            ),
662            Ok(_)
663        );
664    }
665
666    /// success leaving principal unknown
667    #[test]
668    fn success_principal_unknown() {
669        assert_matches!(
670            ast::Request::new_with_unknowns(
671                ast::EntityUIDEntry::unknown(),
672                ast::EntityUIDEntry::known(
673                    ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(),
674                    None,
675                ),
676                ast::EntityUIDEntry::known(
677                    ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
678                    None,
679                ),
680                Some(ast::Context::empty()),
681                Some(&schema()),
682                Extensions::all_available(),
683            ),
684            Ok(_)
685        );
686    }
687
688    /// success leaving action unknown
689    #[test]
690    fn success_action_unknown() {
691        assert_matches!(
692            ast::Request::new_with_unknowns(
693                ast::EntityUIDEntry::known(
694                    ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
695                    None,
696                ),
697                ast::EntityUIDEntry::unknown(),
698                ast::EntityUIDEntry::known(
699                    ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
700                    None,
701                ),
702                Some(ast::Context::empty()),
703                Some(&schema()),
704                Extensions::all_available(),
705            ),
706            Ok(_)
707        );
708    }
709
710    /// success leaving resource unknown
711    #[test]
712    fn success_resource_unknown() {
713        assert_matches!(
714            ast::Request::new_with_unknowns(
715                ast::EntityUIDEntry::known(
716                    ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
717                    None,
718                ),
719                ast::EntityUIDEntry::known(
720                    ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(),
721                    None,
722                ),
723                ast::EntityUIDEntry::unknown(),
724                Some(ast::Context::empty()),
725                Some(&schema()),
726                Extensions::all_available(),
727            ),
728            Ok(_)
729        );
730    }
731
732    /// success leaving context unknown
733    #[test]
734    fn success_context_unknown() {
735        assert_matches!(
736            ast::Request::new_with_unknowns(
737                ast::EntityUIDEntry::known(
738                    ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
739                    None,
740                ),
741                ast::EntityUIDEntry::known(
742                    ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(),
743                    None,
744                ),
745                ast::EntityUIDEntry::known(
746                    ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
747                    None,
748                ),
749                None,
750                Some(&schema()),
751                Extensions::all_available(),
752            ),
753            Ok(_)
754        )
755    }
756
757    /// success leaving everything unknown
758    #[test]
759    fn success_everything_unspecified() {
760        assert_matches!(
761            ast::Request::new_with_unknowns(
762                ast::EntityUIDEntry::unknown(),
763                ast::EntityUIDEntry::unknown(),
764                ast::EntityUIDEntry::unknown(),
765                None,
766                Some(&schema()),
767                Extensions::all_available(),
768            ),
769            Ok(_)
770        );
771    }
772
773    /// this succeeds for now: unknown action, concrete principal and
774    /// resource of valid types, but none of the schema's actions would work
775    /// with this principal and resource type
776    #[test]
777    fn success_unknown_action_but_invalid_types() {
778        assert_matches!(
779            ast::Request::new_with_unknowns(
780                ast::EntityUIDEntry::known(
781                    ast::EntityUID::with_eid_and_type("Album", "abc123").unwrap(),
782                    None,
783                ),
784                ast::EntityUIDEntry::unknown(),
785                ast::EntityUIDEntry::known(
786                    ast::EntityUID::with_eid_and_type("User", "alice").unwrap(),
787                    None,
788                ),
789                None,
790                Some(&schema()),
791                Extensions::all_available(),
792            ),
793            Ok(_)
794        );
795    }
796
797    /// request action not declared in the schema
798    #[test]
799    fn action_not_declared() {
800        assert_matches!(
801            ast::Request::new(
802                (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
803                (ast::EntityUID::with_eid_and_type("Action", "destroy").unwrap(), None),
804                (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
805                ast::Context::empty(),
806                Some(&schema()),
807                Extensions::all_available(),
808            ),
809            Err(e) => {
810                expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(r#"request's action `Action::"destroy"` is not declared in the schema"#).build());
811            }
812        );
813    }
814
815    /// request principal type not declared in the schema (action concrete)
816    #[test]
817    fn principal_type_not_declared() {
818        assert_matches!(
819            ast::Request::new(
820                (ast::EntityUID::with_eid_and_type("Foo", "abc123").unwrap(), None),
821                (ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(), None),
822                (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
823                ast::Context::empty(),
824                Some(&schema()),
825                Extensions::all_available(),
826            ),
827            Err(e) => {
828                expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error("principal type `Foo` is not declared in the schema").build());
829            }
830        );
831    }
832
833    /// request resource type not declared in the schema (action concrete)
834    #[test]
835    fn resource_type_not_declared() {
836        assert_matches!(
837            ast::Request::new(
838                (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
839                (ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(), None),
840                (ast::EntityUID::with_eid_and_type("Foo", "vacationphoto94.jpg").unwrap(), None),
841                ast::Context::empty(),
842                Some(&schema()),
843                Extensions::all_available(),
844            ),
845            Err(e) => {
846                expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error("resource type `Foo` is not declared in the schema").build());
847            }
848        );
849    }
850
851    /// request principal type declared, but invalid for request's action
852    #[test]
853    fn principal_type_invalid() {
854        assert_matches!(
855            ast::Request::new(
856                (ast::EntityUID::with_eid_and_type("Album", "abc123").unwrap(), None),
857                (ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(), None),
858                (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
859                ast::Context::empty(),
860                Some(&schema()),
861                Extensions::all_available(),
862            ),
863            Err(e) => {
864                expect_err(
865                    "",
866                    &miette::Report::new(e),
867                    &ExpectedErrorMessageBuilder::error(r#"principal type `Album` is not valid for `Action::"view_photo"`"#)
868                        .help(r#"valid principal types for `Action::"view_photo"`: `Group`, `User`"#)
869                        .build(),
870                );
871            }
872        );
873    }
874
875    /// request resource type declared, but invalid for request's action
876    #[test]
877    fn resource_type_invalid() {
878        assert_matches!(
879            ast::Request::new(
880                (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
881                (ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(), None),
882                (ast::EntityUID::with_eid_and_type("Group", "coders").unwrap(), None),
883                ast::Context::empty(),
884                Some(&schema()),
885                Extensions::all_available(),
886            ),
887            Err(e) => {
888                expect_err(
889                    "",
890                    &miette::Report::new(e),
891                    &ExpectedErrorMessageBuilder::error(r#"resource type `Group` is not valid for `Action::"view_photo"`"#)
892                        .help(r#"valid resource types for `Action::"view_photo"`: `Photo`"#)
893                        .build(),
894                );
895            }
896        );
897    }
898
899    /// request context does not comply with specification: missing attribute
900    #[test]
901    fn context_missing_attribute() {
902        assert_matches!(
903            ast::Request::new(
904                (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
905                (ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(), None),
906                (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
907                ast::Context::empty(),
908                Some(&schema()),
909                Extensions::all_available(),
910            ),
911            Err(e) => {
912                expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(r#"context `{}` is not valid for `Action::"edit_photo"`"#).build());
913            }
914        );
915    }
916
917    /// request context does not comply with specification: extra attribute
918    #[test]
919    fn context_extra_attribute() {
920        let context_with_extra_attr = ast::Context::from_pairs(
921            [
922                ("admin_approval".into(), ast::RestrictedExpr::val(true)),
923                ("extra".into(), ast::RestrictedExpr::val(42)),
924            ],
925            Extensions::all_available(),
926        )
927        .unwrap();
928        assert_matches!(
929            ast::Request::new(
930                (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
931                (ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(), None),
932                (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
933                context_with_extra_attr,
934                Some(&schema()),
935                Extensions::all_available(),
936            ),
937            Err(e) => {
938                expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(r#"context `{admin_approval: true, extra: 42}` is not valid for `Action::"edit_photo"`"#).build());
939            }
940        );
941    }
942
943    /// request context does not comply with specification: attribute is wrong type
944    #[test]
945    fn context_attribute_wrong_type() {
946        let context_with_wrong_type_attr = ast::Context::from_pairs(
947            [(
948                "admin_approval".into(),
949                ast::RestrictedExpr::set([ast::RestrictedExpr::val(true)]),
950            )],
951            Extensions::all_available(),
952        )
953        .unwrap();
954        assert_matches!(
955            ast::Request::new(
956                (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
957                (ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(), None),
958                (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
959                context_with_wrong_type_attr,
960                Some(&schema()),
961                Extensions::all_available(),
962            ),
963            Err(e) => {
964                expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(r#"context `{admin_approval: [true]}` is not valid for `Action::"edit_photo"`"#).build());
965            }
966        );
967    }
968
969    /// request context contains heterogeneous set
970    #[test]
971    fn context_attribute_heterogeneous_set() {
972        let context_with_heterogeneous_set = ast::Context::from_pairs(
973            [(
974                "admin_approval".into(),
975                ast::RestrictedExpr::set([
976                    ast::RestrictedExpr::val(true),
977                    ast::RestrictedExpr::val(-1001),
978                ]),
979            )],
980            Extensions::all_available(),
981        )
982        .unwrap();
983        assert_matches!(
984            ast::Request::new(
985                (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
986                (ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(), None),
987                (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
988                context_with_heterogeneous_set,
989                Some(&schema()),
990                Extensions::all_available(),
991            ),
992            Err(e) => {
993                expect_err("", &miette::Report::new(e), &ExpectedErrorMessageBuilder::error(r#"context `{admin_approval: [true, -1001]}` is not valid for `Action::"edit_photo"`"#).build());
994            }
995        );
996    }
997
998    /// request context which is large enough that we don't print the whole thing in the error message
999    #[test]
1000    fn context_large() {
1001        let large_context_with_extra_attributes = ast::Context::from_pairs(
1002            [
1003                ("admin_approval".into(), ast::RestrictedExpr::val(true)),
1004                ("extra1".into(), ast::RestrictedExpr::val(false)),
1005                ("also extra".into(), ast::RestrictedExpr::val("spam")),
1006                (
1007                    "extra2".into(),
1008                    ast::RestrictedExpr::set([ast::RestrictedExpr::val(-100)]),
1009                ),
1010                (
1011                    "extra3".into(),
1012                    ast::RestrictedExpr::val(
1013                        ast::EntityUID::with_eid_and_type("User", "alice").unwrap(),
1014                    ),
1015                ),
1016                ("extra4".into(), ast::RestrictedExpr::val("foobar")),
1017            ],
1018            Extensions::all_available(),
1019        )
1020        .unwrap();
1021        assert_matches!(
1022            ast::Request::new(
1023                (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None),
1024                (ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(), None),
1025                (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None),
1026                large_context_with_extra_attributes,
1027                Some(&schema()),
1028                Extensions::all_available(),
1029            ),
1030            Err(e) => {
1031                expect_err(
1032                    "",
1033                    &miette::Report::new(e),
1034                    &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(),
1035                );
1036            }
1037        );
1038    }
1039}