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}