cedar_policy_core/entities/
conformance.rs

1/*
2 * Copyright Cedar Contributors
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17use std::collections::BTreeMap;
18
19use super::{
20    schematype_of_restricted_expr, EntityTypeDescription, GetSchemaTypeError,
21    HeterogeneousSetError, Schema, SchemaType, TypeMismatchError,
22};
23use crate::ast::{
24    BorrowedRestrictedExpr, Entity, EntityType, EntityUID, PartialValue,
25    PartialValueToRestrictedExprError, RestrictedExpr,
26};
27use crate::extensions::{ExtensionFunctionLookupError, Extensions};
28use either::Either;
29use miette::Diagnostic;
30use smol_str::SmolStr;
31use thiserror::Error;
32
33/// Errors raised when entities do not conform to the schema
34#[derive(Debug, Diagnostic, Error)]
35pub enum EntitySchemaConformanceError {
36    /// Encountered attribute that shouldn't exist on entities of this type
37    #[error("attribute `{attr}` on `{uid}` should not exist according to the schema")]
38    UnexpectedEntityAttr {
39        /// Entity that had the unexpected attribute
40        uid: EntityUID,
41        /// Name of the attribute that was unexpected
42        attr: SmolStr,
43    },
44    /// Didn't encounter attribute that should exist
45    #[error("expected entity `{uid}` to have attribute `{attr}`, but it does not")]
46    MissingRequiredEntityAttr {
47        /// Entity that is missing a required attribute
48        uid: EntityUID,
49        /// Name of the attribute which was expected
50        attr: SmolStr,
51    },
52    /// The given attribute on the given entity had a different type than the
53    /// schema indicated
54    #[error("in attribute `{attr}` on `{uid}`, {err}")]
55    TypeMismatch {
56        /// Entity where the type mismatch occurred
57        uid: EntityUID,
58        /// Name of the attribute where the type mismatch occurred
59        attr: SmolStr,
60        /// Underlying error
61        #[diagnostic(transparent)]
62        err: TypeMismatchError,
63    },
64    /// Found a set whose elements don't all have the same type. This doesn't match
65    /// any possible schema.
66    #[error("in attribute `{attr}` on `{uid}`, {err}")]
67    HeterogeneousSet {
68        /// Entity where the error occurred
69        uid: EntityUID,
70        /// Name of the attribute where the error occurred
71        attr: SmolStr,
72        /// Underlying error
73        #[diagnostic(transparent)]
74        err: HeterogeneousSetError,
75    },
76    /// Found an ancestor of a type that's not allowed for that entity
77    #[error(
78        "`{uid}` is not allowed to have an ancestor of type `{ancestor_ty}` according to the schema"
79    )]
80    InvalidAncestorType {
81        /// Entity that has an invalid ancestor type
82        uid: EntityUID,
83        /// Ancestor type which was invalid
84        ancestor_ty: Box<EntityType>, // boxed to avoid this variant being very large (and thus all EntitySchemaConformanceErrors being large)
85    },
86    /// Encountered an entity of a type which is not declared in the schema.
87    /// Note that this error is only used for non-Action entity types.
88    #[error(transparent)]
89    #[diagnostic(transparent)]
90    UnexpectedEntityType(#[from] UnexpectedEntityTypeError),
91    /// Encountered an action which was not declared in the schema
92    #[error("found action entity `{uid}`, but it was not declared as an action in the schema")]
93    UndeclaredAction {
94        /// Action which was not declared in the schema
95        uid: EntityUID,
96    },
97    /// Encountered an action whose definition doesn't precisely match the
98    /// schema's declaration of that action
99    #[error("definition of action `{uid}` does not match its schema declaration")]
100    #[diagnostic(help(
101        "to use the schema's definition of `{uid}`, simply omit it from the entities input data"
102    ))]
103    ActionDeclarationMismatch {
104        /// Action whose definition mismatched between entity data and schema
105        uid: EntityUID,
106    },
107    /// Error looking up an extension function. This error can occur when
108    /// checking entity conformance because that may require getting information
109    /// about any extension functions referenced in entity attribute values.
110    #[error("in attribute `{attr}` on `{uid}`, {err}")]
111    ExtensionFunctionLookup {
112        /// Entity where the error occurred
113        uid: EntityUID,
114        /// Name of the attribute where the error occurred
115        attr: SmolStr,
116        /// Underlying error
117        #[diagnostic(transparent)]
118        err: ExtensionFunctionLookupError,
119    },
120}
121
122/// Encountered an entity of a type which is not declared in the schema.
123/// Note that this error is only used for non-Action entity types.
124#[derive(Debug, Error)]
125#[error("entity `{uid}` has type `{}` which is not declared in the schema", .uid.entity_type())]
126pub struct UnexpectedEntityTypeError {
127    /// Entity that had the unexpected type
128    pub uid: EntityUID,
129    /// Suggested similar entity types that actually are declared in the schema (if any)
130    pub suggested_types: Vec<EntityType>,
131}
132
133impl Diagnostic for UnexpectedEntityTypeError {
134    fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
135        match self.suggested_types.as_slice() {
136            [] => None,
137            [ty] => Some(Box::new(format!("did you mean `{ty}`?"))),
138            tys => Some(Box::new(format!(
139                "did you mean one of {:?}?",
140                tys.iter().map(ToString::to_string).collect::<Vec<String>>()
141            ))),
142        }
143    }
144}
145
146/// Struct used to check whether entities conform to a schema
147#[derive(Debug, Clone)]
148pub struct EntitySchemaConformanceChecker<'a, S: Schema> {
149    /// Schema to check conformance with
150    schema: &'a S,
151    /// Extensions which are active for the conformance checks
152    extensions: Extensions<'a>,
153}
154
155impl<'a, S: Schema> EntitySchemaConformanceChecker<'a, S> {
156    /// Create a new checker
157    pub fn new(schema: &'a S, extensions: Extensions<'a>) -> Self {
158        Self { schema, extensions }
159    }
160
161    /// Validate an entity against the schema, returning an
162    /// [`EntitySchemaConformanceError`] if it does not comply.
163    pub fn validate_entity(&self, entity: &Entity) -> Result<(), EntitySchemaConformanceError> {
164        let uid = entity.uid();
165        let etype = uid.entity_type();
166        if etype.is_action() {
167            let schema_action = self
168                .schema
169                .action(uid)
170                .ok_or(EntitySchemaConformanceError::UndeclaredAction { uid: uid.clone() })?;
171            // check that the action exactly matches the schema's definition
172            if !entity.deep_eq(&schema_action) {
173                return Err(EntitySchemaConformanceError::ActionDeclarationMismatch {
174                    uid: uid.clone(),
175                });
176            }
177        } else {
178            let schema_etype = self.schema.entity_type(etype).ok_or_else(|| {
179                let suggested_types = match etype {
180                    EntityType::Specified(name) => self
181                        .schema
182                        .entity_types_with_basename(name.basename())
183                        .collect(),
184                    EntityType::Unspecified => vec![],
185                };
186                UnexpectedEntityTypeError {
187                    uid: uid.clone(),
188                    suggested_types,
189                }
190            })?;
191            // Ensure that all required attributes for `etype` are actually
192            // included in `entity`
193            for required_attr in schema_etype.required_attrs() {
194                if entity.get(&required_attr).is_none() {
195                    return Err(EntitySchemaConformanceError::MissingRequiredEntityAttr {
196                        uid: uid.clone(),
197                        attr: required_attr,
198                    });
199                }
200            }
201            // For each attribute that actually appears in `entity`, ensure it
202            // complies with the schema
203            for (attr, val) in entity.attrs() {
204                match schema_etype.attr_type(attr) {
205                    None => {
206                        // `None` indicates the attribute shouldn't exist -- see
207                        // docs on the `attr_type()` trait method
208                        if !schema_etype.open_attributes() {
209                            return Err(EntitySchemaConformanceError::UnexpectedEntityAttr {
210                                uid: uid.clone(),
211                                attr: attr.clone(),
212                            });
213                        }
214                    }
215                    Some(expected_ty) => {
216                        // typecheck: ensure that the entity attribute value matches
217                        // the expected type
218                        match typecheck_value_against_schematype(val, &expected_ty, self.extensions)
219                        {
220                            Ok(()) => {} // typecheck passes
221                            Err(TypecheckError::TypeMismatch(err)) => {
222                                return Err(EntitySchemaConformanceError::TypeMismatch {
223                                    uid: uid.clone(),
224                                    attr: attr.clone(),
225                                    err,
226                                });
227                            }
228                            Err(TypecheckError::HeterogeneousSet(err)) => {
229                                return Err(EntitySchemaConformanceError::HeterogeneousSet {
230                                    uid: uid.clone(),
231                                    attr: attr.clone(),
232                                    err,
233                                });
234                            }
235                            Err(TypecheckError::ExtensionFunctionLookup(err)) => {
236                                return Err(
237                                    EntitySchemaConformanceError::ExtensionFunctionLookup {
238                                        uid: uid.clone(),
239                                        attr: attr.clone(),
240                                        err,
241                                    },
242                                );
243                            }
244                        }
245                    }
246                }
247            }
248            // For each ancestor that actually appears in `entity`, ensure the
249            // ancestor type is allowed by the schema
250            for ancestor_euid in entity.ancestors() {
251                let ancestor_type = ancestor_euid.entity_type();
252                if schema_etype.allowed_parent_types().contains(ancestor_type) {
253                    // note that `allowed_parent_types()` was transitively
254                    // closed, so it's actually `allowed_ancestor_types()`
255                    //
256                    // thus, the check passes in this case
257                } else {
258                    return Err(EntitySchemaConformanceError::InvalidAncestorType {
259                        uid: uid.clone(),
260                        ancestor_ty: Box::new(ancestor_type.clone()),
261                    });
262                }
263            }
264        }
265        Ok(())
266    }
267}
268
269/// Check whether the given `PartialValue` typechecks with the given `SchemaType`.
270/// If the typecheck passes, return `Ok(())`.
271/// If the typecheck fails, return an appropriate `Err`.
272pub fn typecheck_value_against_schematype(
273    value: &PartialValue,
274    expected_ty: &SchemaType,
275    extensions: Extensions<'_>,
276) -> Result<(), TypecheckError> {
277    match RestrictedExpr::try_from(value.clone()) {
278        Ok(expr) => typecheck_restricted_expr_against_schematype(
279            expr.as_borrowed(),
280            expected_ty,
281            extensions,
282        ),
283        Err(PartialValueToRestrictedExprError::NontrivialResidual { .. }) => {
284            // this case should be unreachable for the case of `PartialValue`s
285            // which are entity attributes, because a `PartialValue` computed
286            // from a `RestrictedExpr` should only have trivial residuals.
287            // And as of this writing, there are no callers of this function that
288            // pass anything other than entity attributes.
289            // Nonetheless, rather than relying on these delicate invariants,
290            // it's safe to consider this as passing.
291            Ok(())
292        }
293    }
294}
295
296/// Check whether the given `RestrictedExpr` is a valid instance of `SchemaType`
297pub fn does_restricted_expr_implement_schematype(
298    expr: BorrowedRestrictedExpr<'_>,
299    expected_ty: &SchemaType,
300) -> bool {
301    use SchemaType::*;
302
303    match expected_ty {
304        Bool => expr.as_bool().is_some(),
305        Long => expr.as_long().is_some(),
306        String => expr.as_string().is_some(),
307        EmptySet => expr.as_set_elements().is_some_and(|e| e.count() == 0),
308        Set { .. } if expr.as_set_elements().is_some_and(|e| e.count() == 0) => true,
309        Set { element_ty: elty } => match expr.as_set_elements() {
310            Some(mut els) => els.all(|e| does_restricted_expr_implement_schematype(e, elty)),
311            None => false,
312        },
313        Record { attrs, open_attrs } => match expr.as_record_pairs() {
314            Some(pairs) => {
315                let pairs_map: BTreeMap<&SmolStr, BorrowedRestrictedExpr<'_>> = pairs.collect();
316                let all_req_schema_attrs_in_record = attrs.iter().all(|(k, v)| {
317                    !v.required
318                        || match pairs_map.get(k) {
319                            Some(inner_e) => {
320                                does_restricted_expr_implement_schematype(*inner_e, &v.attr_type)
321                            }
322                            None => false,
323                        }
324                });
325                let all_rec_attrs_match_schema =
326                    pairs_map.iter().all(|(k, inner_e)| match attrs.get(*k) {
327                        Some(sch_ty) => {
328                            does_restricted_expr_implement_schematype(*inner_e, &sch_ty.attr_type)
329                        }
330                        None => *open_attrs,
331                    });
332                all_rec_attrs_match_schema && all_req_schema_attrs_in_record
333            }
334            None => false,
335        },
336        Extension { name } => match expr.as_extn_fn_call() {
337            Some((actual_name, _)) => match name.id.as_ref() {
338                "ipaddr" => actual_name.id.as_ref() == "ip",
339                _ => name == actual_name,
340            },
341            None => false,
342        },
343        Entity { ty } => match expr.as_euid() {
344            Some(actual_euid) => actual_euid.entity_type() == ty,
345            None => false,
346        },
347    }
348}
349
350/// Check whether the given `RestrictedExpr` typechecks with the given `SchemaType`.
351/// If the typecheck passes, return `Ok(())`.
352/// If the typecheck fails, return an appropriate `Err`.
353pub fn typecheck_restricted_expr_against_schematype(
354    expr: BorrowedRestrictedExpr<'_>,
355    expected_ty: &SchemaType,
356    extensions: Extensions<'_>,
357) -> Result<(), TypecheckError> {
358    if does_restricted_expr_implement_schematype(expr, expected_ty) {
359        return Ok(());
360    }
361    match schematype_of_restricted_expr(expr, extensions) {
362        Ok(actual_ty) => Err(TypecheckError::TypeMismatch(TypeMismatchError {
363            expected: Box::new(expected_ty.clone()),
364            actual_ty: Some(Box::new(actual_ty)),
365            actual_val: Either::Right(Box::new(expr.to_owned())),
366        })),
367        Err(GetSchemaTypeError::UnknownInsufficientTypeInfo { .. }) => {
368            // in this case we just don't have the information to know whether
369            // the attribute value (an unknown) matches the expected type.
370            // For now we consider this as passing -- we can't really report a
371            // type error.
372            Ok(())
373        }
374        Err(GetSchemaTypeError::NontrivialResidual { .. }) => {
375            // this case is unreachable according to the invariant in the comments
376            // on `schematype_of_restricted_expr()`.
377            // Nonetheless, rather than relying on that invariant, it's safe to
378            // treat this case like the case above and consider this as passing.
379            Ok(())
380        }
381        Err(GetSchemaTypeError::HeterogeneousSet(err)) => {
382            Err(TypecheckError::HeterogeneousSet(err))
383        }
384        Err(GetSchemaTypeError::ExtensionFunctionLookup(err)) => {
385            Err(TypecheckError::ExtensionFunctionLookup(err))
386        }
387    }
388}
389
390/// Errors returned by [`typecheck_value_against_schematype()`] and
391/// [`typecheck_restricted_expr_against_schematype()`]
392#[derive(Debug, Diagnostic, Error)]
393pub enum TypecheckError {
394    /// The given value had a type different than what was expected
395    #[error(transparent)]
396    #[diagnostic(transparent)]
397    TypeMismatch(#[from] TypeMismatchError),
398    /// The given value contained a heterogeneous set, which doesn't conform to
399    /// any possible `SchemaType`
400    #[error(transparent)]
401    #[diagnostic(transparent)]
402    HeterogeneousSet(#[from] HeterogeneousSetError),
403    /// Error looking up an extension function. This error can occur when
404    /// typechecking a `RestrictedExpr` because that may require getting
405    /// information about any extension functions referenced in the
406    /// `RestrictedExpr`; and it can occur when typechecking a `PartialValue`
407    /// because that may require getting information about any extension
408    /// functions referenced in residuals.
409    #[error(transparent)]
410    #[diagnostic(transparent)]
411    ExtensionFunctionLookup(#[from] ExtensionFunctionLookupError),
412}