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