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