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