Skip to main content

cedar_policy_core/pst/
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
17//! Error types for PST (Public Syntax Tree) construction and conversion.
18//!
19//! This module defines errors that can occur when:
20//! - Programmatically constructing PST expressions, policies, and constraints
21//! - Converting between PST and other representations (EST, AST)
22//! - Validating PST structure and semantics
23
24use std::collections::HashSet;
25
26use crate::extensions::ExtensionFunctionLookupError;
27use crate::pst;
28use miette::Diagnostic;
29use smol_str::ToSmolStr;
30use thiserror::Error;
31
32use crate::ast;
33use crate::est;
34
35/// Errors that can occur during PST construction or conversion
36#[derive(Debug, Clone, PartialEq, Eq, Diagnostic, Error)]
37#[non_exhaustive]
38pub enum PstConstructionError {
39    /// A policy is a linked policy but no link id has been provided
40    #[error(transparent)]
41    #[diagnostic(transparent)]
42    PolicyMissingLinkId(#[from] error_body::PolicyMissingLinkIdError),
43
44    /// Action constraints cannot contain template slots
45    #[error(transparent)]
46    #[diagnostic(transparent)]
47    ActionConstraintCannotHaveSlots(#[from] error_body::ActionConstraintCannotHaveSlotsError),
48
49    /// Expected a template with slots, but found a static policy
50    #[error(transparent)]
51    #[diagnostic(transparent)]
52    ExpectedTemplateWithSlots(#[from] error_body::ExpectedTemplateWithSlotsError),
53
54    /// Slot occurs in the wrong position (e.g., principal slot in resource)
55    #[error(transparent)]
56    #[diagnostic(transparent)]
57    WrongSlotPosition(#[from] error_body::WrongSlotPositionError),
58
59    /// Duplicate key found in a record literal
60    #[error(transparent)]
61    #[diagnostic(transparent)]
62    DuplicateRecordKey(#[from] error_body::DuplicateRecordKeyError),
63
64    /// Invalid annotation in a policy or template
65    #[error(transparent)]
66    #[diagnostic(transparent)]
67    InvalidAnnotation(#[from] error_body::InvalidAnnotationError),
68
69    /// Invalid entity UID format or structure
70    #[error(transparent)]
71    #[diagnostic(transparent)]
72    InvalidEntityUid(#[from] error_body::InvalidEntityUidError),
73
74    /// Invalid entity type name
75    #[error(transparent)]
76    #[diagnostic(transparent)]
77    InvalidEntityType(#[from] error_body::InvalidEntityTypeError),
78
79    /// A generic invalid expression error with a description
80    #[error(transparent)]
81    #[diagnostic(transparent)]
82    InvalidExpression(#[from] error_body::InvalidExpressionError),
83
84    /// Unknown function name (not a built-in or registered extension function)
85    #[error(transparent)]
86    #[diagnostic(transparent)]
87    UnknownFunction(#[from] error_body::UnknownFunctionError),
88
89    /// Function called with wrong number of arguments
90    #[error(transparent)]
91    #[diagnostic(transparent)]
92    WrongArity(#[from] error_body::WrongArityError),
93
94    /// Error nodes from parsing are not supported in PST conversion
95    #[error(transparent)]
96    #[diagnostic(transparent)]
97    UnsupportedErrorNode(#[from] error_body::UnsupportedErrorNode),
98
99    /// A parsing error occurred, usually in names
100    #[error(transparent)]
101    #[diagnostic(transparent)]
102    ParsingFailed(#[from] error_body::ParsingFailedError),
103
104    /// A linking error occurred.
105    #[error(transparent)]
106    #[diagnostic(transparent)]
107    LinkingFailed(#[from] error_body::LinkingError),
108
109    /// Contains unexpected slots
110    #[error(transparent)]
111    #[diagnostic(transparent)]
112    ContainsSlots(#[from] error_body::ContainsSlotError),
113}
114
115#[doc(hidden)]
116impl From<est::FromJsonError> for PstConstructionError {
117    fn from(err: est::FromJsonError) -> Self {
118        match err {
119            est::FromJsonError::UnknownExtensionFunction(e) => {
120                error_body::UnknownFunctionError::new(e.to_smolstr()).into()
121            }
122            est::FromJsonError::InvalidEntityType(e) => error_body::InvalidEntityTypeError {
123                description: e.to_string(),
124            }
125            .into(),
126            est::FromJsonError::JsonDeserializationError(e) => {
127                // An error while deserializing JSON can occur only in small transformations; this
128                // is likely a parsing error on a literal.
129                error_body::ParsingFailedError::new(e.to_string()).into()
130            }
131            est::FromJsonError::UnescapeError(e) => {
132                // Show just first error in main error message, like original err
133                error_body::ParsingFailedError::new(e.head.to_string()).into()
134            }
135
136            // Errors below should not occur in normal expression conversion paths, but we still
137            // map them to the closest PST error for completeness.
138            est::FromJsonError::ActionSlot => {
139                error_body::ActionConstraintCannotHaveSlotsError.into()
140            }
141            est::FromJsonError::InvalidActionType(e) => error_body::InvalidEntityTypeError {
142                description: e.to_string(),
143            }
144            .into(),
145            est::FromJsonError::InvalidSlotName => {
146                error_body::ParsingFailedError::new(err.to_string()).into()
147            }
148            est::FromJsonError::TemplateToPolicy(e) => {
149                let mut slots: HashSet<pst::SlotId, _> = HashSet::new();
150                slots.insert(e.slot.id.into());
151                error_body::ContainsSlotError { slots }.into()
152            }
153            est::FromJsonError::PolicyToTemplate(_) => {
154                error_body::ExpectedTemplateWithSlotsError.into()
155            }
156            est::FromJsonError::SlotsInConditionClause(e) => error_body::ContainsSlotError {
157                slots: std::iter::once(e.slot.id.into()).collect(),
158            }
159            .into(),
160            est::FromJsonError::MissingOperator | est::FromJsonError::MultipleOperators { .. } => {
161                error_body::InvalidExpressionError::new(err.to_string()).into()
162            }
163            #[cfg(feature = "tolerant-ast")]
164            est::FromJsonError::ASTErrorNode => {
165                error_body::UnsupportedErrorNode::new("AST contains an error node").into()
166            }
167        }
168    }
169}
170
171#[doc(hidden)]
172impl From<ast::ExpressionConstructionError> for PstConstructionError {
173    fn from(err: ast::ExpressionConstructionError) -> Self {
174        let ast::ExpressionConstructionError::DuplicateKey(k) = err;
175        error_body::DuplicateRecordKeyError { key: k.key }.into()
176    }
177}
178
179#[doc(hidden)]
180impl From<crate::parser::err::ParseErrors> for PstConstructionError {
181    fn from(value: crate::parser::err::ParseErrors) -> Self {
182        error_body::ParsingFailedError::from(value).into()
183    }
184}
185
186// Extension function lookup failed
187
188#[doc(hidden)]
189impl From<ExtensionFunctionLookupError> for PstConstructionError {
190    fn from(err: ExtensionFunctionLookupError) -> Self {
191        let ExtensionFunctionLookupError::FuncDoesNotExist(body) = err;
192        error_body::UnknownFunctionError::new(body.name.to_smolstr()).into()
193    }
194}
195
196/// Error subtypes for [`PstConstructionError`]
197pub mod error_body {
198    use miette::Diagnostic;
199    use smol_str::SmolStr;
200    use std::collections::HashSet;
201    use thiserror::Error;
202
203    use crate::est;
204    use crate::pst;
205
206    /// A policy is a linked policy but no link id has been provided
207    #[derive(Debug, Clone, PartialEq, Eq, Diagnostic, Error)]
208    #[error("linked policy is missing an instance id")]
209    pub struct PolicyMissingLinkIdError;
210
211    /// Action constraints cannot contain template slots
212    #[derive(Debug, Clone, PartialEq, Eq, Diagnostic, Error)]
213    #[error("action constraint cannot have slots")]
214    pub struct ActionConstraintCannotHaveSlotsError;
215
216    /// Expected a template with slots, but found a static policy
217    #[derive(Debug, Clone, PartialEq, Eq, Diagnostic, Error)]
218    #[error("expected a template with slots, but found a static policy")]
219    pub struct ExpectedTemplateWithSlotsError;
220
221    /// Slot occurs in the wrong position (e.g., principal slot in resource)
222    #[derive(Debug, Clone, PartialEq, Eq, Diagnostic, Error)]
223    #[error("slot `{found}` cannot be used in this position (expected slot `{expected}`)")]
224    pub struct WrongSlotPositionError {
225        found: pst::SlotId,
226        expected: pst::SlotId,
227    }
228
229    impl WrongSlotPositionError {
230        pub(crate) fn new(found: pst::SlotId, expected: pst::SlotId) -> Self {
231            Self { found, expected }
232        }
233    }
234
235    /// Duplicate key found in a record literal
236    #[derive(Debug, Clone, PartialEq, Eq, Diagnostic, Error)]
237    #[error("duplicate record key: `{key}`")]
238    pub struct DuplicateRecordKeyError {
239        pub(crate) key: SmolStr,
240    }
241
242    impl DuplicateRecordKeyError {
243        /// The duplicate key
244        pub fn key(&self) -> &str {
245            &self.key
246        }
247    }
248
249    /// Invalid annotation in a policy or template
250    #[derive(Debug, Clone, PartialEq, Eq, Diagnostic, Error)]
251    #[error("invalid annotation: {description}")]
252    pub struct InvalidAnnotationError {
253        description: String,
254    }
255
256    impl InvalidAnnotationError {
257        pub(crate) fn new(description: impl Into<String>) -> Self {
258            Self {
259                description: description.into(),
260            }
261        }
262    }
263
264    /// Invalid entity UID format or structure
265    #[derive(Debug, Clone, PartialEq, Eq, Diagnostic, Error)]
266    #[error("invalid entity UID: {description}")]
267    pub struct InvalidEntityUidError {
268        pub(crate) description: String,
269    }
270
271    /// Invalid entity type error (often failure to parse the name)
272    #[derive(Debug, Clone, PartialEq, Eq, Diagnostic, Error)]
273    #[error("invalid entity type: `{description}`")]
274    pub struct InvalidEntityTypeError {
275        pub(crate) description: String,
276    }
277
278    /// A generic invalid expression error with a description
279    #[derive(Debug, Clone, PartialEq, Eq, Diagnostic, Error)]
280    #[error("invalid expression: {description}")]
281    pub struct InvalidExpressionError {
282        pub(crate) description: String,
283    }
284
285    impl InvalidExpressionError {
286        pub(crate) fn new(description: String) -> Self {
287            Self { description }
288        }
289    }
290
291    /// Unknown function name
292    #[derive(Debug, Clone, PartialEq, Eq, Diagnostic, Error)]
293    #[error("unknown function: `{name}`")]
294    pub struct UnknownFunctionError {
295        pub(crate) name: SmolStr,
296    }
297
298    impl UnknownFunctionError {
299        pub(crate) fn new(name: SmolStr) -> Self {
300            Self { name }
301        }
302
303        /// The unknown function name
304        pub fn name(&self) -> &str {
305            &self.name
306        }
307    }
308
309    /// Function called with wrong number of arguments
310    #[derive(Debug, Clone, PartialEq, Eq, Diagnostic, Error)]
311    #[error("function `{name}` expects {expected} argument(s), got {got}")]
312    pub struct WrongArityError {
313        pub(crate) name: String,
314        pub(crate) expected: usize,
315        pub(crate) got: usize,
316    }
317
318    impl WrongArityError {
319        pub(crate) fn new(name: String, expected: usize, got: usize) -> Self {
320            Self {
321                name,
322                expected,
323                got,
324            }
325        }
326
327        /// The function name
328        pub fn name(&self) -> &str {
329            &self.name
330        }
331
332        /// The expected number of arguments
333        pub fn expected(&self) -> usize {
334            self.expected
335        }
336
337        /// The actual number of arguments provided
338        pub fn got(&self) -> usize {
339            self.got
340        }
341    }
342
343    /// Error nodes from parsing are not supported in PST conversion
344    #[derive(Debug, Clone, PartialEq, Eq, Diagnostic, Error)]
345    #[error("error nodes not supported in conversion: {description}")]
346    pub struct UnsupportedErrorNode {
347        /// Information about where this error node might come from
348        description: &'static str,
349    }
350
351    impl UnsupportedErrorNode {
352        pub(crate) fn new(description: &'static str) -> Self {
353            Self { description }
354        }
355    }
356
357    /// A parsing error occurred
358    #[derive(Debug, Clone, PartialEq, Eq, Diagnostic, Error)]
359    #[error("parse error: {description}")]
360    pub struct ParsingFailedError {
361        pub(crate) description: String,
362    }
363
364    impl ParsingFailedError {
365        pub(crate) fn new(description: String) -> Self {
366            Self { description }
367        }
368    }
369
370    impl From<crate::parser::err::ParseErrors> for ParsingFailedError {
371        fn from(value: crate::parser::err::ParseErrors) -> Self {
372            Self::new(format!("{value}"))
373        }
374    }
375
376    /// Errors that can occur when linking a template policy
377    #[derive(Debug, PartialEq, Eq, Diagnostic, Error, Clone)]
378    pub enum LinkingError {
379        /// Template contains this slot, but a value wasn't provided for it
380        #[error("failed to link template: no value provided for `{slot}`")]
381        MissedSlot {
382            /// Slot which didn't have a value provided for it
383            slot: pst::SlotId,
384        },
385    }
386
387    impl From<LinkingError> for est::LinkingError {
388        fn from(err: LinkingError) -> Self {
389            match err {
390                LinkingError::MissedSlot { slot } => Self::MissedSlot { slot: slot.into() },
391            }
392        }
393    }
394
395    /// The policy or an expression contains slots
396    #[derive(Debug, Clone, PartialEq, Eq, Diagnostic, Error)]
397    #[error("policy or expression contains slots: {slots:?}")]
398    pub struct ContainsSlotError {
399        pub(crate) slots: HashSet<crate::pst::SlotId>,
400    }
401}
402
403#[cfg(test)]
404mod tests {
405    use super::*;
406    use cool_asserts::assert_matches;
407
408    #[test]
409    fn from_json_error_conversions() {
410        use crate::est::FromJsonError;
411
412        // This is a set of rather shallow tests to cover the different cases in FromJsonError.
413        // We don't actually expect those to happen, cause you need a valid EST/AST to then convert
414        // to the PST, so those FromJsonError would already have happened. However, we want to
415        // cover nicely the conversion just in case. The real error cases should be covered in
416        // unit tests in the conversions.
417
418        // JsonDeserializationError
419        let serde_err = serde_json::from_str::<String>("bad").unwrap_err();
420        let json_deser_err: crate::entities::json::err::JsonDeserializationError = serde_err.into();
421        let err: PstConstructionError =
422            FromJsonError::JsonDeserializationError(json_deser_err).into();
423        assert_matches!(err, PstConstructionError::ParsingFailed(..));
424
425        // ActionSlot
426        let err: PstConstructionError = FromJsonError::ActionSlot.into();
427        assert!(matches!(
428            err,
429            PstConstructionError::ActionConstraintCannotHaveSlots(..)
430        ));
431
432        // InvalidActionType
433        let euid = ast::EntityUID::with_eid_and_type("Bad", "act").unwrap();
434        let err: PstConstructionError =
435            FromJsonError::InvalidActionType(crate::parser::err::parse_errors::InvalidActionType {
436                euids: nonempty::nonempty![std::sync::Arc::new(euid)],
437            })
438            .into();
439        assert_matches!(err, PstConstructionError::InvalidEntityType(..));
440
441        // InvalidSlotName
442        let err: PstConstructionError = FromJsonError::InvalidSlotName.into();
443        assert_matches!(err, PstConstructionError::ParsingFailed(..));
444
445        // TemplateToPolicy
446        let err: PstConstructionError = FromJsonError::TemplateToPolicy(
447            crate::parser::err::parse_errors::ExpectedStaticPolicy {
448                slot: ast::Slot {
449                    id: ast::SlotId::principal(),
450                    loc: None,
451                },
452            },
453        )
454        .into();
455        assert_matches!(err, PstConstructionError::ContainsSlots(..));
456
457        // PolicyToTemplate
458        let err: PstConstructionError = FromJsonError::PolicyToTemplate(
459            crate::parser::err::parse_errors::ExpectedTemplate::new(),
460        )
461        .into();
462        assert!(matches!(
463            err,
464            PstConstructionError::ExpectedTemplateWithSlots(..)
465        ));
466
467        // SlotsInConditionClause
468        let err: PstConstructionError = FromJsonError::SlotsInConditionClause(
469            crate::parser::err::parse_errors::SlotsInConditionClause {
470                slot: ast::Slot {
471                    id: ast::SlotId::resource(),
472                    loc: None,
473                },
474                clause_type: "when",
475            },
476        )
477        .into();
478        assert_matches!(err, PstConstructionError::ContainsSlots(..));
479
480        // MissingOperator
481        let err: PstConstructionError = FromJsonError::MissingOperator.into();
482        assert_matches!(err, PstConstructionError::InvalidExpression(..));
483
484        // MultipleOperators
485        let err: PstConstructionError = FromJsonError::MultipleOperators {
486            ops: vec!["a".into(), "b".into()],
487        }
488        .into();
489        assert_matches!(err, PstConstructionError::InvalidExpression(..));
490    }
491
492    #[test]
493    fn from_expression_construction_error() {
494        let err: PstConstructionError = ast::ExpressionConstructionError::DuplicateKey(
495            ast::expression_construction_errors::DuplicateKeyError {
496                key: "k".into(),
497                context: "in record literal",
498            },
499        )
500        .into();
501        assert_matches!(err, PstConstructionError::DuplicateRecordKey(..));
502    }
503
504    #[test]
505    fn from_extension_function_lookup_error() {
506        use crate::extensions::ExtensionFunctionLookupError;
507        let err: PstConstructionError = ExtensionFunctionLookupError::FuncDoesNotExist(
508            crate::extensions::extension_function_lookup_errors::FuncDoesNotExistError {
509                name: "bogus".parse().unwrap(),
510                source_loc: None,
511            },
512        )
513        .into();
514        assert_matches!(err, PstConstructionError::UnknownFunction(..));
515    }
516}