cedar_policy_validator/diagnostics/
validation_errors.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//! Defines errors returned by the validator.
18
19use cedar_policy_core::entities::conformance::err::InvalidEnumEntityError;
20use miette::Diagnostic;
21use thiserror::Error;
22
23use std::fmt::Display;
24
25use cedar_policy_core::fuzzy_match::fuzzy_search;
26use cedar_policy_core::impl_diagnostic_from_source_loc_opt_field;
27use cedar_policy_core::parser::Loc;
28
29use std::collections::BTreeSet;
30
31use cedar_policy_core::ast::{Eid, EntityType, EntityUID, Expr, ExprKind, PolicyID, Var};
32use cedar_policy_core::parser::join_with_conjunction;
33
34use crate::level_validate::EntityDerefLevel;
35use crate::types::{EntityLUB, EntityRecordKind, RequestEnv, Type};
36use crate::ValidatorSchema;
37use itertools::Itertools;
38use smol_str::SmolStr;
39
40/// Structure containing details about an unrecognized entity type error.
41#[derive(Debug, Clone, Error, Hash, Eq, PartialEq)]
42// #[error(error_in_policy!("unrecognized entity type `{actual_entity_type}`"))]
43#[error("for policy `{policy_id}`, unrecognized entity type `{actual_entity_type}`")]
44pub struct UnrecognizedEntityType {
45    /// Source location
46    pub source_loc: Option<Loc>,
47    /// Policy ID where the error occurred
48    pub policy_id: PolicyID,
49    /// The entity type seen in the policy.
50    pub actual_entity_type: String,
51    /// An entity type from the schema that the user might reasonably have
52    /// intended to write.
53    pub suggested_entity_type: Option<String>,
54}
55
56impl Diagnostic for UnrecognizedEntityType {
57    impl_diagnostic_from_source_loc_opt_field!(source_loc);
58
59    fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
60        match &self.suggested_entity_type {
61            Some(s) => Some(Box::new(format!("did you mean `{s}`?"))),
62            None => None,
63        }
64    }
65}
66
67/// Structure containing details about an unrecognized action id error.
68#[derive(Debug, Clone, Error, Hash, Eq, PartialEq)]
69#[error("for policy `{policy_id}`, unrecognized action `{actual_action_id}`")]
70pub struct UnrecognizedActionId {
71    /// Source location
72    pub source_loc: Option<Loc>,
73    /// Policy ID where the error occurred
74    pub policy_id: PolicyID,
75    /// Action Id seen in the policy
76    pub actual_action_id: String,
77    /// Hint for resolving the error
78    pub hint: Option<UnrecognizedActionIdHelp>,
79}
80
81impl Diagnostic for UnrecognizedActionId {
82    impl_diagnostic_from_source_loc_opt_field!(source_loc);
83
84    fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
85        self.hint
86            .as_ref()
87            .map(|help| Box::new(help) as Box<dyn std::fmt::Display>)
88    }
89}
90
91/// Help for resolving an unrecognized action id error
92#[derive(Debug, Clone, Error, Hash, Eq, PartialEq)]
93pub enum UnrecognizedActionIdHelp {
94    /// Draw attention to action id including action type (e.g., `Action::"Action::view"`)
95    #[error("did you intend to include the type in action `{0}`?")]
96    AvoidActionTypeInActionId(String),
97    /// Suggest an alternative action
98    #[error("did you mean `{0}`?")]
99    SuggestAlternative(String),
100}
101
102/// Determine the help to offer in the presence of an unrecognized action id error.
103pub fn unrecognized_action_id_help(
104    euid: &EntityUID,
105    schema: &ValidatorSchema,
106) -> Option<UnrecognizedActionIdHelp> {
107    // Check if the user has included the type (i.e., `Action::`) in the action id
108    let eid_str: &str = euid.eid().as_ref();
109    let eid_with_type = format!("Action::{}", eid_str);
110    let eid_with_type_and_quotes = format!("Action::\"{}\"", eid_str);
111    let maybe_id_with_type = schema.action_ids().find(|action_id| {
112        let eid = <Eid as AsRef<str>>::as_ref(action_id.name().eid());
113        eid.contains(&eid_with_type) || eid.contains(&eid_with_type_and_quotes)
114    });
115    if let Some(id) = maybe_id_with_type {
116        // In that case, let the user know about it
117        Some(UnrecognizedActionIdHelp::AvoidActionTypeInActionId(
118            id.name().to_string(),
119        ))
120    } else {
121        // Otherwise, suggest using another id
122        let euids_strs = schema
123            .action_ids()
124            .map(|id| id.name().to_string())
125            .collect::<Vec<_>>();
126        fuzzy_search(euid.eid().as_ref(), &euids_strs)
127            .map(UnrecognizedActionIdHelp::SuggestAlternative)
128    }
129}
130
131/// Structure containing details about an invalid action application error.
132#[derive(Debug, Clone, Error, Hash, Eq, PartialEq)]
133#[error("for policy `{policy_id}`, unable to find an applicable action given the policy scope constraints")]
134pub struct InvalidActionApplication {
135    /// Source location
136    pub source_loc: Option<Loc>,
137    /// Policy ID where the error occurred
138    pub policy_id: PolicyID,
139    /// `true` if changing `==` to `in` wouuld fix the principal clause
140    pub would_in_fix_principal: bool,
141    /// `true` if changing `==` to `in` wouuld fix the resource clause
142    pub would_in_fix_resource: bool,
143}
144
145impl Diagnostic for InvalidActionApplication {
146    impl_diagnostic_from_source_loc_opt_field!(source_loc);
147
148    fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
149        match (self.would_in_fix_principal, self.would_in_fix_resource) {
150            (true, false) => Some(Box::new(
151                "try replacing `==` with `in` in the principal clause",
152            )),
153            (false, true) => Some(Box::new(
154                "try replacing `==` with `in` in the resource clause",
155            )),
156            (true, true) => Some(Box::new(
157                "try replacing `==` with `in` in the principal clause and the resource clause",
158            )),
159            (false, false) => None,
160        }
161    }
162}
163
164/// Structure containing details about an unexpected type error.
165#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
166#[error("for policy `{policy_id}`, unexpected type: expected {} but saw {}",
167    match .expected.iter().next() {
168        Some(single) if .expected.len() == 1 => format!("{}", single),
169        _ => .expected.iter().join(", or ")
170    },
171    .actual)]
172pub struct UnexpectedType {
173    /// Source location
174    pub source_loc: Option<Loc>,
175    /// Policy ID where the error occurred
176    pub policy_id: PolicyID,
177    /// Type(s) which were expected
178    pub expected: Vec<Type>,
179    /// Type which was encountered
180    pub actual: Type,
181    /// Optional help for resolving the error
182    pub help: Option<UnexpectedTypeHelp>,
183}
184
185impl Diagnostic for UnexpectedType {
186    impl_diagnostic_from_source_loc_opt_field!(source_loc);
187
188    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
189        self.help.as_ref().map(|h| Box::new(h) as Box<dyn Display>)
190    }
191}
192
193/// Help for resolving a type error
194#[derive(Error, Debug, Clone, Hash, Eq, PartialEq)]
195pub enum UnexpectedTypeHelp {
196    /// Try using `like`
197    #[error("try using `like` to examine the contents of a string")]
198    TryUsingLike,
199    /// Try using `contains`, `containsAny`, or `containsAll`
200    #[error(
201        "try using `contains`, `containsAny`, or `containsAll` to examine the contents of a set"
202    )]
203    TryUsingContains,
204    /// Try using `contains`
205    #[error("try using `contains` to test if a single element is in a set")]
206    TryUsingSingleContains,
207    /// Try using `has`
208    #[error("try using `has` to test for an attribute")]
209    TryUsingHas,
210    /// Try using `is`
211    #[error("try using `is` to test for an entity type")]
212    TryUsingIs,
213    /// Try using `in`
214    #[error("try using `in` for entity hierarchy membership")]
215    TryUsingIn,
216    /// Try using `== ""`
217    #[error(r#"try using `== ""` to test if a string is empty"#)]
218    TryUsingEqEmptyString,
219    /// Cedar doesn't support type tests
220    #[error("Cedar only supports run time type tests for entities")]
221    TypeTestNotSupported,
222    /// Cedar doesn't support string concatenation
223    #[error("Cedar does not support string concatenation")]
224    ConcatenationNotSupported,
225    /// Cedar doesn't support set union, intersection, or difference
226    #[error("Cedar does not support computing the union, intersection, or difference of sets")]
227    SetOperationsNotSupported,
228}
229
230/// Structure containing details about an incompatible type error.
231#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
232pub struct IncompatibleTypes {
233    /// Source location
234    pub source_loc: Option<Loc>,
235    /// Policy ID where the error occurred
236    pub policy_id: PolicyID,
237    /// Types which are incompatible
238    pub types: BTreeSet<Type>,
239    /// Hint for resolving the error
240    pub hint: LubHelp,
241    /// `LubContext` for the error
242    pub context: LubContext,
243}
244
245impl Diagnostic for IncompatibleTypes {
246    impl_diagnostic_from_source_loc_opt_field!(source_loc);
247
248    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
249        Some(Box::new(format!(
250            "for policy `{}`, {} must have compatible types. {}",
251            self.policy_id, self.context, self.hint
252        )))
253    }
254}
255
256impl Display for IncompatibleTypes {
257    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
258        write!(f, "the types ")?;
259        join_with_conjunction(f, "and", self.types.iter(), |f, t| write!(f, "{t}"))?;
260        write!(f, " are not compatible")
261    }
262}
263
264/// Hints for resolving an incompatible-types error
265#[derive(Error, Debug, Clone, Hash, Eq, PartialEq)]
266pub enum LubHelp {
267    /// Attribute qualifier problems
268    #[error("Corresponding attributes of compatible record types must have the same optionality, either both being required or both being optional")]
269    AttributeQualifier,
270    /// Width subtyping
271    #[error("Compatible record types must have exactly the same attributes")]
272    RecordWidth,
273    /// Entities are nominally typed
274    #[error("Different entity types are never compatible even when their attributes would be compatible")]
275    EntityType,
276    /// Entity and record types are never compatible
277    #[error("Entity and record types are never compatible even when their attributes would be compatible")]
278    EntityRecord,
279    /// Catchall
280    #[error("Types must be exactly equal to be compatible")]
281    None,
282}
283
284/// Text describing where the incompatible-types error was found
285#[derive(Error, Debug, Clone, Hash, Eq, PartialEq)]
286pub enum LubContext {
287    /// In the elements of a set
288    #[error("elements of a set")]
289    Set,
290    /// In the branches of a conditional
291    #[error("both branches of a conditional")]
292    Conditional,
293    /// In the operands to `==`
294    #[error("both operands to a `==` expression")]
295    Equality,
296    /// In the operands of `contains`
297    #[error("elements of the first operand and the second operand to a `contains` expression")]
298    Contains,
299    /// In the operand of `containsAny` or `containsAll`
300    #[error("elements of both set operands to a `containsAll` or `containsAny` expression")]
301    ContainsAnyAll,
302    /// While computing the type of a `.getTag()` operation
303    #[error("tag types for a `.getTag()` operation")]
304    GetTag,
305}
306
307/// Structure containing details about a missing attribute error.
308#[derive(Debug, Clone, Hash, PartialEq, Eq, Error)]
309#[error("for policy `{policy_id}`, attribute {attribute_access} not found")]
310pub struct UnsafeAttributeAccess {
311    /// Source location
312    pub source_loc: Option<Loc>,
313    /// Policy ID where the error occurred
314    pub policy_id: PolicyID,
315    /// More details about the missing-attribute error
316    pub attribute_access: AttributeAccess,
317    /// Optional suggestion for resolving the error
318    pub suggestion: Option<String>,
319    /// When this is true, the attribute might still exist, but the validator
320    /// cannot guarantee that it will.
321    pub may_exist: bool,
322}
323
324impl Diagnostic for UnsafeAttributeAccess {
325    impl_diagnostic_from_source_loc_opt_field!(source_loc);
326
327    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
328        match (&self.suggestion, self.may_exist) {
329            (Some(suggestion), false) => Some(Box::new(format!("did you mean `{suggestion}`?"))),
330            (None, true) => Some(Box::new("there may be additional attributes that the validator is not able to reason about".to_string())),
331            (Some(suggestion), true) => Some(Box::new(format!("did you mean `{suggestion}`? (there may also be additional attributes that the validator is not able to reason about)"))),
332            (None, false) => None,
333        }
334    }
335}
336
337/// Structure containing details about an unsafe optional attribute error.
338#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
339#[error("for policy `{policy_id}`, unable to guarantee safety of access to optional attribute {attribute_access}")]
340pub struct UnsafeOptionalAttributeAccess {
341    /// Source location
342    pub source_loc: Option<Loc>,
343    /// Policy ID where the error occurred
344    pub policy_id: PolicyID,
345    /// More details about the attribute-access error
346    pub attribute_access: AttributeAccess,
347}
348
349impl Diagnostic for UnsafeOptionalAttributeAccess {
350    impl_diagnostic_from_source_loc_opt_field!(source_loc);
351
352    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
353        Some(Box::new(format!(
354            "try testing for the attribute's presence with `{} && ..`",
355            self.attribute_access.suggested_has_guard()
356        )))
357    }
358}
359
360/// Structure containing details about an unsafe tag access error.
361#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
362#[error(
363    "for policy `{policy_id}`, unable to guarantee safety of access to tag `{tag}`{}",
364    match .entity_ty.as_ref().and_then(|lub| lub.get_single_entity()) {
365        Some(ety) => format!(" on entity type `{ety}`"),
366        None => "".to_string()
367    }
368)]
369pub struct UnsafeTagAccess {
370    /// Source location
371    pub source_loc: Option<Loc>,
372    /// Policy ID where the error occurred
373    pub policy_id: PolicyID,
374    /// `EntityLUB` that we tried to access a tag on (or `None` if not an `EntityLUB`, for example, an `AnyEntity`)
375    pub entity_ty: Option<EntityLUB>,
376    /// Tag name which we tried to access. May be a nonconstant `Expr`.
377    pub tag: Expr<Option<Type>>,
378}
379
380impl Diagnostic for UnsafeTagAccess {
381    impl_diagnostic_from_source_loc_opt_field!(source_loc);
382
383    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
384        Some(Box::new(format!(
385            "try testing for the tag's presence with `.hasTag({}) && ..`",
386            &self.tag
387        )))
388    }
389}
390
391/// Structure containing details about a no-tags-allowed error.
392#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
393#[error(
394    "for policy `{policy_id}`, `.getTag()` is not allowed on entities of {} because no `tags` were declared on the entity type in the schema",
395    match .entity_ty.as_ref() {
396        Some(ty) => format!("type `{ty}`"),
397        None => "this type".to_string(),
398    }
399)]
400pub struct NoTagsAllowed {
401    /// Source location
402    pub source_loc: Option<Loc>,
403    /// Policy ID where the error occurred
404    pub policy_id: PolicyID,
405    /// Entity type which we tried to call `.getTag()` on but which doesn't have any tags allowed in the schema
406    ///
407    /// `None` indicates some kind of LUB involving multiple entity types, or `AnyEntity`
408    pub entity_ty: Option<EntityType>,
409}
410
411impl Diagnostic for NoTagsAllowed {
412    impl_diagnostic_from_source_loc_opt_field!(source_loc);
413}
414
415/// Structure containing details about an undefined function error.
416#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
417#[error("for policy `{policy_id}`, undefined extension function: {name}")]
418pub struct UndefinedFunction {
419    /// Source location
420    pub source_loc: Option<Loc>,
421    /// Policy ID where the error occurred
422    pub policy_id: PolicyID,
423    /// Name of the undefined function
424    pub name: String,
425}
426
427impl Diagnostic for UndefinedFunction {
428    impl_diagnostic_from_source_loc_opt_field!(source_loc);
429}
430
431/// Structure containing details about a wrong number of arguments error.
432#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
433#[error("for policy `{policy_id}`, wrong number of arguments in extension function application. Expected {expected}, got {actual}")]
434pub struct WrongNumberArguments {
435    /// Source location
436    pub source_loc: Option<Loc>,
437    /// Policy ID where the error occurred
438    pub policy_id: PolicyID,
439    /// Expected number of arguments
440    pub expected: usize,
441    /// Actual number of arguments
442    pub actual: usize,
443}
444
445impl Diagnostic for WrongNumberArguments {
446    impl_diagnostic_from_source_loc_opt_field!(source_loc);
447}
448
449/// Structure containing details about a function argument validation error.
450#[derive(Debug, Clone, Hash, Eq, PartialEq, Error)]
451#[error("for policy `{policy_id}`, error during extension function argument validation: {msg}")]
452pub struct FunctionArgumentValidation {
453    /// Source location
454    pub source_loc: Option<Loc>,
455    /// Policy ID where the error occurred
456    pub policy_id: PolicyID,
457    /// Error message
458    pub msg: String,
459}
460
461impl Diagnostic for FunctionArgumentValidation {
462    impl_diagnostic_from_source_loc_opt_field!(source_loc);
463}
464
465/// Structure containing details about a hierarchy not respected error
466#[derive(Debug, Clone, Hash, Eq, PartialEq, Error)]
467#[error("Internal invariant violated: `HierarchyNotRespected` error should never occur. Please file an issue")]
468pub struct HierarchyNotRespected {
469    /// Source location
470    pub source_loc: Option<Loc>,
471    /// Policy ID where the error occurred
472    pub policy_id: PolicyID,
473}
474
475impl Diagnostic for HierarchyNotRespected {
476    impl_diagnostic_from_source_loc_opt_field!(source_loc);
477
478    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
479        Some(Box::new("please file an issue at <https://github.com/cedar-policy/cedar/issues> including the schema and policy that caused this error"))
480    }
481}
482
483/// Structure containing details about entity dereference level violation
484#[derive(Debug, Clone, Hash, Eq, PartialEq, Error)]
485#[error("for policy `{policy_id}`, {violation_kind}")]
486pub struct EntityDerefLevelViolation {
487    /// Location of outer most dereference
488    pub source_loc: Option<Loc>,
489    /// Policy ID where the error occurred
490    pub policy_id: PolicyID,
491    /// Provides more information about the specific kind of violation
492    pub violation_kind: EntityDerefViolationKind,
493}
494
495/// Details for specific kinds of entity deref level violations
496#[derive(Debug, Clone, Hash, Eq, PartialEq, Error)]
497pub enum EntityDerefViolationKind {
498    /// The policy exceeded the maximum allowed level
499    #[error(
500        "this policy requires level {actual_level}, which exceeds the maximum allowed level ({allowed_level})"
501    )]
502    MaximumLevelExceeded {
503        /// The maximum level allowed by the schema
504        allowed_level: EntityDerefLevel,
505        /// The actual level this policy uses
506        actual_level: EntityDerefLevel,
507    },
508    /// The policy dereferences an entity literal, which isn't allowed at any level
509    #[error("entity literals cannot be dereferenced at any level")]
510    LiteralDerefTarget,
511}
512
513impl Diagnostic for EntityDerefLevelViolation {
514    impl_diagnostic_from_source_loc_opt_field!(source_loc);
515}
516
517/// The policy uses an empty set literal in a way that is forbidden
518#[derive(Debug, Clone, Hash, Eq, PartialEq, Error)]
519#[error("for policy `{policy_id}`, empty set literals are forbidden in policies")]
520pub struct EmptySetForbidden {
521    /// Source location
522    pub source_loc: Option<Loc>,
523    /// Policy ID where the error occurred
524    pub policy_id: PolicyID,
525}
526
527impl Diagnostic for EmptySetForbidden {
528    impl_diagnostic_from_source_loc_opt_field!(source_loc);
529}
530
531/// The policy passes a non-literal to an extension constructor, which is
532/// forbidden in strict validation
533#[derive(Debug, Clone, Hash, Eq, PartialEq, Error)]
534#[error("for policy `{policy_id}`, extension constructors may not be called with non-literal expressions")]
535pub struct NonLitExtConstructor {
536    /// Source location
537    pub source_loc: Option<Loc>,
538    /// Policy ID where the error occurred
539    pub policy_id: PolicyID,
540}
541
542impl Diagnostic for NonLitExtConstructor {
543    impl_diagnostic_from_source_loc_opt_field!(source_loc);
544
545    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
546        Some(Box::new(
547            "consider applying extension constructors inside attribute values when constructing entity or context data"
548        ))
549    }
550}
551
552/// Returned when an internal invariant is violated (should not happen; if
553/// this is ever returned, please file an issue)
554#[derive(Debug, Clone, Hash, Eq, PartialEq, Error)]
555#[error("internal invariant violated")]
556pub struct InternalInvariantViolation {
557    /// Source location
558    pub source_loc: Option<Loc>,
559    /// Policy ID where the error occurred
560    pub policy_id: PolicyID,
561}
562
563impl Diagnostic for InternalInvariantViolation {
564    impl_diagnostic_from_source_loc_opt_field!(source_loc);
565
566    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
567        Some(Box::new(
568            "please file an issue at <https://github.com/cedar-policy/cedar/issues> including the schema and policy for which you observed the issue"
569        ))
570    }
571}
572
573/// Contains more detailed information about an attribute access when it occurs
574/// on an entity type expression or on the `context` variable. Track a `Vec` of
575/// attributes rather than a single attribute so that on `principal.foo.bar` can
576/// report that the record attribute `foo` of an entity type (e.g., `User`)
577/// needs attributes `bar` instead of giving up when the immediate target of the
578/// attribute access is not a entity.
579#[derive(Debug, Clone, Hash, Eq, PartialEq)]
580pub enum AttributeAccess {
581    /// The attribute access is some sequence of attributes accesses eventually
582    /// targeting an [`EntityLUB`].
583    EntityLUB(EntityLUB, Vec<SmolStr>),
584    /// The attribute access is some sequence of attributes accesses eventually
585    /// targeting the `context` variable. The context being accessed is identified
586    /// by the [`EntityUID`] for the associated action.
587    Context(EntityUID, Vec<SmolStr>),
588    /// Other cases where we do not attempt to give more information about the
589    /// access. This includes any access on the `AnyEntity` type and on record
590    /// types other than the `context` variable.
591    Other(Vec<SmolStr>),
592}
593
594impl AttributeAccess {
595    /// Construct an `AttributeAccess` access from a `GetAttr` expression `expr.attr`.
596    pub(crate) fn from_expr(
597        req_env: &RequestEnv<'_>,
598        mut expr: &Expr<Option<Type>>,
599        attr: SmolStr,
600    ) -> AttributeAccess {
601        let mut attrs: Vec<SmolStr> = vec![attr];
602        loop {
603            if let Some(Type::EntityOrRecord(EntityRecordKind::Entity(lub))) = expr.data() {
604                return AttributeAccess::EntityLUB(lub.clone(), attrs);
605            } else if matches!(expr.expr_kind(), ExprKind::Var(Var::Context)) {
606                return match req_env.action_entity_uid() {
607                    Some(action) => AttributeAccess::Context(action.clone(), attrs),
608                    None => AttributeAccess::Other(attrs),
609                };
610            } else if let ExprKind::GetAttr {
611                expr: sub_expr,
612                attr,
613            } = expr.expr_kind()
614            {
615                expr = sub_expr;
616                attrs.push(attr.clone());
617            } else {
618                return AttributeAccess::Other(attrs);
619            }
620        }
621    }
622
623    pub(crate) fn attrs(&self) -> &Vec<SmolStr> {
624        match self {
625            AttributeAccess::EntityLUB(_, attrs) => attrs,
626            AttributeAccess::Context(_, attrs) => attrs,
627            AttributeAccess::Other(attrs) => attrs,
628        }
629    }
630
631    /// Construct a `has` expression that we can use to suggest a fix after an
632    /// unsafe optional attribute access.
633    pub(crate) fn suggested_has_guard(&self) -> String {
634        // We know if this is an access directly on `context`, so we can suggest
635        // specifically `context has ..`. Otherwise, we just use a generic `e`.
636        let base_expr = match self {
637            AttributeAccess::Context(_, _) => "context".into(),
638            _ => "e".into(),
639        };
640
641        let (safe_attrs, err_attr) = match self.attrs().split_first() {
642            Some((first, rest)) => (rest, first.clone()),
643            // We should always have a least one attribute stored, so this
644            // shouldn't be possible. If it does happen, just use a placeholder
645            // attribute name `f` since we'd rather avoid panicking.
646            None => (&[] as &[SmolStr], "f".into()),
647        };
648
649        let full_expr = std::iter::once(&base_expr)
650            .chain(safe_attrs.iter().rev())
651            .join(".");
652        format!("{full_expr} has {err_attr}")
653    }
654}
655
656impl Display for AttributeAccess {
657    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
658        let attrs_str = self.attrs().iter().rev().join(".");
659        match self {
660            AttributeAccess::EntityLUB(lub, _) => write!(
661                f,
662                "`{attrs_str}` on entity type{}",
663                match lub.get_single_entity() {
664                    Some(single) => format!(" `{}`", single),
665                    _ => format!("s {}", lub.iter().map(|ety| format!("`{ety}`")).join(", ")),
666                },
667            ),
668            AttributeAccess::Context(action, _) => {
669                write!(f, "`{attrs_str}` in context for {action}",)
670            }
671            AttributeAccess::Other(_) => write!(f, "`{attrs_str}`"),
672        }
673    }
674}
675
676/// Returned when an entity literal is of an enumerated entity type but has
677/// undeclared UID
678#[derive(Debug, Clone, Error, Hash, Eq, PartialEq)]
679#[error("for policy `{policy_id}`: {err}")]
680pub struct InvalidEnumEntity {
681    /// Source location
682    pub source_loc: Option<Loc>,
683    /// Policy ID where the error occurred
684    pub policy_id: PolicyID,
685    /// The error
686    pub err: InvalidEnumEntityError,
687}
688
689impl Diagnostic for InvalidEnumEntity {
690    impl_diagnostic_from_source_loc_opt_field!(source_loc);
691
692    fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
693        self.err.help()
694    }
695}
696
697// These tests all assume that the typechecker found an error while checking the
698// outermost `GetAttr` in the expressions. If the attribute didn't exist at all,
699// only the primary message would included in the final error. If it was an
700// optional attribute without a guard, then the help message is also printed.
701#[cfg(test)]
702mod test_attr_access {
703    use cedar_policy_core::{
704        ast::{EntityUID, Expr, ExprBuilder, ExprKind, Var},
705        expr_builder::ExprBuilder as _,
706    };
707
708    use super::AttributeAccess;
709    use crate::types::{OpenTag, RequestEnv, Type};
710
711    // PANIC SAFETY: testing
712    #[allow(clippy::panic)]
713    #[track_caller]
714    fn assert_message_and_help(
715        attr_access: &Expr<Option<Type>>,
716        msg: impl AsRef<str>,
717        help: impl AsRef<str>,
718    ) {
719        let env = RequestEnv::DeclaredAction {
720            principal: &"Principal".parse().unwrap(),
721            action: &EntityUID::with_eid_and_type(
722                cedar_policy_core::ast::ACTION_ENTITY_TYPE,
723                "action",
724            )
725            .unwrap(),
726            resource: &"Resource".parse().unwrap(),
727            context: &Type::record_with_attributes(None, OpenTag::ClosedAttributes),
728            principal_slot: None,
729            resource_slot: None,
730        };
731
732        let ExprKind::GetAttr { expr, attr } = attr_access.expr_kind() else {
733            panic!("Can only test `AttributeAccess::from_expr` for `GetAttr` expressions");
734        };
735
736        let access = AttributeAccess::from_expr(&env, expr, attr.clone());
737        assert_eq!(
738            access.to_string().as_str(),
739            msg.as_ref(),
740            "Error message did not match expected"
741        );
742        assert_eq!(
743            access.suggested_has_guard().as_str(),
744            help.as_ref(),
745            "Suggested has guard did not match expected"
746        );
747    }
748
749    #[test]
750    fn context_access() {
751        // We have to build the Expr manually because the `EntityLUB` case
752        // requires type annotations, even though the other cases ignore them.
753        let e = ExprBuilder::new().get_attr(ExprBuilder::new().var(Var::Context), "foo".into());
754        assert_message_and_help(
755            &e,
756            "`foo` in context for Action::\"action\"",
757            "context has foo",
758        );
759        let e = ExprBuilder::new().get_attr(e, "bar".into());
760        assert_message_and_help(
761            &e,
762            "`foo.bar` in context for Action::\"action\"",
763            "context.foo has bar",
764        );
765        let e = ExprBuilder::new().get_attr(e, "baz".into());
766        assert_message_and_help(
767            &e,
768            "`foo.bar.baz` in context for Action::\"action\"",
769            "context.foo.bar has baz",
770        );
771    }
772
773    #[test]
774    fn entity_access() {
775        let e = ExprBuilder::new().get_attr(
776            ExprBuilder::with_data(Some(Type::named_entity_reference_from_str("User")))
777                .val("User::\"alice\"".parse::<EntityUID>().unwrap()),
778            "foo".into(),
779        );
780        assert_message_and_help(&e, "`foo` on entity type `User`", "e has foo");
781        let e = ExprBuilder::new().get_attr(e, "bar".into());
782        assert_message_and_help(&e, "`foo.bar` on entity type `User`", "e.foo has bar");
783        let e = ExprBuilder::new().get_attr(e, "baz".into());
784        assert_message_and_help(
785            &e,
786            "`foo.bar.baz` on entity type `User`",
787            "e.foo.bar has baz",
788        );
789    }
790
791    #[test]
792    fn entity_type_attr_access() {
793        let e = ExprBuilder::with_data(Some(Type::named_entity_reference_from_str("Thing")))
794            .get_attr(
795                ExprBuilder::with_data(Some(Type::named_entity_reference_from_str("User")))
796                    .var(Var::Principal),
797                "thing".into(),
798            );
799        assert_message_and_help(&e, "`thing` on entity type `User`", "e has thing");
800        let e = ExprBuilder::new().get_attr(e, "bar".into());
801        assert_message_and_help(&e, "`bar` on entity type `Thing`", "e has bar");
802        let e = ExprBuilder::new().get_attr(e, "baz".into());
803        assert_message_and_help(&e, "`bar.baz` on entity type `Thing`", "e.bar has baz");
804    }
805
806    #[test]
807    fn other_access() {
808        let e = ExprBuilder::new().get_attr(
809            ExprBuilder::new().ite(
810                ExprBuilder::new().val(true),
811                ExprBuilder::new().record([]).unwrap(),
812                ExprBuilder::new().record([]).unwrap(),
813            ),
814            "foo".into(),
815        );
816        assert_message_and_help(&e, "`foo`", "e has foo");
817        let e = ExprBuilder::new().get_attr(e, "bar".into());
818        assert_message_and_help(&e, "`foo.bar`", "e.foo has bar");
819        let e = ExprBuilder::new().get_attr(e, "baz".into());
820        assert_message_and_help(&e, "`foo.bar.baz`", "e.foo.bar has baz");
821    }
822}