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