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