1use miette::Diagnostic;
20use thiserror::Error;
21
22use std::fmt::Display;
23
24use cedar_policy_core::impl_diagnostic_from_source_loc_opt_field;
25use cedar_policy_core::parser::Loc;
26
27use std::collections::BTreeSet;
28
29use cedar_policy_core::ast::{EntityType, EntityUID, Expr, ExprKind, PolicyID, Var};
30use cedar_policy_core::parser::join_with_conjunction;
31
32use crate::types::{EntityLUB, EntityRecordKind, RequestEnv, Type};
33use itertools::Itertools;
34use smol_str::SmolStr;
35
36#[derive(Debug, Clone, Error, Hash, Eq, PartialEq)]
38#[error("for policy `{policy_id}`, unrecognized entity type `{actual_entity_type}`")]
40pub struct UnrecognizedEntityType {
41 pub source_loc: Option<Loc>,
43 pub policy_id: PolicyID,
45 pub actual_entity_type: String,
47 pub suggested_entity_type: Option<String>,
50}
51
52impl Diagnostic for UnrecognizedEntityType {
53 impl_diagnostic_from_source_loc_opt_field!(source_loc);
54
55 fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
56 match &self.suggested_entity_type {
57 Some(s) => Some(Box::new(format!("did you mean `{s}`?"))),
58 None => None,
59 }
60 }
61}
62
63#[derive(Debug, Clone, Error, Hash, Eq, PartialEq)]
65#[error("for policy `{policy_id}`, unrecognized action `{actual_action_id}`")]
66pub struct UnrecognizedActionId {
67 pub source_loc: Option<Loc>,
69 pub policy_id: PolicyID,
71 pub actual_action_id: String,
73 pub suggested_action_id: Option<String>,
76}
77
78impl Diagnostic for UnrecognizedActionId {
79 impl_diagnostic_from_source_loc_opt_field!(source_loc);
80
81 fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
82 match &self.suggested_action_id {
83 Some(s) => Some(Box::new(format!("did you mean `{s}`?"))),
84 None => None,
85 }
86 }
87}
88
89#[derive(Debug, Clone, Error, Hash, Eq, PartialEq)]
91#[error("for policy `{policy_id}`, unable to find an applicable action given the policy scope constraints")]
92pub struct InvalidActionApplication {
93 pub source_loc: Option<Loc>,
95 pub policy_id: PolicyID,
97 pub would_in_fix_principal: bool,
99 pub would_in_fix_resource: bool,
101}
102
103impl Diagnostic for InvalidActionApplication {
104 impl_diagnostic_from_source_loc_opt_field!(source_loc);
105
106 fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
107 match (self.would_in_fix_principal, self.would_in_fix_resource) {
108 (true, false) => Some(Box::new(
109 "try replacing `==` with `in` in the principal clause",
110 )),
111 (false, true) => Some(Box::new(
112 "try replacing `==` with `in` in the resource clause",
113 )),
114 (true, true) => Some(Box::new(
115 "try replacing `==` with `in` in the principal clause and the resource clause",
116 )),
117 (false, false) => None,
118 }
119 }
120}
121
122#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
124#[error("for policy `{policy_id}`, unexpected type: expected {} but saw {}",
125 match .expected.iter().next() {
126 Some(single) if .expected.len() == 1 => format!("{}", single),
127 _ => .expected.iter().join(", or ")
128 },
129 .actual)]
130pub struct UnexpectedType {
131 pub source_loc: Option<Loc>,
133 pub policy_id: PolicyID,
135 pub expected: BTreeSet<Type>,
137 pub actual: Type,
139 pub help: Option<UnexpectedTypeHelp>,
141}
142
143impl Diagnostic for UnexpectedType {
144 impl_diagnostic_from_source_loc_opt_field!(source_loc);
145
146 fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
147 self.help.as_ref().map(|h| Box::new(h) as Box<dyn Display>)
148 }
149}
150
151#[derive(Error, Debug, Clone, Hash, Eq, PartialEq)]
153pub enum UnexpectedTypeHelp {
154 #[error("try using `like` to examine the contents of a string")]
156 TryUsingLike,
157 #[error(
159 "try using `contains`, `containsAny`, or `containsAll` to examine the contents of a set"
160 )]
161 TryUsingContains,
162 #[error("try using `contains` to test if a single element is in a set")]
164 TryUsingSingleContains,
165 #[error("try using `has` to test for an attribute")]
167 TryUsingHas,
168 #[error("try using `is` to test for an entity type")]
170 TryUsingIs,
171 #[error("try using `in` for entity hierarchy membership")]
173 TryUsingIn,
174 #[error("Cedar only supports run time type tests for entities")]
176 TypeTestNotSupported,
177 #[error("Cedar does not support string concatenation")]
179 ConcatenationNotSupported,
180 #[error("Cedar does not support computing the union, intersection, or difference of sets")]
182 SetOperationsNotSupported,
183}
184
185#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
187pub struct IncompatibleTypes {
188 pub source_loc: Option<Loc>,
190 pub policy_id: PolicyID,
192 pub types: BTreeSet<Type>,
194 pub hint: LubHelp,
196 pub context: LubContext,
198}
199
200impl Diagnostic for IncompatibleTypes {
201 impl_diagnostic_from_source_loc_opt_field!(source_loc);
202
203 fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
204 Some(Box::new(format!(
205 "for policy `{}`, {} must have compatible types. {}",
206 self.policy_id, self.context, self.hint
207 )))
208 }
209}
210
211impl Display for IncompatibleTypes {
212 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213 write!(f, "the types ")?;
214 join_with_conjunction(f, "and", self.types.iter(), |f, t| write!(f, "{t}"))?;
215 write!(f, " are not compatible")
216 }
217}
218
219#[derive(Error, Debug, Clone, Hash, Eq, PartialEq)]
221pub enum LubHelp {
222 #[error("Corresponding attributes of compatible record types must have the same optionality, either both being required or both being optional")]
224 AttributeQualifier,
225 #[error("Compatible record types must have exactly the same attributes")]
227 RecordWidth,
228 #[error("Different entity types are never compatible even when their attributes would be compatible")]
230 EntityType,
231 #[error("Entity and record types are never compatible even when their attributes would be compatible")]
233 EntityRecord,
234 #[error("Types must be exactly equal to be compatible")]
236 None,
237}
238
239#[derive(Error, Debug, Clone, Hash, Eq, PartialEq)]
241pub enum LubContext {
242 #[error("elements of a set")]
244 Set,
245 #[error("both branches of a conditional")]
247 Conditional,
248 #[error("both operands to a `==` expression")]
250 Equality,
251 #[error("elements of the first operand and the second operand to a `contains` expression")]
253 Contains,
254 #[error("elements of both set operands to a `containsAll` or `containsAny` expression")]
256 ContainsAnyAll,
257}
258
259#[derive(Debug, Clone, Hash, PartialEq, Eq, Error)]
261#[error("for policy `{policy_id}`, attribute {attribute_access} not found")]
262pub struct UnsafeAttributeAccess {
263 pub source_loc: Option<Loc>,
265 pub policy_id: PolicyID,
267 pub attribute_access: AttributeAccess,
269 pub suggestion: Option<String>,
271 pub may_exist: bool,
274}
275
276impl Diagnostic for UnsafeAttributeAccess {
277 impl_diagnostic_from_source_loc_opt_field!(source_loc);
278
279 fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
280 match (&self.suggestion, self.may_exist) {
281 (Some(suggestion), false) => Some(Box::new(format!("did you mean `{suggestion}`?"))),
282 (None, true) => Some(Box::new("there may be additional attributes that the validator is not able to reason about".to_string())),
283 (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)"))),
284 (None, false) => None,
285 }
286 }
287}
288
289#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
291#[error("unable to guarantee safety of access to optional attribute {attribute_access}")]
292pub struct UnsafeOptionalAttributeAccess {
293 pub source_loc: Option<Loc>,
295 pub policy_id: PolicyID,
297 pub attribute_access: AttributeAccess,
299}
300
301impl Diagnostic for UnsafeOptionalAttributeAccess {
302 impl_diagnostic_from_source_loc_opt_field!(source_loc);
303
304 fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
305 Some(Box::new(format!(
306 "try testing for the attribute with `{} && ..`",
307 self.attribute_access.suggested_has_guard()
308 )))
309 }
310}
311
312#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
314#[error("for policy `{policy_id}`, undefined extension function: {name}")]
315pub struct UndefinedFunction {
316 pub source_loc: Option<Loc>,
318 pub policy_id: PolicyID,
320 pub name: String,
322}
323
324impl Diagnostic for UndefinedFunction {
325 impl_diagnostic_from_source_loc_opt_field!(source_loc);
326}
327
328#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
330#[error("for policy `{policy_id}`, wrong number of arguments in extension function application. Expected {expected}, got {actual}")]
331pub struct WrongNumberArguments {
332 pub source_loc: Option<Loc>,
334 pub policy_id: PolicyID,
336 pub expected: usize,
338 pub actual: usize,
340}
341
342impl Diagnostic for WrongNumberArguments {
343 impl_diagnostic_from_source_loc_opt_field!(source_loc);
344}
345
346#[derive(Debug, Clone, Hash, Eq, PartialEq, Error)]
348#[error("for policy `{policy_id}`, error during extension function argument validation: {msg}")]
349pub struct FunctionArgumentValidation {
350 pub source_loc: Option<Loc>,
352 pub policy_id: PolicyID,
354 pub msg: String,
356}
357
358impl Diagnostic for FunctionArgumentValidation {
359 impl_diagnostic_from_source_loc_opt_field!(source_loc);
360}
361
362#[derive(Debug, Clone, Hash, Eq, PartialEq, Error)]
364#[error("for policy `{policy_id}`, operands to `in` do not respect the entity hierarchy")]
365pub struct HierarchyNotRespected {
366 pub source_loc: Option<Loc>,
368 pub policy_id: PolicyID,
370 pub in_lhs: Option<EntityType>,
372 pub in_rhs: Option<EntityType>,
374}
375
376impl Diagnostic for HierarchyNotRespected {
377 impl_diagnostic_from_source_loc_opt_field!(source_loc);
378
379 fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
380 match (&self.in_lhs, &self.in_rhs) {
381 (Some(in_lhs), Some(in_rhs)) => Some(Box::new(format!(
382 "`{in_lhs}` cannot be a descendant of `{in_rhs}`"
383 ))),
384 _ => None,
385 }
386 }
387}
388
389#[derive(Debug, Clone, Hash, Eq, PartialEq, Error)]
391#[error("for policy `{policy_id}`, empty set literals are forbidden in policies")]
392pub struct EmptySetForbidden {
393 pub source_loc: Option<Loc>,
395 pub policy_id: PolicyID,
397}
398
399impl Diagnostic for EmptySetForbidden {
400 impl_diagnostic_from_source_loc_opt_field!(source_loc);
401}
402
403#[derive(Debug, Clone, Hash, Eq, PartialEq, Error)]
406#[error("for policy `{policy_id}`, extension constructors may not be called with non-literal expressions")]
407pub struct NonLitExtConstructor {
408 pub source_loc: Option<Loc>,
410 pub policy_id: PolicyID,
412}
413
414impl Diagnostic for NonLitExtConstructor {
415 impl_diagnostic_from_source_loc_opt_field!(source_loc);
416
417 fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
418 Some(Box::new(
419 "consider applying extension constructors inside attribute values when constructing entity or context data"
420 ))
421 }
422}
423
424#[derive(Debug, Clone, Hash, Eq, PartialEq)]
431pub enum AttributeAccess {
432 EntityLUB(EntityLUB, Vec<SmolStr>),
435 Context(EntityUID, Vec<SmolStr>),
439 Other(Vec<SmolStr>),
443}
444
445impl AttributeAccess {
446 pub(crate) fn from_expr(
448 req_env: &RequestEnv<'_>,
449 mut expr: &Expr<Option<Type>>,
450 attr: SmolStr,
451 ) -> AttributeAccess {
452 let mut attrs: Vec<SmolStr> = vec![attr];
453 loop {
454 if let Some(Type::EntityOrRecord(EntityRecordKind::Entity(lub))) = expr.data() {
455 return AttributeAccess::EntityLUB(lub.clone(), attrs);
456 } else if let ExprKind::Var(Var::Context) = expr.expr_kind() {
457 return match req_env.action_entity_uid() {
458 Some(action) => AttributeAccess::Context(action.clone(), attrs),
459 None => AttributeAccess::Other(attrs),
460 };
461 } else if let ExprKind::GetAttr {
462 expr: sub_expr,
463 attr,
464 } = expr.expr_kind()
465 {
466 expr = sub_expr;
467 attrs.push(attr.clone());
468 } else {
469 return AttributeAccess::Other(attrs);
470 }
471 }
472 }
473
474 pub(crate) fn attrs(&self) -> &Vec<SmolStr> {
475 match self {
476 AttributeAccess::EntityLUB(_, attrs) => attrs,
477 AttributeAccess::Context(_, attrs) => attrs,
478 AttributeAccess::Other(attrs) => attrs,
479 }
480 }
481
482 pub(crate) fn suggested_has_guard(&self) -> String {
485 let base_expr = match self {
488 AttributeAccess::Context(_, _) => "context".into(),
489 _ => "e".into(),
490 };
491
492 let (safe_attrs, err_attr) = match self.attrs().split_first() {
493 Some((first, rest)) => (rest, first.clone()),
494 None => (&[] as &[SmolStr], "f".into()),
498 };
499
500 let full_expr = std::iter::once(&base_expr)
501 .chain(safe_attrs.iter().rev())
502 .join(".");
503 format!("{full_expr} has {err_attr}")
504 }
505}
506
507impl Display for AttributeAccess {
508 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
509 let attrs_str = self.attrs().iter().rev().join(".");
510 match self {
511 AttributeAccess::EntityLUB(lub, _) => write!(
512 f,
513 "`{attrs_str}` for entity type{}",
514 match lub.get_single_entity() {
515 Some(single) => format!(" {}", single),
516 _ => format!("s {}", lub.iter().join(", ")),
517 },
518 ),
519 AttributeAccess::Context(action, _) => {
520 write!(f, "`{attrs_str}` in context for {action}",)
521 }
522 AttributeAccess::Other(_) => write!(f, "`{attrs_str}`"),
523 }
524 }
525}
526
527#[cfg(test)]
532mod test_attr_access {
533 use cedar_policy_core::ast::{EntityUID, Expr, ExprBuilder, ExprKind, Var};
534
535 use super::AttributeAccess;
536 use crate::types::{OpenTag, RequestEnv, Type};
537
538 #[allow(clippy::panic)]
540 #[track_caller]
541 fn assert_message_and_help(
542 attr_access: &Expr<Option<Type>>,
543 msg: impl AsRef<str>,
544 help: impl AsRef<str>,
545 ) {
546 let env = RequestEnv::DeclaredAction {
547 principal: &"Principal".parse().unwrap(),
548 action: &EntityUID::with_eid_and_type(
549 cedar_policy_core::ast::ACTION_ENTITY_TYPE,
550 "action",
551 )
552 .unwrap(),
553 resource: &"Resource".parse().unwrap(),
554 context: &Type::record_with_attributes(None, OpenTag::ClosedAttributes),
555 principal_slot: None,
556 resource_slot: None,
557 };
558
559 let ExprKind::GetAttr { expr, attr } = attr_access.expr_kind() else {
560 panic!("Can only test `AttributeAccess::from_expr` for `GetAttr` expressions");
561 };
562
563 let access = AttributeAccess::from_expr(&env, expr, attr.clone());
564 assert_eq!(
565 access.to_string().as_str(),
566 msg.as_ref(),
567 "Error message did not match expected"
568 );
569 assert_eq!(
570 access.suggested_has_guard().as_str(),
571 help.as_ref(),
572 "Suggested has guard did not match expected"
573 );
574 }
575
576 #[test]
577 fn context_access() {
578 let e = ExprBuilder::new().get_attr(ExprBuilder::new().var(Var::Context), "foo".into());
581 assert_message_and_help(
582 &e,
583 "`foo` in context for Action::\"action\"",
584 "context has foo",
585 );
586 let e = ExprBuilder::new().get_attr(e, "bar".into());
587 assert_message_and_help(
588 &e,
589 "`foo.bar` in context for Action::\"action\"",
590 "context.foo has bar",
591 );
592 let e = ExprBuilder::new().get_attr(e, "baz".into());
593 assert_message_and_help(
594 &e,
595 "`foo.bar.baz` in context for Action::\"action\"",
596 "context.foo.bar has baz",
597 );
598 }
599
600 #[test]
601 fn entity_access() {
602 let e = ExprBuilder::new().get_attr(
603 ExprBuilder::with_data(Some(Type::named_entity_reference_from_str("User")))
604 .val("User::\"alice\"".parse::<EntityUID>().unwrap()),
605 "foo".into(),
606 );
607 assert_message_and_help(&e, "`foo` for entity type User", "e has foo");
608 let e = ExprBuilder::new().get_attr(e, "bar".into());
609 assert_message_and_help(&e, "`foo.bar` for entity type User", "e.foo has bar");
610 let e = ExprBuilder::new().get_attr(e, "baz".into());
611 assert_message_and_help(
612 &e,
613 "`foo.bar.baz` for entity type User",
614 "e.foo.bar has baz",
615 );
616 }
617
618 #[test]
619 fn entity_type_attr_access() {
620 let e = ExprBuilder::with_data(Some(Type::named_entity_reference_from_str("Thing")))
621 .get_attr(
622 ExprBuilder::with_data(Some(Type::named_entity_reference_from_str("User")))
623 .var(Var::Principal),
624 "thing".into(),
625 );
626 assert_message_and_help(&e, "`thing` for entity type User", "e has thing");
627 let e = ExprBuilder::new().get_attr(e, "bar".into());
628 assert_message_and_help(&e, "`bar` for entity type Thing", "e has bar");
629 let e = ExprBuilder::new().get_attr(e, "baz".into());
630 assert_message_and_help(&e, "`bar.baz` for entity type Thing", "e.bar has baz");
631 }
632
633 #[test]
634 fn other_access() {
635 let e = ExprBuilder::new().get_attr(
636 ExprBuilder::new().ite(
637 ExprBuilder::new().val(true),
638 ExprBuilder::new().record([]).unwrap(),
639 ExprBuilder::new().record([]).unwrap(),
640 ),
641 "foo".into(),
642 );
643 assert_message_and_help(&e, "`foo`", "e has foo");
644 let e = ExprBuilder::new().get_attr(e, "bar".into());
645 assert_message_and_help(&e, "`foo.bar`", "e.foo has bar");
646 let e = ExprBuilder::new().get_attr(e, "baz".into());
647 assert_message_and_help(&e, "`foo.bar.baz`", "e.foo.bar has baz");
648 }
649}