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}