cedar_policy_validator/
coreschema.rs

1use crate::{ValidatorEntityType, ValidatorSchema};
2use cedar_policy_core::entities::GetSchemaTypeError;
3use cedar_policy_core::extensions::Extensions;
4use cedar_policy_core::{ast, entities};
5use smol_str::SmolStr;
6use std::collections::{HashMap, HashSet};
7use std::sync::Arc;
8use thiserror::Error;
9
10/// Struct which carries enough information that it can (efficiently) impl Core's `Schema`
11pub struct CoreSchema<'a> {
12    /// Contains all the information
13    schema: &'a ValidatorSchema,
14    /// For easy lookup, this is a map from action name to `Entity` object
15    /// for each action in the schema. This information is contained in the
16    /// `ValidatorSchema`, but not efficient to extract -- getting the `Entity`
17    /// from the `ValidatorSchema` is O(N) as of this writing, but with this
18    /// cache it's O(1).
19    actions: HashMap<ast::EntityUID, Arc<ast::Entity>>,
20}
21
22impl<'a> CoreSchema<'a> {
23    pub fn new(schema: &'a ValidatorSchema) -> Self {
24        Self {
25            actions: schema
26                .action_entities_iter()
27                .map(|e| (e.uid(), Arc::new(e)))
28                .collect(),
29            schema,
30        }
31    }
32}
33
34impl<'a> entities::Schema for CoreSchema<'a> {
35    type EntityTypeDescription = EntityTypeDescription;
36    type ActionEntityIterator = Vec<Arc<ast::Entity>>;
37
38    fn entity_type(&self, entity_type: &ast::EntityType) -> Option<EntityTypeDescription> {
39        match entity_type {
40            ast::EntityType::Unspecified => None, // Unspecified entities cannot be declared in the schema and should not appear in JSON data
41            ast::EntityType::Specified(name) => EntityTypeDescription::new(self.schema, name),
42        }
43    }
44
45    fn action(&self, action: &ast::EntityUID) -> Option<Arc<ast::Entity>> {
46        self.actions.get(action).map(Arc::clone)
47    }
48
49    fn entity_types_with_basename<'b>(
50        &'b self,
51        basename: &'b ast::Id,
52    ) -> Box<dyn Iterator<Item = ast::EntityType> + 'b> {
53        Box::new(self.schema.entity_types().filter_map(move |(name, _)| {
54            if name.basename() == basename {
55                Some(ast::EntityType::Specified(name.clone()))
56            } else {
57                None
58            }
59        }))
60    }
61
62    fn action_entities(&self) -> Self::ActionEntityIterator {
63        self.actions.values().map(Arc::clone).collect()
64    }
65}
66
67/// Struct which carries enough information that it can impl Core's `EntityTypeDescription`
68pub struct EntityTypeDescription {
69    /// Core `EntityType` this is describing
70    core_type: ast::EntityType,
71    /// Contains most of the schema information for this entity type
72    validator_type: ValidatorEntityType,
73    /// Allowed parent types for this entity type. (As of this writing, this
74    /// information is not contained in the `validator_type` by itself.)
75    allowed_parent_types: Arc<HashSet<ast::EntityType>>,
76}
77
78impl EntityTypeDescription {
79    /// Create a description of the given type in the given schema.
80    /// Returns `None` if the given type is not in the given schema.
81    pub fn new(schema: &ValidatorSchema, type_name: &ast::Name) -> Option<Self> {
82        Some(Self {
83            core_type: ast::EntityType::Specified(type_name.clone()),
84            validator_type: schema.get_entity_type(type_name).cloned()?,
85            allowed_parent_types: {
86                let mut set = HashSet::new();
87                for (possible_parent_typename, possible_parent_et) in schema.entity_types() {
88                    if possible_parent_et.descendants.contains(type_name) {
89                        set.insert(ast::EntityType::Specified(possible_parent_typename.clone()));
90                    }
91                }
92                Arc::new(set)
93            },
94        })
95    }
96}
97
98impl entities::EntityTypeDescription for EntityTypeDescription {
99    fn entity_type(&self) -> ast::EntityType {
100        self.core_type.clone()
101    }
102
103    fn attr_type(&self, attr: &str) -> Option<entities::SchemaType> {
104        let attr_type: &crate::types::Type = &self.validator_type.attr(attr)?.attr_type;
105        // This converts a type from a schema into the representation of schema
106        // types used by core. `attr_type` is taken from a `ValidatorEntityType`
107        // which was constructed from a schema.
108        // PANIC SAFETY: see above
109        #[allow(clippy::expect_used)]
110        let core_schema_type: entities::SchemaType = attr_type
111            .clone()
112            .try_into()
113            .expect("failed to convert validator type into Core SchemaType");
114        debug_assert!(attr_type.is_consistent_with(&core_schema_type));
115        Some(core_schema_type)
116    }
117
118    fn required_attrs<'s>(&'s self) -> Box<dyn Iterator<Item = SmolStr> + 's> {
119        Box::new(
120            self.validator_type
121                .attributes
122                .iter()
123                .filter(|(_, ty)| ty.is_required)
124                .map(|(attr, _)| attr.clone()),
125        )
126    }
127
128    fn allowed_parent_types(&self) -> Arc<HashSet<ast::EntityType>> {
129        Arc::clone(&self.allowed_parent_types)
130    }
131
132    fn open_attributes(&self) -> bool {
133        self.validator_type.open_attributes.is_open()
134    }
135}
136
137impl ast::RequestSchema for ValidatorSchema {
138    type Error = RequestValidationError;
139    fn validate_request(
140        &self,
141        request: &ast::Request,
142        extensions: Extensions<'_>,
143    ) -> std::result::Result<(), Self::Error> {
144        use ast::EntityUIDEntry;
145        // first check that principal and resource are of types that exist in
146        // the schema, or unspecified.
147        // we can do this check even if action is unknown.
148        if let EntityUIDEntry::Known(principal) = request.principal() {
149            match principal.entity_type() {
150                ast::EntityType::Specified(name) => {
151                    if self.get_entity_type(name).is_none() {
152                        return Err(RequestValidationError::UndeclaredPrincipalType {
153                            principal_ty: principal.entity_type().clone(),
154                        });
155                    }
156                }
157                ast::EntityType::Unspecified => {} // unspecified principal is allowed, unless we find it is not allowed for this action, which we will check below
158            }
159        }
160        if let EntityUIDEntry::Known(resource) = request.resource() {
161            match resource.entity_type() {
162                ast::EntityType::Specified(name) => {
163                    if self.get_entity_type(name).is_none() {
164                        return Err(RequestValidationError::UndeclaredResourceType {
165                            resource_ty: resource.entity_type().clone(),
166                        });
167                    }
168                }
169                ast::EntityType::Unspecified => {} // unspecified resource is allowed, unless we find it is not allowed for this action, which we will check below
170            }
171        }
172
173        // the remaining checks require knowing about the action.
174        match request.action() {
175            EntityUIDEntry::Known(action) => {
176                let validator_action_id = self.get_action_id(action).ok_or_else(|| {
177                    RequestValidationError::UndeclaredAction {
178                        action: Arc::clone(action),
179                    }
180                })?;
181                if let EntityUIDEntry::Known(principal) = request.principal() {
182                    if !validator_action_id
183                        .applies_to
184                        .is_applicable_principal_type(principal.entity_type())
185                    {
186                        return Err(RequestValidationError::InvalidPrincipalType {
187                            principal_ty: principal.entity_type().clone(),
188                            action: Arc::clone(action),
189                        });
190                    }
191                }
192                if let EntityUIDEntry::Known(resource) = request.resource() {
193                    if !validator_action_id
194                        .applies_to
195                        .is_applicable_resource_type(resource.entity_type())
196                    {
197                        return Err(RequestValidationError::InvalidResourceType {
198                            resource_ty: resource.entity_type().clone(),
199                            action: Arc::clone(action),
200                        });
201                    }
202                }
203                if let Some(context) = request.context() {
204                    let expected_context_ty = validator_action_id.context_type();
205                    if !expected_context_ty
206                        .typecheck_partial_value(context.as_ref(), extensions)
207                        .map_err(RequestValidationError::TypeOfContext)?
208                    {
209                        return Err(RequestValidationError::InvalidContext {
210                            context: context.clone(),
211                            action: Arc::clone(action),
212                        });
213                    }
214                }
215            }
216            EntityUIDEntry::Unknown => {
217                // We could hypothetically ensure that the concrete parts of the
218                // request are valid for _some_ action, but this is probably more
219                // expensive than we want for this validation step.
220                // Instead, we just let the above checks (that principal and
221                // resource are of types that at least _exist_ in the schema)
222                // suffice.
223            }
224        }
225        Ok(())
226    }
227}
228
229impl<'a> ast::RequestSchema for CoreSchema<'a> {
230    type Error = RequestValidationError;
231    fn validate_request(
232        &self,
233        request: &ast::Request,
234        extensions: Extensions<'_>,
235    ) -> Result<(), Self::Error> {
236        self.schema.validate_request(request, extensions)
237    }
238}
239
240#[derive(Debug, Error)]
241pub enum RequestValidationError {
242    /// Request action is not declared in the schema
243    #[error("request's action `{action}` is not declared in the schema")]
244    UndeclaredAction {
245        /// Action which was not declared in the schema
246        action: Arc<ast::EntityUID>,
247    },
248    /// Request principal is of a type not declared in the schema
249    #[error("principal type `{principal_ty}` is not declared in the schema")]
250    UndeclaredPrincipalType {
251        /// Principal type which was not declared in the schema
252        principal_ty: ast::EntityType,
253    },
254    /// Request resource is of a type not declared in the schema
255    #[error("resource type `{resource_ty}` is not declared in the schema")]
256    UndeclaredResourceType {
257        /// Resource type which was not declared in the schema
258        resource_ty: ast::EntityType,
259    },
260    /// Request principal is of a type that is declared in the schema, but is
261    /// not valid for the request action
262    #[error("principal type `{principal_ty}` is not valid for `{action}`")]
263    InvalidPrincipalType {
264        /// Principal type which is not valid
265        principal_ty: ast::EntityType,
266        /// Action which it is not valid for
267        action: Arc<ast::EntityUID>,
268    },
269    /// Request resource is of a type that is declared in the schema, but is
270    /// not valid for the request action
271    #[error("resource type `{resource_ty}` is not valid for `{action}`")]
272    InvalidResourceType {
273        /// Resource type which is not valid
274        resource_ty: ast::EntityType,
275        /// Action which it is not valid for
276        action: Arc<ast::EntityUID>,
277    },
278    /// Context does not comply with the shape specified for the request action
279    #[error("context `{context}` is not valid for `{action}`")]
280    InvalidContext {
281        /// Context which is not valid
282        context: ast::Context,
283        /// Action which it is not valid for
284        action: Arc<ast::EntityUID>,
285    },
286    /// Error computing the type of the `Context`; see the contained error type
287    /// for details about the kinds of errors that can occur
288    #[error("context is not valid: {0}")]
289    TypeOfContext(GetSchemaTypeError),
290}
291
292/// Struct which carries enough information that it can impl Core's
293/// `ContextSchema`.
294pub struct ContextSchema(
295    // INVARIANT: The `Type` stored in this struct must be representable as a
296    // `SchemaType` to avoid panicking in `context_type`.
297    crate::types::Type,
298);
299
300/// A `Type` contains all the information we need for a Core `ContextSchema`.
301impl entities::ContextSchema for ContextSchema {
302    fn context_type(&self) -> entities::SchemaType {
303        // PANIC SAFETY: By `ContextSchema` invariant, `self.0` is representable as a schema type.
304        #[allow(clippy::expect_used)]
305        self.0
306            .clone()
307            .try_into()
308            .expect("failed to convert validator type into Core SchemaType")
309    }
310}
311
312/// Since different Actions have different schemas for `Context`, you must
313/// specify the `Action` in order to get a `ContextSchema`.
314///
315/// Returns `None` if the action is not in the schema.
316pub fn context_schema_for_action(
317    schema: &ValidatorSchema,
318    action: &ast::EntityUID,
319) -> Option<ContextSchema> {
320    // The invariant on `ContextSchema` requires that the inner type is
321    // representable as a schema type. `ValidatorSchema::context_type`
322    // always returns a closed record type, which are representable as long
323    // as their values are representable. The values are representable
324    // because they are taken from the context of a `ValidatorActionId`
325    // which was constructed directly from a schema.
326    schema.context_type(action).map(ContextSchema)
327}
328
329#[cfg(test)]
330mod test {
331    use super::*;
332    use cool_asserts::assert_matches;
333    use serde_json::json;
334
335    fn schema() -> ValidatorSchema {
336        let src = json!(
337        { "": {
338            "entityTypes": {
339                "User": {
340                    "memberOfTypes": [ "Group" ]
341                },
342                "Group": {
343                    "memberOfTypes": []
344                },
345                "Photo": {
346                    "memberOfTypes": [ "Album" ]
347                },
348                "Album": {
349                    "memberOfTypes": []
350                }
351            },
352            "actions": {
353                "view_photo": {
354                    "appliesTo": {
355                        "principalTypes": ["User", "Group"],
356                        "resourceTypes": ["Photo"]
357                    }
358                },
359                "edit_photo": {
360                    "appliesTo": {
361                        "principalTypes": ["User", "Group"],
362                        "resourceTypes": ["Photo"],
363                        "context": {
364                            "type": "Record",
365                            "attributes": {
366                                "admin_approval": {
367                                    "type": "Boolean",
368                                    "required": true,
369                                }
370                            }
371                        }
372                    }
373                }
374            }
375        }});
376        ValidatorSchema::from_json_value(src, Extensions::all_available())
377            .expect("failed to create ValidatorSchema")
378    }
379
380    /// basic success with concrete request and no context
381    #[test]
382    fn success_concrete_request_no_context() {
383        assert_matches!(
384            ast::Request::new(
385                ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
386                ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(),
387                ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
388                ast::Context::empty(),
389                Some(&schema()),
390                Extensions::all_available(),
391            ),
392            Ok(_)
393        );
394    }
395
396    /// basic success with concrete request and a context
397    #[test]
398    fn success_concrete_request_with_context() {
399        assert_matches!(
400            ast::Request::new(
401                ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
402                ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(),
403                ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
404                ast::Context::from_pairs(
405                    [("admin_approval".into(), ast::RestrictedExpr::val(true))],
406                    Extensions::all_available()
407                )
408                .unwrap(),
409                Some(&schema()),
410                Extensions::all_available(),
411            ),
412            Ok(_)
413        );
414    }
415
416    /// success leaving principal unknown
417    #[test]
418    fn success_principal_unknown() {
419        assert_matches!(
420            ast::Request::new_with_unknowns(
421                ast::EntityUIDEntry::Unknown,
422                ast::EntityUIDEntry::concrete(
423                    ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap()
424                ),
425                ast::EntityUIDEntry::concrete(
426                    ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap()
427                ),
428                Some(ast::Context::empty()),
429                Some(&schema()),
430                Extensions::all_available(),
431            ),
432            Ok(_)
433        );
434    }
435
436    /// success leaving action unknown
437    #[test]
438    fn success_action_unknown() {
439        assert_matches!(
440            ast::Request::new_with_unknowns(
441                ast::EntityUIDEntry::concrete(
442                    ast::EntityUID::with_eid_and_type("User", "abc123").unwrap()
443                ),
444                ast::EntityUIDEntry::Unknown,
445                ast::EntityUIDEntry::concrete(
446                    ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap()
447                ),
448                Some(ast::Context::empty()),
449                Some(&schema()),
450                Extensions::all_available(),
451            ),
452            Ok(_)
453        );
454    }
455
456    /// success leaving resource unknown
457    #[test]
458    fn success_resource_unknown() {
459        assert_matches!(
460            ast::Request::new_with_unknowns(
461                ast::EntityUIDEntry::concrete(
462                    ast::EntityUID::with_eid_and_type("User", "abc123").unwrap()
463                ),
464                ast::EntityUIDEntry::concrete(
465                    ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap()
466                ),
467                ast::EntityUIDEntry::Unknown,
468                Some(ast::Context::empty()),
469                Some(&schema()),
470                Extensions::all_available(),
471            ),
472            Ok(_)
473        );
474    }
475
476    /// success leaving context unknown
477    #[test]
478    fn success_context_unknown() {
479        assert_matches!(
480            ast::Request::new_with_unknowns(
481                ast::EntityUIDEntry::concrete(
482                    ast::EntityUID::with_eid_and_type("User", "abc123").unwrap()
483                ),
484                ast::EntityUIDEntry::concrete(
485                    ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap()
486                ),
487                ast::EntityUIDEntry::concrete(
488                    ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap()
489                ),
490                None,
491                Some(&schema()),
492                Extensions::all_available(),
493            ),
494            Ok(_)
495        )
496    }
497
498    /// success leaving everything unknown
499    #[test]
500    fn success_everything_unspecified() {
501        assert_matches!(
502            ast::Request::new_with_unknowns(
503                ast::EntityUIDEntry::Unknown,
504                ast::EntityUIDEntry::Unknown,
505                ast::EntityUIDEntry::Unknown,
506                None,
507                Some(&schema()),
508                Extensions::all_available(),
509            ),
510            Ok(_)
511        );
512    }
513
514    /// this succeeds for now: unknown action, concrete principal and
515    /// resource of valid types, but none of the schema's actions would work
516    /// with this principal and resource type
517    #[test]
518    fn success_unknown_action_but_invalid_types() {
519        assert_matches!(
520            ast::Request::new_with_unknowns(
521                ast::EntityUIDEntry::concrete(
522                    ast::EntityUID::with_eid_and_type("Album", "abc123").unwrap()
523                ),
524                ast::EntityUIDEntry::Unknown,
525                ast::EntityUIDEntry::concrete(
526                    ast::EntityUID::with_eid_and_type("User", "alice").unwrap()
527                ),
528                None,
529                Some(&schema()),
530                Extensions::all_available(),
531            ),
532            Ok(_)
533        );
534    }
535
536    /// request action not declared in the schema
537    #[test]
538    fn action_not_declared() {
539        assert_matches!(
540            ast::Request::new(
541                ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
542                ast::EntityUID::with_eid_and_type("Action", "destroy").unwrap(),
543                ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
544                ast::Context::empty(),
545                Some(&schema()),
546                Extensions::all_available(),
547            ),
548            Err(RequestValidationError::UndeclaredAction { action }) => {
549                assert_eq!(&*action, &ast::EntityUID::with_eid_and_type("Action", "destroy").unwrap());
550            }
551        );
552    }
553
554    /// request action unspecified (and not declared in the schema)
555    #[test]
556    fn action_unspecified() {
557        assert_matches!(
558            ast::Request::new(
559                ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
560                ast::EntityUID::unspecified_from_eid(ast::Eid::new("blahblah")),
561                ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
562                ast::Context::empty(),
563                Some(&schema()),
564                Extensions::all_available(),
565            ),
566            Err(RequestValidationError::UndeclaredAction { action }) => {
567                assert_eq!(&*action, &ast::EntityUID::unspecified_from_eid(ast::Eid::new("blahblah")));
568            }
569        );
570    }
571
572    /// request principal type not declared in the schema (action concrete)
573    #[test]
574    fn principal_type_not_declared() {
575        assert_matches!(
576            ast::Request::new(
577                ast::EntityUID::with_eid_and_type("Foo", "abc123").unwrap(),
578                ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(),
579                ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
580                ast::Context::empty(),
581                Some(&schema()),
582                Extensions::all_available(),
583            ),
584            Err(RequestValidationError::UndeclaredPrincipalType { principal_ty }) => {
585                assert_eq!(principal_ty, ast::EntityType::Specified(ast::Name::parse_unqualified_name("Foo").unwrap()));
586            }
587        );
588    }
589
590    /// request principal type not declared in the schema (action unspecified)
591    #[test]
592    fn principal_type_not_declared_action_unspecified() {
593        assert_matches!(
594            ast::Request::new(
595                ast::EntityUID::with_eid_and_type("Foo", "abc123").unwrap(),
596                ast::EntityUID::unspecified_from_eid(ast::Eid::new("blahblah")),
597                ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
598                ast::Context::empty(),
599                Some(&schema()),
600                Extensions::all_available(),
601            ),
602            Err(RequestValidationError::UndeclaredPrincipalType { principal_ty }) => {
603                assert_eq!(principal_ty, ast::EntityType::Specified(ast::Name::parse_unqualified_name("Foo").unwrap()));
604            }
605        );
606    }
607
608    /// request principal type unspecified (and not declared in the schema)
609    #[test]
610    fn principal_unspecified() {
611        assert_matches!(
612            ast::Request::new(
613                ast::EntityUID::unspecified_from_eid(ast::Eid::new("principal")),
614                ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(),
615                ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
616                ast::Context::empty(),
617                Some(&schema()),
618                Extensions::all_available(),
619            ),
620            Err(RequestValidationError::InvalidPrincipalType { principal_ty, .. }) => {
621                assert_eq!(principal_ty, ast::EntityType::Unspecified);
622            }
623        );
624    }
625
626    /// request resource type not declared in the schema (action concrete)
627    #[test]
628    fn resource_type_not_declared() {
629        assert_matches!(
630            ast::Request::new(
631                ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
632                ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(),
633                ast::EntityUID::with_eid_and_type("Foo", "vacationphoto94.jpg").unwrap(),
634                ast::Context::empty(),
635                Some(&schema()),
636                Extensions::all_available(),
637            ),
638            Err(RequestValidationError::UndeclaredResourceType { resource_ty }) => {
639                assert_eq!(resource_ty, ast::EntityType::Specified(ast::Name::parse_unqualified_name("Foo").unwrap()));
640            }
641        );
642    }
643
644    /// request resource type not declared in the schema (action unspecified)
645    #[test]
646    fn resource_type_not_declared_action_unspecified() {
647        assert_matches!(
648            ast::Request::new(
649                ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
650                ast::EntityUID::unspecified_from_eid(ast::Eid::new("blahblah")),
651                ast::EntityUID::with_eid_and_type("Foo", "vacationphoto94.jpg").unwrap(),
652                ast::Context::empty(),
653                Some(&schema()),
654                Extensions::all_available(),
655            ),
656            Err(RequestValidationError::UndeclaredResourceType { resource_ty }) => {
657                assert_eq!(resource_ty, ast::EntityType::Specified(ast::Name::parse_unqualified_name("Foo").unwrap()));
658            }
659        );
660    }
661
662    /// request resource type unspecified (and not declared in the schema)
663    #[test]
664    fn resource_unspecified() {
665        assert_matches!(
666            ast::Request::new(
667                ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
668                ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(),
669                ast::EntityUID::unspecified_from_eid(ast::Eid::new("resource")),
670                ast::Context::empty(),
671                Some(&schema()),
672                Extensions::all_available(),
673            ),
674            Err(RequestValidationError::InvalidResourceType { resource_ty, .. }) => {
675                assert_eq!(resource_ty, ast::EntityType::Unspecified);
676            }
677        );
678    }
679
680    /// request principal type declared, but invalid for request's action
681    #[test]
682    fn principal_type_invalid() {
683        assert_matches!(
684            ast::Request::new(
685                ast::EntityUID::with_eid_and_type("Album", "abc123").unwrap(),
686                ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(),
687                ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
688                ast::Context::empty(),
689                Some(&schema()),
690                Extensions::all_available(),
691            ),
692            Err(RequestValidationError::InvalidPrincipalType { principal_ty, action }) => {
693                assert_eq!(principal_ty, ast::EntityType::Specified(ast::Name::parse_unqualified_name("Album").unwrap()));
694                assert_eq!(&*action, &ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap());
695            }
696        );
697    }
698
699    /// request resource type declared, but invalid for request's action
700    #[test]
701    fn resource_type_invalid() {
702        assert_matches!(
703            ast::Request::new(
704                ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
705                ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap(),
706                ast::EntityUID::with_eid_and_type("Group", "coders").unwrap(),
707                ast::Context::empty(),
708                Some(&schema()),
709                Extensions::all_available(),
710            ),
711            Err(RequestValidationError::InvalidResourceType { resource_ty, action }) => {
712                assert_eq!(resource_ty, ast::EntityType::Specified(ast::Name::parse_unqualified_name("Group").unwrap()));
713                assert_eq!(&*action, &ast::EntityUID::with_eid_and_type("Action", "view_photo").unwrap());
714            }
715        );
716    }
717
718    /// request context does not comply with specification: missing attribute
719    #[test]
720    fn context_missing_attribute() {
721        assert_matches!(
722            ast::Request::new(
723                ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
724                ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(),
725                ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
726                ast::Context::empty(),
727                Some(&schema()),
728                Extensions::all_available(),
729            ),
730            Err(RequestValidationError::InvalidContext { context, action }) => {
731                assert_eq!(context, ast::Context::empty());
732                assert_eq!(&*action, &ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap());
733            }
734        );
735    }
736
737    /// request context does not comply with specification: extra attribute
738    #[test]
739    fn context_extra_attribute() {
740        let context_with_extra_attr = ast::Context::from_pairs(
741            [
742                ("admin_approval".into(), ast::RestrictedExpr::val(true)),
743                ("extra".into(), ast::RestrictedExpr::val(42)),
744            ],
745            Extensions::all_available(),
746        )
747        .unwrap();
748        assert_matches!(
749            ast::Request::new(
750                ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
751                ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(),
752                ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
753                context_with_extra_attr.clone(),
754                Some(&schema()),
755                Extensions::all_available(),
756            ),
757            Err(RequestValidationError::InvalidContext { context, action }) => {
758                assert_eq!(context, context_with_extra_attr);
759                assert_eq!(&*action, &ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap());
760            }
761        );
762    }
763
764    /// request context does not comply with specification: attribute is wrong type
765    #[test]
766    fn context_attribute_wrong_type() {
767        let context_with_wrong_type_attr = ast::Context::from_pairs(
768            [(
769                "admin_approval".into(),
770                ast::RestrictedExpr::set([ast::RestrictedExpr::val(true)]),
771            )],
772            Extensions::all_available(),
773        )
774        .unwrap();
775        assert_matches!(
776            ast::Request::new(
777                ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
778                ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(),
779                ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
780                context_with_wrong_type_attr.clone(),
781                Some(&schema()),
782                Extensions::all_available(),
783            ),
784            Err(RequestValidationError::InvalidContext { context, action }) => {
785                assert_eq!(context, context_with_wrong_type_attr);
786                assert_eq!(&*action, &ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap());
787            }
788        );
789    }
790
791    /// request context contains heterogeneous set
792    #[test]
793    fn context_attribute_heterogeneous_set() {
794        let context_with_heterogeneous_set = ast::Context::from_pairs(
795            [(
796                "admin_approval".into(),
797                ast::RestrictedExpr::set([
798                    ast::RestrictedExpr::val(true),
799                    ast::RestrictedExpr::val(-1001),
800                ]),
801            )],
802            Extensions::all_available(),
803        )
804        .unwrap();
805        assert_matches!(
806            ast::Request::new(
807                ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(),
808                ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(),
809                ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(),
810                context_with_heterogeneous_set.clone(),
811                Some(&schema()),
812                Extensions::all_available(),
813            ),
814            Err(RequestValidationError::InvalidContext { context, action }) => {
815                assert_eq!(context, context_with_heterogeneous_set);
816                assert_eq!(&*action, &ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap());
817            }
818        );
819    }
820}