use miette::Diagnostic;
use thiserror::Error;
use std::fmt::Display;
use std::ops::{Add, Neg};
use cedar_policy_core::fuzzy_match::fuzzy_search;
use cedar_policy_core::impl_diagnostic_from_source_loc_opt_field;
use cedar_policy_core::parser::Loc;
use std::collections::BTreeSet;
use cedar_policy_core::ast::{Eid, EntityType, EntityUID, Expr, ExprKind, PolicyID, Var};
use cedar_policy_core::parser::join_with_conjunction;
use crate::types::{EntityLUB, EntityRecordKind, RequestEnv, Type};
use crate::ValidatorSchema;
use itertools::Itertools;
use smol_str::SmolStr;
#[derive(Debug, Clone, Error, Hash, Eq, PartialEq)]
#[error("for policy `{policy_id}`, unrecognized entity type `{actual_entity_type}`")]
pub struct UnrecognizedEntityType {
pub source_loc: Option<Loc>,
pub policy_id: PolicyID,
pub actual_entity_type: String,
pub suggested_entity_type: Option<String>,
}
impl Diagnostic for UnrecognizedEntityType {
impl_diagnostic_from_source_loc_opt_field!(source_loc);
fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
match &self.suggested_entity_type {
Some(s) => Some(Box::new(format!("did you mean `{s}`?"))),
None => None,
}
}
}
#[derive(Debug, Clone, Error, Hash, Eq, PartialEq)]
#[error("for policy `{policy_id}`, unrecognized action `{actual_action_id}`")]
pub struct UnrecognizedActionId {
pub source_loc: Option<Loc>,
pub policy_id: PolicyID,
pub actual_action_id: String,
pub hint: Option<UnrecognizedActionIdHelp>,
}
impl Diagnostic for UnrecognizedActionId {
impl_diagnostic_from_source_loc_opt_field!(source_loc);
fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
self.hint
.as_ref()
.map(|help| Box::new(help) as Box<dyn std::fmt::Display>)
}
}
#[derive(Debug, Clone, Error, Hash, Eq, PartialEq)]
pub enum UnrecognizedActionIdHelp {
#[error("did you intend to include the type in action `{0}`?")]
AvoidActionTypeInActionId(String),
#[error("did you mean `{0}`?")]
SuggestAlternative(String),
}
pub fn unrecognized_action_id_help(
euid: &EntityUID,
schema: &ValidatorSchema,
) -> Option<UnrecognizedActionIdHelp> {
let eid_str: &str = euid.eid().as_ref();
let eid_with_type = format!("Action::{}", eid_str);
let eid_with_type_and_quotes = format!("Action::\"{}\"", eid_str);
let maybe_id_with_type = schema.known_action_ids().find(|euid| {
let eid = <Eid as AsRef<str>>::as_ref(euid.eid());
eid.contains(&eid_with_type) || eid.contains(&eid_with_type_and_quotes)
});
if let Some(id) = maybe_id_with_type {
Some(UnrecognizedActionIdHelp::AvoidActionTypeInActionId(
id.to_string(),
))
} else {
let euids_strs = schema
.known_action_ids()
.map(ToString::to_string)
.collect::<Vec<_>>();
fuzzy_search(euid.eid().as_ref(), &euids_strs)
.map(UnrecognizedActionIdHelp::SuggestAlternative)
}
}
#[derive(Debug, Clone, Error, Hash, Eq, PartialEq)]
#[error("for policy `{policy_id}`, unable to find an applicable action given the policy scope constraints")]
pub struct InvalidActionApplication {
pub source_loc: Option<Loc>,
pub policy_id: PolicyID,
pub would_in_fix_principal: bool,
pub would_in_fix_resource: bool,
}
impl Diagnostic for InvalidActionApplication {
impl_diagnostic_from_source_loc_opt_field!(source_loc);
fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
match (self.would_in_fix_principal, self.would_in_fix_resource) {
(true, false) => Some(Box::new(
"try replacing `==` with `in` in the principal clause",
)),
(false, true) => Some(Box::new(
"try replacing `==` with `in` in the resource clause",
)),
(true, true) => Some(Box::new(
"try replacing `==` with `in` in the principal clause and the resource clause",
)),
(false, false) => None,
}
}
}
#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
#[error("for policy `{policy_id}`, unexpected type: expected {} but saw {}",
match .expected.iter().next() {
Some(single) if .expected.len() == 1 => format!("{}", single),
_ => .expected.iter().join(", or ")
},
.actual)]
pub struct UnexpectedType {
pub source_loc: Option<Loc>,
pub policy_id: PolicyID,
pub expected: BTreeSet<Type>,
pub actual: Type,
pub help: Option<UnexpectedTypeHelp>,
}
impl Diagnostic for UnexpectedType {
impl_diagnostic_from_source_loc_opt_field!(source_loc);
fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
self.help.as_ref().map(|h| Box::new(h) as Box<dyn Display>)
}
}
#[derive(Error, Debug, Clone, Hash, Eq, PartialEq)]
pub enum UnexpectedTypeHelp {
#[error("try using `like` to examine the contents of a string")]
TryUsingLike,
#[error(
"try using `contains`, `containsAny`, or `containsAll` to examine the contents of a set"
)]
TryUsingContains,
#[error("try using `contains` to test if a single element is in a set")]
TryUsingSingleContains,
#[error("try using `has` to test for an attribute")]
TryUsingHas,
#[error("try using `is` to test for an entity type")]
TryUsingIs,
#[error("try using `in` for entity hierarchy membership")]
TryUsingIn,
#[error("Cedar only supports run time type tests for entities")]
TypeTestNotSupported,
#[error("Cedar does not support string concatenation")]
ConcatenationNotSupported,
#[error("Cedar does not support computing the union, intersection, or difference of sets")]
SetOperationsNotSupported,
}
#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
pub struct IncompatibleTypes {
pub source_loc: Option<Loc>,
pub policy_id: PolicyID,
pub types: BTreeSet<Type>,
pub hint: LubHelp,
pub context: LubContext,
}
impl Diagnostic for IncompatibleTypes {
impl_diagnostic_from_source_loc_opt_field!(source_loc);
fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
Some(Box::new(format!(
"for policy `{}`, {} must have compatible types. {}",
self.policy_id, self.context, self.hint
)))
}
}
impl Display for IncompatibleTypes {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "the types ")?;
join_with_conjunction(f, "and", self.types.iter(), |f, t| write!(f, "{t}"))?;
write!(f, " are not compatible")
}
}
#[derive(Error, Debug, Clone, Hash, Eq, PartialEq)]
pub enum LubHelp {
#[error("Corresponding attributes of compatible record types must have the same optionality, either both being required or both being optional")]
AttributeQualifier,
#[error("Compatible record types must have exactly the same attributes")]
RecordWidth,
#[error("Different entity types are never compatible even when their attributes would be compatible")]
EntityType,
#[error("Entity and record types are never compatible even when their attributes would be compatible")]
EntityRecord,
#[error("Types must be exactly equal to be compatible")]
None,
}
#[derive(Error, Debug, Clone, Hash, Eq, PartialEq)]
pub enum LubContext {
#[error("elements of a set")]
Set,
#[error("both branches of a conditional")]
Conditional,
#[error("both operands to a `==` expression")]
Equality,
#[error("elements of the first operand and the second operand to a `contains` expression")]
Contains,
#[error("elements of both set operands to a `containsAll` or `containsAny` expression")]
ContainsAnyAll,
#[error("tag types for a `.getTag()` operation")]
GetTag,
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, Error)]
#[error("for policy `{policy_id}`, attribute {attribute_access} not found")]
pub struct UnsafeAttributeAccess {
pub source_loc: Option<Loc>,
pub policy_id: PolicyID,
pub attribute_access: AttributeAccess,
pub suggestion: Option<String>,
pub may_exist: bool,
}
impl Diagnostic for UnsafeAttributeAccess {
impl_diagnostic_from_source_loc_opt_field!(source_loc);
fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
match (&self.suggestion, self.may_exist) {
(Some(suggestion), false) => Some(Box::new(format!("did you mean `{suggestion}`?"))),
(None, true) => Some(Box::new("there may be additional attributes that the validator is not able to reason about".to_string())),
(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)"))),
(None, false) => None,
}
}
}
#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
#[error("for policy `{policy_id}`, unable to guarantee safety of access to optional attribute {attribute_access}")]
pub struct UnsafeOptionalAttributeAccess {
pub source_loc: Option<Loc>,
pub policy_id: PolicyID,
pub attribute_access: AttributeAccess,
}
impl Diagnostic for UnsafeOptionalAttributeAccess {
impl_diagnostic_from_source_loc_opt_field!(source_loc);
fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
Some(Box::new(format!(
"try testing for the attribute's presence with `{} && ..`",
self.attribute_access.suggested_has_guard()
)))
}
}
#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
#[error(
"for policy `{policy_id}`, unable to guarantee safety of access to tag `{tag}`{}",
match .entity_ty.as_ref().and_then(|lub| lub.get_single_entity()) {
Some(ety) => format!(" on entity type `{ety}`"),
None => "".to_string()
}
)]
pub struct UnsafeTagAccess {
pub source_loc: Option<Loc>,
pub policy_id: PolicyID,
pub entity_ty: Option<EntityLUB>,
pub tag: Expr<Option<Type>>,
}
impl Diagnostic for UnsafeTagAccess {
impl_diagnostic_from_source_loc_opt_field!(source_loc);
fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
Some(Box::new(format!(
"try testing for the tag's presence with `.hasTag({}) && ..`",
&self.tag
)))
}
}
#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
#[error(
"for policy `{policy_id}`, `.getTag()` is not allowed on entities of {} because no `tags` were declared on the entity type in the schema",
match .entity_ty.as_ref() {
Some(ty) => format!("type `{ty}`"),
None => "this type".to_string(),
}
)]
pub struct NoTagsAllowed {
pub source_loc: Option<Loc>,
pub policy_id: PolicyID,
pub entity_ty: Option<EntityType>,
}
impl Diagnostic for NoTagsAllowed {
impl_diagnostic_from_source_loc_opt_field!(source_loc);
}
#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
#[error("for policy `{policy_id}`, undefined extension function: {name}")]
pub struct UndefinedFunction {
pub source_loc: Option<Loc>,
pub policy_id: PolicyID,
pub name: String,
}
impl Diagnostic for UndefinedFunction {
impl_diagnostic_from_source_loc_opt_field!(source_loc);
}
#[derive(Error, Debug, Clone, Hash, PartialEq, Eq)]
#[error("for policy `{policy_id}`, wrong number of arguments in extension function application. Expected {expected}, got {actual}")]
pub struct WrongNumberArguments {
pub source_loc: Option<Loc>,
pub policy_id: PolicyID,
pub expected: usize,
pub actual: usize,
}
impl Diagnostic for WrongNumberArguments {
impl_diagnostic_from_source_loc_opt_field!(source_loc);
}
#[derive(Debug, Clone, Hash, Eq, PartialEq, Error)]
#[error("for policy `{policy_id}`, error during extension function argument validation: {msg}")]
pub struct FunctionArgumentValidation {
pub source_loc: Option<Loc>,
pub policy_id: PolicyID,
pub msg: String,
}
impl Diagnostic for FunctionArgumentValidation {
impl_diagnostic_from_source_loc_opt_field!(source_loc);
}
#[derive(Debug, Clone, Hash, Eq, PartialEq, Error)]
#[error("for policy `{policy_id}`, operands to `in` do not respect the entity hierarchy")]
pub struct HierarchyNotRespected {
pub source_loc: Option<Loc>,
pub policy_id: PolicyID,
pub in_lhs: Option<EntityType>,
pub in_rhs: Option<EntityType>,
}
impl Diagnostic for HierarchyNotRespected {
impl_diagnostic_from_source_loc_opt_field!(source_loc);
fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
match (&self.in_lhs, &self.in_rhs) {
(Some(in_lhs), Some(in_rhs)) => Some(Box::new(format!(
"`{in_lhs}` cannot be a descendant of `{in_rhs}`"
))),
_ => None,
}
}
}
#[derive(Default, Debug, Clone, Hash, Eq, PartialEq, Error, Copy, Ord, PartialOrd)]
pub struct EntityDerefLevel {
pub level: i64,
}
impl Display for EntityDerefLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
write!(f, "{}", self.level)
}
}
impl From<u32> for EntityDerefLevel {
fn from(value: u32) -> Self {
EntityDerefLevel {
level: value as i64,
}
}
}
impl Add for EntityDerefLevel {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
EntityDerefLevel {
level: self.level + rhs.level,
}
}
}
impl Neg for EntityDerefLevel {
type Output = Self;
fn neg(self) -> Self::Output {
EntityDerefLevel { level: -self.level }
}
}
impl EntityDerefLevel {
pub fn decrement(&self) -> Self {
EntityDerefLevel {
level: self.level - 1,
}
}
}
#[derive(Debug, Clone, Hash, Eq, PartialEq, Error)]
#[error("for policy `{policy_id}`, the maximum allowed level {allowed_level} is violated. Actual level is {}", (allowed_level.add(actual_level.neg())))]
pub struct EntityDerefLevelViolation {
pub source_loc: Option<Loc>,
pub policy_id: PolicyID,
pub allowed_level: EntityDerefLevel,
pub actual_level: EntityDerefLevel,
}
impl Diagnostic for EntityDerefLevelViolation {
impl_diagnostic_from_source_loc_opt_field!(source_loc);
fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
Some(Box::new("Consider increasing the level"))
}
}
#[derive(Debug, Clone, Hash, Eq, PartialEq, Error)]
#[error("for policy `{policy_id}`, empty set literals are forbidden in policies")]
pub struct EmptySetForbidden {
pub source_loc: Option<Loc>,
pub policy_id: PolicyID,
}
impl Diagnostic for EmptySetForbidden {
impl_diagnostic_from_source_loc_opt_field!(source_loc);
}
#[derive(Debug, Clone, Hash, Eq, PartialEq, Error)]
#[error("for policy `{policy_id}`, extension constructors may not be called with non-literal expressions")]
pub struct NonLitExtConstructor {
pub source_loc: Option<Loc>,
pub policy_id: PolicyID,
}
impl Diagnostic for NonLitExtConstructor {
impl_diagnostic_from_source_loc_opt_field!(source_loc);
fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
Some(Box::new(
"consider applying extension constructors inside attribute values when constructing entity or context data"
))
}
}
#[derive(Debug, Clone, Hash, Eq, PartialEq, Error)]
#[error("internal invariant violated")]
pub struct InternalInvariantViolation {
pub source_loc: Option<Loc>,
pub policy_id: PolicyID,
}
impl Diagnostic for InternalInvariantViolation {
impl_diagnostic_from_source_loc_opt_field!(source_loc);
fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
Some(Box::new(
"please file an issue at <https://github.com/cedar-policy/cedar/issues> including the schema and policy for which you observed the issue"
))
}
}
#[derive(Debug, Clone, Hash, Eq, PartialEq)]
pub enum AttributeAccess {
EntityLUB(EntityLUB, Vec<SmolStr>),
Context(EntityUID, Vec<SmolStr>),
Other(Vec<SmolStr>),
}
impl AttributeAccess {
pub(crate) fn from_expr(
req_env: &RequestEnv<'_>,
mut expr: &Expr<Option<Type>>,
attr: SmolStr,
) -> AttributeAccess {
let mut attrs: Vec<SmolStr> = vec![attr];
loop {
if let Some(Type::EntityOrRecord(EntityRecordKind::Entity(lub))) = expr.data() {
return AttributeAccess::EntityLUB(lub.clone(), attrs);
} else if let ExprKind::Var(Var::Context) = expr.expr_kind() {
return match req_env.action_entity_uid() {
Some(action) => AttributeAccess::Context(action.clone(), attrs),
None => AttributeAccess::Other(attrs),
};
} else if let ExprKind::GetAttr {
expr: sub_expr,
attr,
} = expr.expr_kind()
{
expr = sub_expr;
attrs.push(attr.clone());
} else {
return AttributeAccess::Other(attrs);
}
}
}
pub(crate) fn attrs(&self) -> &Vec<SmolStr> {
match self {
AttributeAccess::EntityLUB(_, attrs) => attrs,
AttributeAccess::Context(_, attrs) => attrs,
AttributeAccess::Other(attrs) => attrs,
}
}
pub(crate) fn suggested_has_guard(&self) -> String {
let base_expr = match self {
AttributeAccess::Context(_, _) => "context".into(),
_ => "e".into(),
};
let (safe_attrs, err_attr) = match self.attrs().split_first() {
Some((first, rest)) => (rest, first.clone()),
None => (&[] as &[SmolStr], "f".into()),
};
let full_expr = std::iter::once(&base_expr)
.chain(safe_attrs.iter().rev())
.join(".");
format!("{full_expr} has {err_attr}")
}
}
impl Display for AttributeAccess {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let attrs_str = self.attrs().iter().rev().join(".");
match self {
AttributeAccess::EntityLUB(lub, _) => write!(
f,
"`{attrs_str}` on entity type{}",
match lub.get_single_entity() {
Some(single) => format!(" `{}`", single),
_ => format!("s {}", lub.iter().map(|ety| format!("`{ety}`")).join(", ")),
},
),
AttributeAccess::Context(action, _) => {
write!(f, "`{attrs_str}` in context for {action}",)
}
AttributeAccess::Other(_) => write!(f, "`{attrs_str}`"),
}
}
}
#[cfg(test)]
mod test_attr_access {
use cedar_policy_core::ast::{EntityUID, Expr, ExprBuilder, ExprKind, Var};
use super::AttributeAccess;
use crate::types::{OpenTag, RequestEnv, Type};
#[allow(clippy::panic)]
#[track_caller]
fn assert_message_and_help(
attr_access: &Expr<Option<Type>>,
msg: impl AsRef<str>,
help: impl AsRef<str>,
) {
let env = RequestEnv::DeclaredAction {
principal: &"Principal".parse().unwrap(),
action: &EntityUID::with_eid_and_type(
cedar_policy_core::ast::ACTION_ENTITY_TYPE,
"action",
)
.unwrap(),
resource: &"Resource".parse().unwrap(),
context: &Type::record_with_attributes(None, OpenTag::ClosedAttributes),
principal_slot: None,
resource_slot: None,
};
let ExprKind::GetAttr { expr, attr } = attr_access.expr_kind() else {
panic!("Can only test `AttributeAccess::from_expr` for `GetAttr` expressions");
};
let access = AttributeAccess::from_expr(&env, expr, attr.clone());
assert_eq!(
access.to_string().as_str(),
msg.as_ref(),
"Error message did not match expected"
);
assert_eq!(
access.suggested_has_guard().as_str(),
help.as_ref(),
"Suggested has guard did not match expected"
);
}
#[test]
fn context_access() {
let e = ExprBuilder::new().get_attr(ExprBuilder::new().var(Var::Context), "foo".into());
assert_message_and_help(
&e,
"`foo` in context for Action::\"action\"",
"context has foo",
);
let e = ExprBuilder::new().get_attr(e, "bar".into());
assert_message_and_help(
&e,
"`foo.bar` in context for Action::\"action\"",
"context.foo has bar",
);
let e = ExprBuilder::new().get_attr(e, "baz".into());
assert_message_and_help(
&e,
"`foo.bar.baz` in context for Action::\"action\"",
"context.foo.bar has baz",
);
}
#[test]
fn entity_access() {
let e = ExprBuilder::new().get_attr(
ExprBuilder::with_data(Some(Type::named_entity_reference_from_str("User")))
.val("User::\"alice\"".parse::<EntityUID>().unwrap()),
"foo".into(),
);
assert_message_and_help(&e, "`foo` on entity type `User`", "e has foo");
let e = ExprBuilder::new().get_attr(e, "bar".into());
assert_message_and_help(&e, "`foo.bar` on entity type `User`", "e.foo has bar");
let e = ExprBuilder::new().get_attr(e, "baz".into());
assert_message_and_help(
&e,
"`foo.bar.baz` on entity type `User`",
"e.foo.bar has baz",
);
}
#[test]
fn entity_type_attr_access() {
let e = ExprBuilder::with_data(Some(Type::named_entity_reference_from_str("Thing")))
.get_attr(
ExprBuilder::with_data(Some(Type::named_entity_reference_from_str("User")))
.var(Var::Principal),
"thing".into(),
);
assert_message_and_help(&e, "`thing` on entity type `User`", "e has thing");
let e = ExprBuilder::new().get_attr(e, "bar".into());
assert_message_and_help(&e, "`bar` on entity type `Thing`", "e has bar");
let e = ExprBuilder::new().get_attr(e, "baz".into());
assert_message_and_help(&e, "`bar.baz` on entity type `Thing`", "e.bar has baz");
}
#[test]
fn other_access() {
let e = ExprBuilder::new().get_attr(
ExprBuilder::new().ite(
ExprBuilder::new().val(true),
ExprBuilder::new().record([]).unwrap(),
ExprBuilder::new().record([]).unwrap(),
),
"foo".into(),
);
assert_message_and_help(&e, "`foo`", "e has foo");
let e = ExprBuilder::new().get_attr(e, "bar".into());
assert_message_and_help(&e, "`foo.bar`", "e.foo has bar");
let e = ExprBuilder::new().get_attr(e, "baz".into());
assert_message_and_help(&e, "`foo.bar.baz`", "e.foo.bar has baz");
}
}