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