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