cedar_policy_core/validator/
coreschema.rs

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