Skip to main content

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