cedar_policy_core/entities/json/
err.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::fmt::Display;
18
19use super::{HeterogeneousSetError, SchemaType};
20use crate::ast::{
21    BorrowedRestrictedExpr, ContextCreationError, EntityAttrEvaluationError, EntityUID, Expr,
22    ExprKind, Name, PartialValue, PolicyID, RestrictedExpr, RestrictedExprError,
23};
24use crate::entities::conformance::EntitySchemaConformanceError;
25use crate::extensions::ExtensionFunctionLookupError;
26use crate::parser::err::ParseErrors;
27use either::Either;
28use itertools::Itertools;
29use miette::Diagnostic;
30use smol_str::SmolStr;
31use thiserror::Error;
32
33/// Escape kind
34#[derive(Debug)]
35pub enum EscapeKind {
36    /// Escape `__entity`
37    Entity,
38    /// Escape `__extn`
39    Extension,
40}
41
42impl Display for EscapeKind {
43    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        match self {
45            Self::Entity => write!(f, "__entity"),
46            Self::Extension => write!(f, "__extn"),
47        }
48    }
49}
50
51/// Errors thrown during deserialization from JSON
52#[derive(Debug, Diagnostic, Error)]
53pub enum JsonDeserializationError {
54    /// Error thrown by the `serde_json` crate
55    #[error(transparent)]
56    Serde(#[from] serde_json::Error),
57    /// Contents of an escape failed to parse.
58    #[error("failed to parse escape `{kind}`: {value}, errors: {errs}")]
59    #[diagnostic(help("{}", match .kind {
60        EscapeKind::Entity => r#"an __entity escape should have a value like `{ "type": "SomeType", "id": "SomeId" }`"#,
61        EscapeKind::Extension => r#"an __extn escape should have a value like `{ "fn": "SomeFn", "arg": "SomeArg" }`"#,
62    }))]
63    ParseEscape {
64        /// Escape kind
65        kind: EscapeKind,
66        /// Escape value at fault
67        value: String,
68        /// Parse errors
69        #[diagnostic(transparent)]
70        errs: ParseErrors,
71    },
72    /// Restricted expression error
73    #[error(transparent)]
74    #[diagnostic(transparent)]
75    RestrictedExpressionError(#[from] RestrictedExprError),
76    /// A field that needs to be a literal entity reference, was some other JSON value
77    #[error("{ctx}, expected a literal entity reference, but got `{}`", display_json_value(.got.as_ref()))]
78    #[diagnostic(help(
79        r#"literal entity references can be made with `{{ "type": "SomeType", "id": "SomeId" }}`"#
80    ))]
81    ExpectedLiteralEntityRef {
82        /// Context of this error
83        ctx: Box<JsonDeserializationErrorContext>,
84        /// the expression we got instead
85        got: Box<Either<serde_json::Value, Expr>>,
86    },
87    /// A field that needs to be an extension value, was some other JSON value
88    #[error("{ctx}, expected an extension value, but got `{}`", display_json_value(.got.as_ref()))]
89    #[diagnostic(help(
90        r#"extension values can be made with `{{ "fn": "SomeFn", "id": "SomeId" }}`"#
91    ))]
92    ExpectedExtnValue {
93        /// Context of this error
94        ctx: Box<JsonDeserializationErrorContext>,
95        /// the expression we got instead
96        got: Box<Either<serde_json::Value, Expr>>,
97    },
98    /// Errors creating the request context from JSON
99    #[error("while parsing context, {0}")]
100    #[diagnostic(transparent)]
101    ContextCreation(#[from] ContextCreationError),
102    /// Parents of actions should be actions, but this action has a non-action parent
103    #[error("action `{uid}` has a non-action parent `{parent}`")]
104    #[diagnostic(help(
105        "parents of actions need to have type `Action` themselves, perhaps namespaced"
106    ))]
107    ActionParentIsNotAction {
108        /// Action entity that had the invalid parent
109        uid: EntityUID,
110        /// Parent that is invalid
111        parent: EntityUID,
112    },
113    /// Schema-based parsing needed an implicit extension constructor, but no suitable
114    /// constructor was found
115    #[error("{ctx}, missing extension constructor for {arg_type} -> {return_type}")]
116    #[diagnostic(help("expected a value of type {return_type} because of the schema"))]
117    MissingImpliedConstructor {
118        /// Context of this error
119        ctx: Box<JsonDeserializationErrorContext>,
120        /// return type of the constructor we were looking for
121        return_type: Box<SchemaType>,
122        /// argument type of the constructor we were looking for
123        arg_type: Box<SchemaType>,
124    },
125    /// The same key appears two or more times in a single record
126    #[error(transparent)]
127    #[diagnostic(transparent)]
128    DuplicateKey(DuplicateKey),
129    /// Error when evaluating an entity attribute
130    #[error(transparent)]
131    #[diagnostic(transparent)]
132    EntityAttributeEvaluation(#[from] EntityAttrEvaluationError),
133    /// During schema-based parsing, encountered an entity which does not
134    /// conform to the schema.
135    ///
136    /// This error contains the `Entity` analogues some of the other errors
137    /// listed below, among other things.
138    #[error(transparent)]
139    #[diagnostic(transparent)]
140    EntitySchemaConformance(EntitySchemaConformanceError),
141    /// During schema-based parsing, encountered this attribute on a record, but
142    /// that attribute shouldn't exist on that record
143    #[error("{ctx}, record attribute `{record_attr}` should not exist according to the schema")]
144    UnexpectedRecordAttr {
145        /// Context of this error
146        ctx: Box<JsonDeserializationErrorContext>,
147        /// Name of the (Record) attribute which was unexpected
148        record_attr: SmolStr,
149    },
150    /// During schema-based parsing, didn't encounter this attribute of a
151    /// record, but that attribute should have existed
152    #[error("{ctx}, expected the record to have an attribute `{record_attr}`, but it does not")]
153    MissingRequiredRecordAttr {
154        /// Context of this error
155        ctx: Box<JsonDeserializationErrorContext>,
156        /// Name of the (Record) attribute which was expected
157        record_attr: SmolStr,
158    },
159    /// During schema-based parsing, found a different type than the schema indicated.
160    ///
161    /// (This is used in all cases except inside entity attributes; type mismatches in
162    /// entity attributes are reported as `Self::EntitySchemaConformance`. As of
163    /// this writing, that means this should only be used for schema-based
164    /// parsing of the `Context`.)
165    #[error("{ctx}, {err}")]
166    TypeMismatch {
167        /// Context of this error, which will be something other than `EntityAttribute`.
168        /// (Type mismatches in entity attributes are reported as
169        /// `Self::EntitySchemaConformance`.)
170        ctx: Box<JsonDeserializationErrorContext>,
171        /// Underlying error
172        #[diagnostic(transparent)]
173        err: TypeMismatchError,
174    },
175    /// During schema-based parsing, found a set whose elements don't all have
176    /// the same type.  This doesn't match any possible schema.
177    ///
178    /// (This is used in all cases except inside entity attributes;
179    /// heterogeneous sets in entity attributes are reported as
180    /// `Self::EntitySchemaConformance`. As of this writing, that means this
181    /// should only be used for schema-based parsing of the `Context`. Note that
182    /// for non-schema-based parsing, heterogeneous sets are not an error.)
183    #[error("{ctx}, {err}")]
184    HeterogeneousSet {
185        /// Context of this error, which will be something other than `EntityAttribute`.
186        /// (Heterogeneous sets in entity attributes are reported as
187        /// `Self::EntitySchemaConformance`.)
188        ctx: Box<JsonDeserializationErrorContext>,
189        /// Underlying error
190        #[diagnostic(transparent)]
191        err: HeterogeneousSetError,
192    },
193    /// During schema-based parsing, error looking up an extension function.
194    /// This error can occur during schema-based parsing because that may
195    /// require getting information about any extension functions referenced in
196    /// the JSON.
197    ///
198    /// (This is used in all cases except inside entity attributes; extension
199    /// function lookup errors in entity attributes are reported as
200    /// `Self::EntitySchemaConformance`. As of this writing, that means this
201    /// should only be used for schema-based parsing of the `Context`.)
202    #[error("{ctx}, {err}")]
203    ExtensionFunctionLookup {
204        /// Context of this error, which will be something other than
205        /// `EntityAttribute`.
206        /// (Extension function lookup errors in entity attributes are reported
207        /// as `Self::EntitySchemaConformance`.)
208        ctx: Box<JsonDeserializationErrorContext>,
209        /// Underlying error
210        #[diagnostic(transparent)]
211        err: ExtensionFunctionLookupError,
212    },
213    /// During schema-based parsing, found an unknown in an _argument_ to an
214    /// extension function being processed in implicit-constructor form. This is
215    /// not currently supported.
216    /// To pass an unknown to an extension function, use the
217    /// explicit-constructor form.
218    #[error("{ctx}, argument `{arg}` to implicit constructor contains an unknown; this is not currently supported")]
219    #[diagnostic(help(
220        r#"expected an extension value here because of the schema. To pass an unknown to an extension function, use the explicit constructor form: `{{ "fn": "SomeFn", "arg": "SomeArg" }}`"#
221    ))]
222    UnknownInImplicitConstructorArg {
223        /// Context of this error
224        ctx: Box<JsonDeserializationErrorContext>,
225        /// Argument which contains an unknown
226        arg: Box<RestrictedExpr>,
227    },
228    /// Raised when a JsonValue contains the no longer supported `__expr` escape
229    #[error("{0}, the `__expr` escape is no longer supported")]
230    #[diagnostic(help("to create an entity reference, use `__entity`; to create an extension value, use `__extn`; and for all other values, use JSON directly"))]
231    ExprTag(Box<JsonDeserializationErrorContext>),
232    /// Raised when the input JSON contains a `null`
233    #[error("{0}, found a `null`; JSON `null`s are not allowed in Cedar")]
234    Null(Box<JsonDeserializationErrorContext>),
235}
236
237impl JsonDeserializationError {
238    pub(crate) fn duplicate_key(
239        ctx: JsonDeserializationErrorContext,
240        key: impl Into<SmolStr>,
241    ) -> Self {
242        Self::DuplicateKey(DuplicateKey {
243            ctx: Box::new(ctx),
244            key: key.into(),
245        })
246    }
247}
248
249#[derive(Debug, Error, Diagnostic)]
250#[error("{}, duplicate key `{}` in record", .ctx, .key)]
251/// Error type for records having duplicate keys
252pub struct DuplicateKey {
253    /// Context of this error
254    ctx: Box<JsonDeserializationErrorContext>,
255    /// The key that appeared two or more times
256    key: SmolStr,
257}
258
259/// Errors thrown during serialization to JSON
260#[derive(Debug, Diagnostic, Error)]
261pub enum JsonSerializationError {
262    /// Error thrown by `serde_json`
263    #[error(transparent)]
264    Serde(#[from] serde_json::Error),
265    /// Extension-function calls with 0 arguments are not currently supported in
266    /// our JSON format.
267    #[error("unsupported call to `{func}` with 0 arguments")]
268    #[diagnostic(help(
269        "extension function calls with 0 arguments are not currently supported in our JSON format"
270    ))]
271    ExtnCall0Arguments {
272        /// Name of the function which was called with 0 arguments
273        func: Name,
274    },
275    /// Extension-function calls with 2 or more arguments are not currently
276    /// supported in our JSON format.
277    #[error("unsupported call to `{func}` with 2 or more arguments")]
278    #[diagnostic(help("extension function calls with 2 or more arguments are not currently supported in our JSON format"))]
279    ExtnCall2OrMoreArguments {
280        /// Name of the function which was called with 2 or more arguments
281        func: Name,
282    },
283    /// Encountered a `Record` which can't be serialized to JSON because it
284    /// contains a key which is reserved as a JSON escape.
285    #[error("record uses reserved key `{key}`")]
286    ReservedKey {
287        /// Reserved key which was used by the `Record`
288        key: SmolStr,
289    },
290    /// Encountered an `ExprKind` which we didn't expect. Either a case is
291    /// missing in `CedarValueJson::from_expr()`, or an internal invariant was
292    /// violated and there is a non-restricted expression in `RestrictedExpr`
293    #[error("unexpected restricted expression `{kind:?}`")]
294    UnexpectedRestrictedExprKind {
295        /// `ExprKind` which we didn't expect to find
296        kind: ExprKind,
297    },
298    /// Encountered a (partial-evaluation) residual which can't be encoded in
299    /// JSON
300    #[error("cannot encode residual as JSON: {residual}")]
301    Residual {
302        /// Residual which can't be encoded in JSON
303        residual: Expr,
304    },
305}
306
307/// Gives information about the context of a JSON deserialization error (e.g.,
308/// where we were in the JSON document).
309#[derive(Debug, Clone)]
310pub enum JsonDeserializationErrorContext {
311    /// The error occurred while deserializing the attribute `attr` of an entity.
312    EntityAttribute {
313        /// Entity where the error occurred
314        uid: EntityUID,
315        /// Attribute where the error occurred
316        attr: SmolStr,
317    },
318    /// The error occurred while deserializing the `parents` field of an entity.
319    EntityParents {
320        /// Entity where the error occurred
321        uid: EntityUID,
322    },
323    /// The error occurred while deserializing the `uid` field of an entity.
324    EntityUid,
325    /// The error occurred while deserializing the `Context`.
326    Context,
327    /// The error occurred while deserializing a policy in JSON (EST) form.
328    Policy {
329        /// ID of the policy we were deserializing
330        id: PolicyID,
331    },
332    /// The error occured while deserializing a template link
333    TemplateLink,
334    /// The context was unknown, this shouldn't surface to users
335    Unknown,
336}
337
338/// Type mismatch error (in terms of `SchemaType`)
339#[derive(Debug, Diagnostic, Error)]
340#[error("type mismatch: value was expected to have type {expected}, but {}: `{}`",
341    match .actual_ty {
342        Some(actual_ty) => format!("actually has type {actual_ty}"),
343        None => "it does not".to_string(),
344    },
345    match .actual_val {
346        Either::Left(pval) => format!("{pval}"),
347        Either::Right(expr) => display_restricted_expr(expr.as_borrowed()),
348    }
349)]
350pub struct TypeMismatchError {
351    /// Type which was expected
352    pub expected: Box<SchemaType>,
353    /// Type which was encountered instead. May be `None` in the case that
354    /// the encountered value was an `Unknown` with insufficient type
355    /// information to produce a `SchemaType`
356    pub actual_ty: Option<Box<SchemaType>>,
357    /// Value which doesn't have the expected type; represented as either a
358    /// PartialValue or RestrictedExpr, whichever is more convenient for the
359    /// caller
360    pub actual_val: Either<PartialValue, Box<RestrictedExpr>>,
361}
362
363impl std::fmt::Display for JsonDeserializationErrorContext {
364    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
365        match self {
366            Self::EntityAttribute { uid, attr } => write!(f, "in attribute `{attr}` on `{uid}`"),
367            Self::EntityParents { uid } => write!(f, "in parents field of `{uid}`"),
368            Self::EntityUid => write!(f, "in uid field of <unknown entity>"),
369            Self::Context => write!(f, "while parsing context"),
370            Self::Policy { id } => write!(f, "while parsing JSON policy `{id}`"),
371            Self::TemplateLink => write!(f, "while parsing a template link"),
372            Self::Unknown => write!(f, "parsing context was unknown, please file a bug report at https://github.com/cedar-policy/cedar so we can improve this error message"),
373        }
374    }
375}
376
377fn display_json_value(v: &Either<serde_json::Value, Expr>) -> String {
378    match v {
379        Either::Left(json) => display_value(json),
380        Either::Right(e) => e.to_string(),
381    }
382}
383
384/// Display a `serde_json::Value`, but sorting object attributes, so that the
385/// output is deterministic (important for tests that check equality of error
386/// messages).
387///
388/// Note that this doesn't sort array elements, because JSON arrays are ordered,
389/// so all JSON-handling functions naturally preserve order for arrays and thus
390/// provide a deterministic output.
391fn display_value(v: &serde_json::Value) -> String {
392    match v {
393        serde_json::Value::Array(contents) => {
394            format!("[{}]", contents.iter().map(display_value).join(", "))
395        }
396        serde_json::Value::Object(map) => {
397            let mut v: Vec<_> = map.iter().collect();
398            // We sort the keys here so that our error messages are consistent and defined
399            v.sort_by_key(|p| p.0);
400            let display_kv = |kv: &(&String, &serde_json::Value)| format!("\"{}\":{}", kv.0, kv.1);
401            format!("{{{}}}", v.iter().map(display_kv).join(","))
402        }
403        other => other.to_string(),
404    }
405}
406
407/// Display a `RestrictedExpr`, but sorting record attributes and set elements,
408/// so that the output is deterministic (important for tests that check equality
409/// of error messages).
410fn display_restricted_expr(expr: BorrowedRestrictedExpr<'_>) -> String {
411    match expr.expr_kind() {
412        ExprKind::Set(elements) => {
413            let restricted_exprs = elements.iter().map(BorrowedRestrictedExpr::new_unchecked); // since the RestrictedExpr invariant holds for the input, it holds for all set elements
414            format!(
415                "[{}]",
416                restricted_exprs
417                    .map(display_restricted_expr)
418                    .sorted_unstable()
419                    .join(", ")
420            )
421        }
422        ExprKind::Record(m) => {
423            format!(
424                "{{{}}}",
425                m.iter()
426                    .sorted_unstable_by_key(|(k, _)| SmolStr::clone(k))
427                    .map(|(k, v)| format!("\"{}\": {}", k.escape_debug(), v))
428                    .join(", ")
429            )
430        }
431        _ => format!("{expr}"), // all other cases: use the normal Display
432    }
433}