From 5cfb918f88902a32b3f4c24bc913b578b7c12996 Mon Sep 17 00:00:00 2001 From: Craig Disselkoen Date: Thu, 12 Dec 2024 14:32:00 -0500 Subject: [PATCH 01/16] introduce `BoundedDisplay` trait and properly bound error message (#1373) Signed-off-by: Craig Disselkoen --- cedar-policy-core/src/ast/expr.rs | 8 + cedar-policy-core/src/ast/partial_value.rs | 11 +- cedar-policy-core/src/ast/request.rs | 10 +- cedar-policy-core/src/ast/value.rs | 110 +++++-- cedar-policy-core/src/est/expr.rs | 333 ++++++++++++++++----- cedar-policy-validator/src/coreschema.rs | 43 +-- 6 files changed, 366 insertions(+), 149 deletions(-) diff --git a/cedar-policy-core/src/ast/expr.rs b/cedar-policy-core/src/ast/expr.rs index 9a757b01c..75e1e8b5c 100644 --- a/cedar-policy-core/src/ast/expr.rs +++ b/cedar-policy-core/src/ast/expr.rs @@ -770,6 +770,14 @@ impl std::fmt::Display for Expr { } } +impl BoundedDisplay for Expr { + fn fmt(&self, f: &mut impl std::fmt::Write, n: Option) -> std::fmt::Result { + // Like the `std::fmt::Display` impl, we convert to EST and use the EST + // pretty-printer. Note that converting AST->EST is lossless and infallible. + BoundedDisplay::fmt(&crate::est::Expr::from(self.clone()), f, n) + } +} + impl std::str::FromStr for Expr { type Err = ParseErrors; diff --git a/cedar-policy-core/src/ast/partial_value.rs b/cedar-policy-core/src/ast/partial_value.rs index 484e75235..d1783298f 100644 --- a/cedar-policy-core/src/ast/partial_value.rs +++ b/cedar-policy-core/src/ast/partial_value.rs @@ -14,7 +14,7 @@ * limitations under the License. */ -use super::{Expr, Unknown, Value}; +use super::{BoundedDisplay, Expr, Unknown, Value}; use crate::parser::Loc; use itertools::Either; use miette::Diagnostic; @@ -99,6 +99,15 @@ impl std::fmt::Display for PartialValue { } } +impl BoundedDisplay for PartialValue { + fn fmt(&self, f: &mut impl std::fmt::Write, n: Option) -> std::fmt::Result { + match self { + PartialValue::Value(v) => BoundedDisplay::fmt(v, f, n), + PartialValue::Residual(r) => BoundedDisplay::fmt(r, f, n), + } + } +} + /// Collect an iterator of either residuals or values into one of the following /// a) An iterator over values, if everything evaluated to values /// b) An iterator over residuals expressions, if anything only evaluated to a residual diff --git a/cedar-policy-core/src/ast/request.rs b/cedar-policy-core/src/ast/request.rs index f58691d71..76c2ee5a1 100644 --- a/cedar-policy-core/src/ast/request.rs +++ b/cedar-policy-core/src/ast/request.rs @@ -31,8 +31,8 @@ use thiserror::Error; use crate::ast::proto; use super::{ - BorrowedRestrictedExpr, EntityType, EntityUID, Expr, ExprKind, ExpressionConstructionError, - PartialValue, RestrictedExpr, Unknown, Value, ValueKind, Var, + BorrowedRestrictedExpr, BoundedDisplay, EntityType, EntityUID, Expr, ExprKind, + ExpressionConstructionError, PartialValue, RestrictedExpr, Unknown, Value, ValueKind, Var, }; /// Represents the request tuple (see the Cedar design doc). @@ -566,6 +566,12 @@ impl std::fmt::Display for Context { } } +impl BoundedDisplay for Context { + fn fmt(&self, f: &mut impl std::fmt::Write, n: Option) -> std::fmt::Result { + BoundedDisplay::fmt(&PartialValue::from(self.clone()), f, n) + } +} + #[cfg(feature = "protobufs")] impl From<&proto::Context> for Context { fn from(v: &proto::Context) -> Self { diff --git a/cedar-policy-core/src/ast/value.rs b/cedar-policy-core/src/ast/value.rs index a07f9e1c0..825f41f5f 100644 --- a/cedar-policy-core/src/ast/value.rs +++ b/cedar-policy-core/src/ast/value.rs @@ -148,18 +148,11 @@ impl Value { pub fn eq_and_same_source_loc(&self, other: &Self) -> bool { self == other && self.source_loc() == other.source_loc() } +} - /// Alternate `Display` impl, that truncates large sets/records (including recursively). - /// - /// `n`: the maximum number of set elements, or record key-value pairs, that - /// will be shown before eliding the rest with `..`. - /// `None` means no bound. - pub fn bounded_display( - &self, - f: &mut std::fmt::Formatter<'_>, - n: Option, - ) -> std::fmt::Result { - self.value.bounded_display(f, n) +impl BoundedDisplay for Value { + fn fmt(&self, f: &mut impl std::fmt::Write, n: Option) -> std::fmt::Result { + BoundedDisplay::fmt(&self.value, f, n) } } @@ -208,17 +201,10 @@ impl ValueKind { _ => None, } } +} - /// Alternate `Display` impl, that truncates large sets/records (including recursively). - /// - /// `n`: the maximum number of set elements, or record key-value pairs, that - /// will be shown before eliding the rest with `..`. - /// `None` means no bound. - pub fn bounded_display( - &self, - f: &mut std::fmt::Formatter<'_>, - n: Option, - ) -> std::fmt::Result { +impl BoundedDisplay for ValueKind { + fn fmt(&self, f: &mut impl std::fmt::Write, n: Option) -> std::fmt::Result { match self { Self::Lit(lit) => write!(f, "{lit}"), Self::Set(Set { @@ -253,7 +239,7 @@ impl ValueKind { as Box>, }; for (i, item) in elements.enumerate() { - item.bounded_display(f, n)?; + BoundedDisplay::fmt(item, f, n)?; if i < authoritative.len() - 1 { write!(f, ", ")?; } @@ -287,7 +273,7 @@ impl ValueKind { write!(f, "\"{k}\": ")?; } } - v.bounded_display(f, n)?; + BoundedDisplay::fmt(v, f, n)?; if i < record.len() - 1 { write!(f, ", ")?; } @@ -556,6 +542,76 @@ impl StaticallyTyped for ValueKind { } } +/// Like `Display`, but optionally truncates embedded sets/records to `n` +/// elements/pairs, including recursively. +/// +/// `n`: the maximum number of set elements, or record key-value pairs, that +/// will be shown before eliding the rest with `..`. +/// `None` means no bound. +/// +/// Intended for error messages, to avoid very large/long error messages. +pub trait BoundedDisplay { + /// Write `self` to the writer `f`, truncating set elements or key-value + /// pairs if necessary based on `n` + fn fmt(&self, f: &mut impl std::fmt::Write, n: Option) -> std::fmt::Result; + + /// Convenience method, equivalent to `fmt()` with `n` as `Some`. + /// + /// You should generally not re-implement this, just use the default implementation. + fn fmt_bounded(&self, f: &mut impl std::fmt::Write, n: usize) -> std::fmt::Result { + self.fmt(f, Some(n)) + } + + /// Convenience method, equivalent to `fmt()` with `n` as `None`. + /// + /// You should generally not re-implement this, just use the default implementation. + fn fmt_unbounded(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result { + self.fmt(f, None) + } +} + +/// Like `ToString`, but optionally truncates embedded sets/records to `n` +/// elements/pairs, including recursively. +/// +/// `n`: the maximum number of set elements, or record key-value pairs, that +/// will be shown before eliding the rest with `..`. +/// `None` means no bound. +/// +/// Intended for error messages, to avoid very large/long error messages. +pub trait BoundedToString { + /// Convert `self` to a `String`, truncating set elements or key-value pairs + /// if necessary based on `n` + fn to_string(&self, n: Option) -> String; + + /// Convenience method, equivalent to `to_string()` with `n` as `Some`. + /// + /// You should generally not re-implement this, just use the default implementation. + fn to_string_bounded(&self, n: usize) -> String { + self.to_string(Some(n)) + } + + /// Convenience method, equivalent to `to_string()` with `n` as `None`. + /// + /// You should generally not re-implement this, just use the default implementation. + fn to_string_unbounded(&self) -> String { + self.to_string(None) + } +} + +/// Like the impl of `ToString` for `T: Display` in the standard library, +/// this impl of `BoundedToString` for `T: BoundedDisplay` panics if the `BoundedDisplay` +/// implementation returns an error, which would indicate an incorrect `BoundedDisplay` +/// implementation since `fmt::Write`-ing to a `String` never returns an error. +impl BoundedToString for T { + fn to_string(&self, n: Option) -> String { + let mut s = String::new(); + // PANIC SAFETY: `std::fmt::Write` does not return errors when writing to a `String` + #[allow(clippy::expect_used)] + BoundedDisplay::fmt(self, &mut s, n).expect("a `BoundedDisplay` implementation returned an error when writing to a `String`, which shouldn't happen"); + s + } +} + impl std::fmt::Display for Value { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.value) @@ -564,7 +620,7 @@ impl std::fmt::Display for Value { impl std::fmt::Display for ValueKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.bounded_display(f, None) + BoundedDisplay::fmt_unbounded(self, f) } } @@ -758,9 +814,9 @@ mod test { #[test] fn pretty_printer() { - assert_eq!(Value::from("abc").to_string(), r#""abc""#); - assert_eq!(Value::from("\t").to_string(), r#""\t""#); - assert_eq!(Value::from("๐Ÿˆ").to_string(), r#""๐Ÿˆ""#); + assert_eq!(ToString::to_string(&Value::from("abc")), r#""abc""#); + assert_eq!(ToString::to_string(&Value::from("\t")), r#""\t""#); + assert_eq!(ToString::to_string(&Value::from("๐Ÿˆ")), r#""๐Ÿˆ""#); } #[test] diff --git a/cedar-policy-core/src/est/expr.rs b/cedar-policy-core/src/est/expr.rs index e6bbe5447..49edc729c 100644 --- a/cedar-policy-core/src/est/expr.rs +++ b/cedar-policy-core/src/est/expr.rs @@ -15,7 +15,7 @@ */ use super::FromJsonError; -use crate::ast::{self, EntityUID, InputInteger}; +use crate::ast::{self, BoundedDisplay, EntityUID, InputInteger}; use crate::entities::json::{ err::EscapeKind, err::JsonDeserializationError, err::JsonDeserializationErrorContext, CedarValueJson, FnAndArg, TypeAndId, @@ -384,7 +384,7 @@ pub enum ExprNoExt { Record( #[serde_as(as = "serde_with::MapPreventDuplicates<_,_>")] #[cfg_attr(feature = "wasm", tsify(type = "Record"))] - HashMap, + BTreeMap, ), } @@ -641,7 +641,7 @@ impl Expr { } /// e.g. {foo: 1+2, bar: !(context has department)} - pub fn record(map: HashMap) -> Self { + pub fn record(map: BTreeMap) -> Self { Expr::ExprNoExt(ExprNoExt::Record(map)) } @@ -798,7 +798,7 @@ impl Expr { Ok(Expr::ExprNoExt(ExprNoExt::Set(new_v))) } ExprNoExt::Record(m) => { - let mut new_m = HashMap::new(); + let mut new_m = BTreeMap::new(); for (k, v) in m { new_m.insert(k, v.sub_entity_literals(mapping)?); } @@ -1447,7 +1447,7 @@ fn interpret_primary( let s = k.to_expr_or_special().and_then(|es| es.into_valid_attr())?; Ok((s, v.try_into()?)) }) - .collect::, ParseErrors>>() + .collect::, ParseErrors>>() .map(Expr::record) .map(Either::Right), } @@ -1677,12 +1677,25 @@ impl std::fmt::Display for Expr { } } -fn display_cedarvaluejson(f: &mut std::fmt::Formatter<'_>, v: &CedarValueJson) -> std::fmt::Result { +impl BoundedDisplay for Expr { + fn fmt(&self, f: &mut impl std::fmt::Write, n: Option) -> std::fmt::Result { + match self { + Self::ExprNoExt(e) => BoundedDisplay::fmt(e, f, n), + Self::ExtFuncCall(e) => BoundedDisplay::fmt(e, f, n), + } + } +} + +fn display_cedarvaluejson( + f: &mut impl std::fmt::Write, + v: &CedarValueJson, + n: Option, +) -> std::fmt::Result { match v { // Add parentheses around negative numeric literals otherwise // round-tripping fuzzer fails for expressions like `(-1)["a"]`. - CedarValueJson::Long(n) if *n < 0 => write!(f, "({n})"), - CedarValueJson::Long(n) => write!(f, "{n}"), + CedarValueJson::Long(i) if *i < 0 => write!(f, "({i})"), + CedarValueJson::Long(i) => write!(f, "{i}"), CedarValueJson::Bool(b) => write!(f, "{b}"), CedarValueJson::String(s) => write!(f, "\"{}\"", s.escape_debug()), CedarValueJson::EntityEscape { __entity } => { @@ -1705,40 +1718,71 @@ fn display_cedarvaluejson(f: &mut std::fmt::Formatter<'_>, v: &CedarValueJson) - }); match style { Some(ast::CallStyle::MethodStyle) => { - display_cedarvaluejson(f, arg)?; + display_cedarvaluejson(f, arg, n)?; write!(f, ".{ext_fn}()")?; Ok(()) } Some(ast::CallStyle::FunctionStyle) | None => { write!(f, "{ext_fn}(")?; - display_cedarvaluejson(f, arg)?; + display_cedarvaluejson(f, arg, n)?; write!(f, ")")?; Ok(()) } } } CedarValueJson::Set(v) => { - write!(f, "[")?; - for (i, val) in v.iter().enumerate() { - display_cedarvaluejson(f, val)?; - if i < (v.len() - 1) { - write!(f, ", ")?; + match n { + Some(n) if v.len() > n => { + // truncate to n elements + write!(f, "[")?; + for val in v.iter().take(n) { + display_cedarvaluejson(f, val, Some(n))?; + write!(f, ", ")?; + } + write!(f, "..]")?; + Ok(()) + } + _ => { + // no truncation + write!(f, "[")?; + for (i, val) in v.iter().enumerate() { + display_cedarvaluejson(f, val, n)?; + if i < v.len() - 1 { + write!(f, ", ")?; + } + } + write!(f, "]")?; + Ok(()) } } - write!(f, "]")?; - Ok(()) } - CedarValueJson::Record(m) => { - write!(f, "{{")?; - for (i, (k, v)) in m.iter().enumerate() { - write!(f, "\"{}\": ", k.escape_debug())?; - display_cedarvaluejson(f, v)?; - if i < (m.len() - 1) { - write!(f, ", ")?; + CedarValueJson::Record(r) => { + match n { + Some(n) if r.len() > n => { + // truncate to n key-value pairs + write!(f, "{{")?; + for (k, v) in r.iter().take(n) { + write!(f, "\"{}\": ", k.escape_debug())?; + display_cedarvaluejson(f, v, Some(n))?; + write!(f, ", ")?; + } + write!(f, "..}}")?; + Ok(()) + } + _ => { + // no truncation + write!(f, "{{")?; + for (i, (k, v)) in r.iter().enumerate() { + write!(f, "\"{}\": ", k.escape_debug())?; + display_cedarvaluejson(f, v, n)?; + if i < r.len() - 1 { + write!(f, ", ")?; + } + } + write!(f, "}}")?; + Ok(()) } } - write!(f, "}}")?; - Ok(()) } CedarValueJson::Null => { write!(f, "null")?; @@ -1749,13 +1793,19 @@ fn display_cedarvaluejson(f: &mut std::fmt::Formatter<'_>, v: &CedarValueJson) - impl std::fmt::Display for ExprNoExt { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + BoundedDisplay::fmt_unbounded(self, f) + } +} + +impl BoundedDisplay for ExprNoExt { + fn fmt(&self, f: &mut impl std::fmt::Write, n: Option) -> std::fmt::Result { match &self { - ExprNoExt::Value(v) => display_cedarvaluejson(f, v), + ExprNoExt::Value(v) => display_cedarvaluejson(f, v, n), ExprNoExt::Var(v) => write!(f, "{v}"), ExprNoExt::Slot(id) => write!(f, "{id}"), ExprNoExt::Not { arg } => { write!(f, "!")?; - maybe_with_parens(f, arg) + maybe_with_parens(f, arg, n) } ExprNoExt::Neg { arg } => { // Always add parentheses instead of calling @@ -1766,99 +1816,99 @@ impl std::fmt::Display for ExprNoExt { write!(f, "-({arg})") } ExprNoExt::Eq { left, right } => { - maybe_with_parens(f, left)?; + maybe_with_parens(f, left, n)?; write!(f, " == ")?; - maybe_with_parens(f, right) + maybe_with_parens(f, right, n) } ExprNoExt::NotEq { left, right } => { - maybe_with_parens(f, left)?; + maybe_with_parens(f, left, n)?; write!(f, " != ")?; - maybe_with_parens(f, right) + maybe_with_parens(f, right, n) } ExprNoExt::In { left, right } => { - maybe_with_parens(f, left)?; + maybe_with_parens(f, left, n)?; write!(f, " in ")?; - maybe_with_parens(f, right) + maybe_with_parens(f, right, n) } ExprNoExt::Less { left, right } => { - maybe_with_parens(f, left)?; + maybe_with_parens(f, left, n)?; write!(f, " < ")?; - maybe_with_parens(f, right) + maybe_with_parens(f, right, n) } ExprNoExt::LessEq { left, right } => { - maybe_with_parens(f, left)?; + maybe_with_parens(f, left, n)?; write!(f, " <= ")?; - maybe_with_parens(f, right) + maybe_with_parens(f, right, n) } ExprNoExt::Greater { left, right } => { - maybe_with_parens(f, left)?; + maybe_with_parens(f, left, n)?; write!(f, " > ")?; - maybe_with_parens(f, right) + maybe_with_parens(f, right, n) } ExprNoExt::GreaterEq { left, right } => { - maybe_with_parens(f, left)?; + maybe_with_parens(f, left, n)?; write!(f, " >= ")?; - maybe_with_parens(f, right) + maybe_with_parens(f, right, n) } ExprNoExt::And { left, right } => { - maybe_with_parens(f, left)?; + maybe_with_parens(f, left, n)?; write!(f, " && ")?; - maybe_with_parens(f, right) + maybe_with_parens(f, right, n) } ExprNoExt::Or { left, right } => { - maybe_with_parens(f, left)?; + maybe_with_parens(f, left, n)?; write!(f, " || ")?; - maybe_with_parens(f, right) + maybe_with_parens(f, right, n) } ExprNoExt::Add { left, right } => { - maybe_with_parens(f, left)?; + maybe_with_parens(f, left, n)?; write!(f, " + ")?; - maybe_with_parens(f, right) + maybe_with_parens(f, right, n) } ExprNoExt::Sub { left, right } => { - maybe_with_parens(f, left)?; + maybe_with_parens(f, left, n)?; write!(f, " - ")?; - maybe_with_parens(f, right) + maybe_with_parens(f, right, n) } ExprNoExt::Mul { left, right } => { - maybe_with_parens(f, left)?; + maybe_with_parens(f, left, n)?; write!(f, " * ")?; - maybe_with_parens(f, right) + maybe_with_parens(f, right, n) } ExprNoExt::Contains { left, right } => { - maybe_with_parens(f, left)?; + maybe_with_parens(f, left, n)?; write!(f, ".contains({right})") } ExprNoExt::ContainsAll { left, right } => { - maybe_with_parens(f, left)?; + maybe_with_parens(f, left, n)?; write!(f, ".containsAll({right})") } ExprNoExt::ContainsAny { left, right } => { - maybe_with_parens(f, left)?; + maybe_with_parens(f, left, n)?; write!(f, ".containsAny({right})") } ExprNoExt::IsEmpty { arg } => { - maybe_with_parens(f, arg)?; + maybe_with_parens(f, arg, n)?; write!(f, ".isEmpty()") } ExprNoExt::GetTag { left, right } => { - maybe_with_parens(f, left)?; + maybe_with_parens(f, left, n)?; write!(f, ".getTag({right})") } ExprNoExt::HasTag { left, right } => { - maybe_with_parens(f, left)?; + maybe_with_parens(f, left, n)?; write!(f, ".hasTag({right})") } ExprNoExt::GetAttr { left, attr } => { - maybe_with_parens(f, left)?; + maybe_with_parens(f, left, n)?; write!(f, "[\"{}\"]", attr.escape_debug()) } ExprNoExt::HasAttr { left, attr } => { - maybe_with_parens(f, left)?; + maybe_with_parens(f, left, n)?; write!(f, " has \"{}\"", attr.escape_debug()) } ExprNoExt::Like { left, pattern } => { - maybe_with_parens(f, left)?; + maybe_with_parens(f, left, n)?; write!( f, " like \"{}\"", @@ -1870,12 +1920,12 @@ impl std::fmt::Display for ExprNoExt { entity_type, in_expr, } => { - maybe_with_parens(f, left)?; + maybe_with_parens(f, left, n)?; write!(f, " is {entity_type}")?; match in_expr { Some(in_expr) => { write!(f, " in ")?; - maybe_with_parens(f, in_expr) + maybe_with_parens(f, in_expr, n) } None => Ok(()), } @@ -1886,26 +1936,78 @@ impl std::fmt::Display for ExprNoExt { else_expr, } => { write!(f, "if ")?; - maybe_with_parens(f, cond_expr)?; + maybe_with_parens(f, cond_expr, n)?; write!(f, " then ")?; - maybe_with_parens(f, then_expr)?; + maybe_with_parens(f, then_expr, n)?; write!(f, " else ")?; - maybe_with_parens(f, else_expr) + maybe_with_parens(f, else_expr, n) + } + ExprNoExt::Set(v) => { + match n { + Some(n) if v.len() > n => { + // truncate to n elements + write!(f, "[")?; + for element in v.iter().take(n) { + BoundedDisplay::fmt(element, f, Some(n))?; + write!(f, ", ")?; + } + write!(f, "..]")?; + Ok(()) + } + _ => { + // no truncation + write!(f, "[")?; + for (i, element) in v.iter().enumerate() { + BoundedDisplay::fmt(element, f, n)?; + if i < v.len() - 1 { + write!(f, ", ")?; + } + } + write!(f, "]")?; + Ok(()) + } + } + } + ExprNoExt::Record(m) => { + match n { + Some(n) if m.len() > n => { + // truncate to n key-value pairs + write!(f, "{{")?; + for (k, v) in m.iter().take(n) { + write!(f, "\"{}\": ", k.escape_debug())?; + BoundedDisplay::fmt(v, f, Some(n))?; + write!(f, ", ")?; + } + write!(f, "..}}")?; + Ok(()) + } + _ => { + // no truncation + write!(f, "{{")?; + for (i, (k, v)) in m.iter().enumerate() { + write!(f, "\"{}\": ", k.escape_debug())?; + BoundedDisplay::fmt(v, f, n)?; + if i < m.len() - 1 { + write!(f, ", ")?; + } + } + write!(f, "}}")?; + Ok(()) + } + } } - ExprNoExt::Set(v) => write!(f, "[{}]", v.iter().join(", ")), - ExprNoExt::Record(m) => write!( - f, - "{{{}}}", - m.iter() - .map(|(k, v)| format!("\"{}\": {}", k.escape_debug(), v)) - .join(", ") - ), } } } impl std::fmt::Display for ExtFuncCall { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + BoundedDisplay::fmt_unbounded(self, f) + } +} + +impl BoundedDisplay for ExtFuncCall { + fn fmt(&self, f: &mut impl std::fmt::Write, n: Option) -> std::fmt::Result { // PANIC SAFETY: safe due to INVARIANT on `ExtFuncCall` #[allow(clippy::unreachable)] let Some((fn_name, args)) = self.call.iter().next() else { @@ -1921,7 +2023,7 @@ impl std::fmt::Display for ExtFuncCall { }); match (style, args.iter().next()) { (Some(ast::CallStyle::MethodStyle), Some(receiver)) => { - maybe_with_parens(f, receiver)?; + maybe_with_parens(f, receiver, n)?; write!(f, ".{}({})", fn_name, args.iter().skip(1).join(", ")) } (_, _) => { @@ -1931,18 +2033,22 @@ impl std::fmt::Display for ExtFuncCall { } } -/// returns the `Display` representation of the Expr, adding parens around +/// returns the `BoundedDisplay` representation of the Expr, adding parens around /// the entire string if necessary. /// E.g., won't add parens for constants or `principal` etc, but will for things /// like `(2 < 5)`. /// When in doubt, add the parens. -fn maybe_with_parens(f: &mut std::fmt::Formatter<'_>, expr: &Expr) -> std::fmt::Result { +fn maybe_with_parens( + f: &mut impl std::fmt::Write, + expr: &Expr, + n: Option, +) -> std::fmt::Result { match expr { Expr::ExprNoExt(ExprNoExt::Set(_)) | Expr::ExprNoExt(ExprNoExt::Record(_)) | Expr::ExprNoExt(ExprNoExt::Value(_)) | Expr::ExprNoExt(ExprNoExt::Var(_)) | - Expr::ExprNoExt(ExprNoExt::Slot(_)) => write!(f, "{expr}"), + Expr::ExprNoExt(ExprNoExt::Slot(_)) => BoundedDisplay::fmt(expr, f, n), // we want parens here because things like parse((!x).y) // would be printed into !x.y which has a different meaning @@ -1973,7 +2079,12 @@ fn maybe_with_parens(f: &mut std::fmt::Formatter<'_>, expr: &Expr) -> std::fmt:: Expr::ExprNoExt(ExprNoExt::Like { .. }) | Expr::ExprNoExt(ExprNoExt::Is { .. }) | Expr::ExprNoExt(ExprNoExt::If { .. }) | - Expr::ExtFuncCall { .. } => write!(f, "({expr})"), + Expr::ExtFuncCall { .. } => { + write!(f, "(")?; + BoundedDisplay::fmt(expr, f, n)?; + write!(f, ")")?; + Ok(()) + } } } @@ -1983,9 +2094,10 @@ fn maybe_with_parens(f: &mut std::fmt::Formatter<'_>, expr: &Expr) -> std::fmt:: // PANIC SAFETY: Unit Test Code #[allow(clippy::panic)] mod test { - use crate::parser::err::ParseError; + use crate::parser::{err::ParseError, parse_expr}; use super::*; + use ast::BoundedToString; use cool_asserts::assert_matches; #[test] @@ -2012,4 +2124,65 @@ mod test { ); }); } + + #[test] + fn display_and_bounded_display() { + let expr = Expr::from(parse_expr(r#"[100, [3, 4, 5], -20, "foo"]"#).unwrap()); + assert_eq!(format!("{expr}"), r#"[100, [3, 4, 5], (-20), "foo"]"#); + assert_eq!( + BoundedToString::to_string(&expr, None), + r#"[100, [3, 4, 5], (-20), "foo"]"# + ); + assert_eq!( + BoundedToString::to_string(&expr, Some(4)), + r#"[100, [3, 4, 5], (-20), "foo"]"# + ); + assert_eq!( + BoundedToString::to_string(&expr, Some(3)), + r#"[100, [3, 4, 5], (-20), ..]"# + ); + assert_eq!( + BoundedToString::to_string(&expr, Some(2)), + r#"[100, [3, 4, ..], ..]"# + ); + assert_eq!(BoundedToString::to_string(&expr, Some(1)), r#"[100, ..]"#); + assert_eq!(BoundedToString::to_string(&expr, Some(0)), r#"[..]"#); + + let expr = Expr::from( + parse_expr( + r#"{ + a: 12, + b: [3, 4, true], + c: -20, + "hello โˆž world": "โˆ‚ยตรŸโ‰ˆยฅ" + }"#, + ) + .unwrap(), + ); + assert_eq!( + format!("{expr}"), + r#"{"a": 12, "b": [3, 4, true], "c": (-20), "hello โˆž world": "โˆ‚ยตรŸโ‰ˆยฅ"}"# + ); + assert_eq!( + BoundedToString::to_string(&expr, None), + r#"{"a": 12, "b": [3, 4, true], "c": (-20), "hello โˆž world": "โˆ‚ยตรŸโ‰ˆยฅ"}"# + ); + assert_eq!( + BoundedToString::to_string(&expr, Some(4)), + r#"{"a": 12, "b": [3, 4, true], "c": (-20), "hello โˆž world": "โˆ‚ยตรŸโ‰ˆยฅ"}"# + ); + assert_eq!( + BoundedToString::to_string(&expr, Some(3)), + r#"{"a": 12, "b": [3, 4, true], "c": (-20), ..}"# + ); + assert_eq!( + BoundedToString::to_string(&expr, Some(2)), + r#"{"a": 12, "b": [3, 4, ..], ..}"# + ); + assert_eq!( + BoundedToString::to_string(&expr, Some(1)), + r#"{"a": 12, ..}"# + ); + assert_eq!(BoundedToString::to_string(&expr, Some(0)), r#"{..}"#); + } } diff --git a/cedar-policy-validator/src/coreschema.rs b/cedar-policy-validator/src/coreschema.rs index 673fa2211..1b00694b6 100644 --- a/cedar-policy-validator/src/coreschema.rs +++ b/cedar-policy-validator/src/coreschema.rs @@ -454,7 +454,7 @@ pub mod request_validation_errors { /// Context does not comply with the shape specified for the request action #[derive(Debug, Error, Diagnostic)] - #[error("context `{}` is not valid for `{action}`", pretty_print(&.context))] + #[error("context `{}` is not valid for `{action}`", ast::BoundedToString::to_string_bounded(.context, BOUNDEDDISPLAY_BOUND_FOR_INVALID_CONTEXT_ERROR))] pub struct InvalidContextError { /// Context which is not valid pub(super) context: ast::Context, @@ -462,6 +462,8 @@ pub mod request_validation_errors { pub(super) action: Arc, } + const BOUNDEDDISPLAY_BOUND_FOR_INVALID_CONTEXT_ERROR: usize = 5; + impl InvalidContextError { /// The context which is not valid pub fn context(&self) -> &ast::Context { @@ -473,43 +475,6 @@ pub mod request_validation_errors { &self.action } } - - const MAX_KEYS_TO_PRETTY_PRINT: usize = 5; - - fn pretty_print(context: &ast::Context) -> String { - if context.num_keys() <= MAX_KEYS_TO_PRETTY_PRINT { - // just print the context using its `Display` impl - context.to_string() - } else { - // shares a lot of code with the `Display` impl for `ValueKind`, but we need to add `, ..` just before the last `}` - let try_creating_string = || -> Result { - use std::fmt::Write; - use std::str::FromStr; - let mut s = String::new(); - write!(s, "{{")?; - for (k, v) in context.clone().into_iter().take(MAX_KEYS_TO_PRETTY_PRINT) { - match ast::UnreservedId::from_str(&k) { - Ok(k) => { - // we can omit the quotes around the key, it's a valid identifier and not a reserved keyword - write!(s, "{k}: {v}, ")?; - } - Err(_) => { - // put quotes around the key - write!(s, "\"{k}\": {v}, ")?; - } - } - } - write!(s, ".. }}")?; - Ok(s) - }; - try_creating_string().unwrap_or_else(|_| { - // failed to pretty-print just the first MAX_KEYS_TO_PRETTY_PRINT - // pairs (this should never happen), just fall back on printing - // the whole context, which is guaranteed to not fail - context.to_string() - }) - } - } } /// Struct which carries enough information that it can impl Core's @@ -1024,7 +989,7 @@ mod test { expect_err( "", &miette::Report::new(e), - &ExpectedErrorMessageBuilder::error(r#"context `{admin_approval: true, "also extra": "spam", extra1: false, extra2: [(-100)], extra3: User::"alice", .. }` is not valid for `Action::"edit_photo"`"#).build(), + &ExpectedErrorMessageBuilder::error(r#"context `{admin_approval: true, "also extra": "spam", extra1: false, extra2: [-100], extra3: User::"alice", .. }` is not valid for `Action::"edit_photo"`"#).build(), ); } ); From 0d02eb76a5712fb372b7d08a737ebf33bb008a36 Mon Sep 17 00:00:00 2001 From: John Kastner <130772734+john-h-kastner-aws@users.noreply.github.com> Date: Thu, 12 Dec 2024 16:00:07 -0500 Subject: [PATCH 02/16] Apply clippy lint suggestions (#1372) Signed-off-by: John Kastner --- cedar-policy-cli/src/main.rs | 2 +- cedar-policy-core/src/ast/policy.rs | 2 +- cedar-policy-core/src/parser/err.rs | 2 +- .../src/cedar_schema/err.rs | 6 +++--- .../src/cedar_schema/test.rs | 10 ++++----- cedar-policy-validator/src/coreschema.rs | 10 ++++----- cedar-policy-validator/src/entity_manifest.rs | 4 ++-- .../src/entity_manifest/loader.rs | 6 +++--- .../src/entity_manifest/slicing.rs | 2 +- .../src/entity_manifest/type_annotations.rs | 10 ++++----- cedar-policy-validator/src/lib.rs | 6 +++--- cedar-policy-validator/src/schema.rs | 21 +++++++++---------- .../src/typecheck/test/expr.rs | 2 +- .../src/typecheck/test/extensions.rs | 20 +++++++----------- .../src/typecheck/test/namespace.rs | 6 +++--- .../src/typecheck/test/partial.rs | 10 ++++----- .../src/typecheck/test/policy.rs | 2 +- .../src/typecheck/test/strict.rs | 2 +- .../src/typecheck/test/test_utils.rs | 2 +- cedar-policy-validator/src/types.rs | 3 +-- cedar-policy/src/api.rs | 5 +---- cedar-policy/src/ffi/utils.rs | 10 +++++++++ 22 files changed, 72 insertions(+), 71 deletions(-) diff --git a/cedar-policy-cli/src/main.rs b/cedar-policy-cli/src/main.rs index 0890bf3da..56a8ba339 100644 --- a/cedar-policy-cli/src/main.rs +++ b/cedar-policy-cli/src/main.rs @@ -75,7 +75,7 @@ mod test { let test_data_root = PathBuf::from(r"../sample-data/sandbox_b"); let mut schema_file = test_data_root.clone(); schema_file.push("schema.cedarschema"); - let mut old_policies_file = test_data_root.clone(); + let mut old_policies_file = test_data_root; old_policies_file.push("policies_4.cedar"); let new_policies_file = old_policies_file.clone(); diff --git a/cedar-policy-core/src/ast/policy.rs b/cedar-policy-core/src/ast/policy.rs index 2c735bf56..22a75ceb1 100644 --- a/cedar-policy-core/src/ast/policy.rs +++ b/cedar-policy-core/src/ast/policy.rs @@ -2246,7 +2246,7 @@ impl AsRef for PolicyID { } #[cfg(feature = "arbitrary")] -impl<'u> arbitrary::Arbitrary<'u> for PolicyID { +impl arbitrary::Arbitrary<'_> for PolicyID { fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result { let s: String = u.arbitrary()?; Ok(PolicyID::from_string(s)) diff --git a/cedar-policy-core/src/parser/err.rs b/cedar-policy-core/src/parser/err.rs index d12568a1f..49b750523 100644 --- a/cedar-policy-core/src/parser/err.rs +++ b/cedar-policy-core/src/parser/err.rs @@ -259,7 +259,7 @@ pub enum ToASTErrorKind { InvalidPattern(String), /// Returned when the right hand side of a `is` expression is not an entity type name #[error("right hand side of an `is` expression must be an entity type name, but got `{rhs}`")] - #[diagnostic(help("{}", invalid_is_help(&.lhs, &.rhs)))] + #[diagnostic(help("{}", invalid_is_help(lhs, rhs)))] InvalidIsType { /// LHS of the invalid `is` expression, as a string lhs: String, diff --git a/cedar-policy-validator/src/cedar_schema/err.rs b/cedar-policy-validator/src/cedar_schema/err.rs index 232529cbb..bbe7dcc48 100644 --- a/cedar-policy-validator/src/cedar_schema/err.rs +++ b/cedar-policy-validator/src/cedar_schema/err.rs @@ -635,7 +635,7 @@ pub mod schema_warnings { // CAUTION: this type is publicly exported in `cedar-policy`. // Don't make fields `pub`, don't make breaking changes, and use caution // when adding public methods. - #[derive(Debug, Clone, Error)] + #[derive(Eq, PartialEq, Debug, Clone, Error)] #[error("The name `{name}` shadows a builtin Cedar name. You'll have to refer to the builtin as `__cedar::{name}`.")] pub struct ShadowsBuiltinWarning { pub(crate) name: SmolStr, @@ -655,7 +655,7 @@ pub mod schema_warnings { // CAUTION: this type is publicly exported in `cedar-policy`. // Don't make fields `pub`, don't make breaking changes, and use caution // when adding public methods. - #[derive(Debug, Clone, Error)] + #[derive(Eq, PartialEq, Debug, Clone, Error)] #[error("The common type name {name} shadows an entity name")] pub struct ShadowsEntityWarning { pub(crate) name: SmolStr, @@ -690,7 +690,7 @@ pub mod schema_warnings { // CAUTION: this type is publicly exported in `cedar-policy`. // Don't make fields `pub`, don't make breaking changes, and use caution // when adding public methods. -#[derive(Debug, Clone, Error, Diagnostic)] +#[derive(Eq, PartialEq, Debug, Clone, Error, Diagnostic)] #[non_exhaustive] pub enum SchemaWarning { /// Warning when a declaration shadows a builtin type diff --git a/cedar-policy-validator/src/cedar_schema/test.rs b/cedar-policy-validator/src/cedar_schema/test.rs index ab416f75b..158a602ba 100644 --- a/cedar-policy-validator/src/cedar_schema/test.rs +++ b/cedar-policy-validator/src/cedar_schema/test.rs @@ -528,7 +528,7 @@ namespace Baz {action "Foo" appliesTo { Extensions::all_available(), ) .expect("Schema should parse"); - assert!(warnings.collect::>().is_empty()); + assert_eq!(warnings.collect::>(), vec![]); let github = fragment .0 .get(&Some("GitHub".parse().unwrap())) @@ -657,7 +657,7 @@ namespace Baz {action "Foo" appliesTo { Extensions::all_available(), ) .expect("failed to parse"); - assert!(warnings.collect::>().is_empty()); + assert_eq!(warnings.collect::>(), vec![]); let doccloud = fragment .0 .get(&Some("DocCloud".parse().unwrap())) @@ -785,7 +785,7 @@ namespace Baz {action "Foo" appliesTo { "#; let (_, warnings) = json_schema::Fragment::from_cedarschema_str(src, Extensions::all_available()).unwrap(); - assert!(warnings.collect::>().is_empty()); + assert_eq!(warnings.collect::>(), vec![]); } #[test] @@ -855,7 +855,7 @@ namespace Baz {action "Foo" appliesTo { let (_, warnings) = json_schema::Fragment::from_cedarschema_str(src, Extensions::all_available()).unwrap(); - assert!(warnings.collect::>().is_empty()); + assert_eq!(warnings.collect::>(), vec![]); } #[test] @@ -876,7 +876,7 @@ namespace Baz {action "Foo" appliesTo { "#; let (fragment, warnings) = json_schema::Fragment::from_cedarschema_str(src, Extensions::all_available()).unwrap(); - assert!(warnings.collect::>().is_empty()); + assert_eq!(warnings.collect::>(), vec![]); let service = fragment.0.get(&Some("Service".parse().unwrap())).unwrap(); let resource = service .entity_types diff --git a/cedar-policy-validator/src/coreschema.rs b/cedar-policy-validator/src/coreschema.rs index 1b00694b6..18c4c18d4 100644 --- a/cedar-policy-validator/src/coreschema.rs +++ b/cedar-policy-validator/src/coreschema.rs @@ -358,7 +358,7 @@ pub mod request_validation_errors { /// not valid for the request action #[derive(Debug, Error, Diagnostic)] #[error("principal type `{principal_ty}` is not valid for `{action}`")] - #[diagnostic(help("{}", invalid_principal_type_help(&.valid_principal_tys, .action.as_ref())))] + #[diagnostic(help("{}", invalid_principal_type_help(valid_principal_tys, .action.as_ref())))] pub struct InvalidPrincipalTypeError { /// Principal type which is not valid pub(super) principal_ty: ast::EntityType, @@ -407,7 +407,7 @@ pub mod request_validation_errors { /// not valid for the request action #[derive(Debug, Error, Diagnostic)] #[error("resource type `{resource_ty}` is not valid for `{action}`")] - #[diagnostic(help("{}", invalid_resource_type_help(&.valid_resource_tys, .action.as_ref())))] + #[diagnostic(help("{}", invalid_resource_type_help(valid_resource_tys, .action.as_ref())))] pub struct InvalidResourceTypeError { /// Resource type which is not valid pub(super) resource_ty: ast::EntityType, @@ -888,7 +888,7 @@ mod test { (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None), (ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(), None), (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None), - context_with_extra_attr.clone(), + context_with_extra_attr, Some(&schema()), Extensions::all_available(), ), @@ -914,7 +914,7 @@ mod test { (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None), (ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(), None), (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None), - context_with_wrong_type_attr.clone(), + context_with_wrong_type_attr, Some(&schema()), Extensions::all_available(), ), @@ -943,7 +943,7 @@ mod test { (ast::EntityUID::with_eid_and_type("User", "abc123").unwrap(), None), (ast::EntityUID::with_eid_and_type("Action", "edit_photo").unwrap(), None), (ast::EntityUID::with_eid_and_type("Photo", "vacationphoto94.jpg").unwrap(), None), - context_with_heterogeneous_set.clone(), + context_with_heterogeneous_set, Some(&schema()), Extensions::all_available(), ), diff --git a/cedar-policy-validator/src/entity_manifest.rs b/cedar-policy-validator/src/entity_manifest.rs index 209e542aa..08a39f936 100644 --- a/cedar-policy-validator/src/entity_manifest.rs +++ b/cedar-policy-validator/src/entity_manifest.rs @@ -304,7 +304,7 @@ fn union_fields(first: &Fields, second: &Fields) -> Fields { for (key, value) in second { res.entry(key.clone()) .and_modify(|existing| existing.union_mut(value)) - .or_insert(value.clone()); + .or_insert_with(|| value.clone()); } res } @@ -375,7 +375,7 @@ impl RootAccessTrie { self.trie .entry(key.clone()) .and_modify(|existing| existing.union_mut(value)) - .or_insert(value.clone()); + .or_insert_with(|| value.clone()); } } } diff --git a/cedar-policy-validator/src/entity_manifest/loader.rs b/cedar-policy-validator/src/entity_manifest/loader.rs index 902f80c3c..bafc2e1c7 100644 --- a/cedar-policy-validator/src/entity_manifest/loader.rs +++ b/cedar-policy-validator/src/entity_manifest/loader.rs @@ -58,7 +58,7 @@ pub(crate) struct EntityRequestRef<'a> { access_trie: &'a AccessTrie, } -impl<'a> EntityRequestRef<'a> { +impl EntityRequestRef<'_> { fn to_request(&self) -> EntityRequest { EntityRequest { entity_id: self.entity_id.clone(), @@ -251,7 +251,7 @@ pub(crate) fn load_entities( } let mut next_to_load = vec![]; - for (entity_request, loaded_maybe) in to_load.drain(..).zip(new_entities) { + for (entity_request, loaded_maybe) in to_load.into_iter().zip(new_entities) { if let Some(loaded) = loaded_maybe { next_to_load.extend(find_remaining_entities( &loaded, @@ -435,7 +435,7 @@ fn compute_ancestors_request( while !to_visit.is_empty() { let mut next_to_visit = vec![]; - for entity_request in to_visit.drain(..) { + for entity_request in to_visit { // check the is_ancestor flag for entities // the is_ancestor flag on sets of entities is handled by find_remaining_entities if entity_request.access_trie.is_ancestor { diff --git a/cedar-policy-validator/src/entity_manifest/slicing.rs b/cedar-policy-validator/src/entity_manifest/slicing.rs index 999f756ee..73285db4d 100644 --- a/cedar-policy-validator/src/entity_manifest/slicing.rs +++ b/cedar-policy-validator/src/entity_manifest/slicing.rs @@ -140,7 +140,7 @@ struct EntitySlicer<'a> { entities: &'a Entities, } -impl<'a> EntityLoader for EntitySlicer<'a> { +impl EntityLoader for EntitySlicer<'_> { fn load_entities( &mut self, to_load: &[EntityRequest], diff --git a/cedar-policy-validator/src/entity_manifest/type_annotations.rs b/cedar-policy-validator/src/entity_manifest/type_annotations.rs index 6ae2fde80..0c70e771e 100644 --- a/cedar-policy-validator/src/entity_manifest/type_annotations.rs +++ b/cedar-policy-validator/src/entity_manifest/type_annotations.rs @@ -69,16 +69,16 @@ impl RootAccessTrie { match key { EntityRoot::Literal(lit) => slice.to_typed( request_type, - &Type::euid_literal(lit.clone(), schema).ok_or( + &Type::euid_literal(lit.clone(), schema).ok_or_else(|| { MismatchedMissingEntityError { entity: lit.clone(), - }, - )?, + } + })?, schema, )?, EntityRoot::Var(Var::Action) => { let ty = Type::euid_literal(request_type.action.clone(), schema) - .ok_or(MismatchedMissingEntityError { + .ok_or_else(|| MismatchedMissingEntityError { entity: request_type.action.clone(), })?; slice.to_typed(request_type, &ty, schema)? @@ -96,7 +96,7 @@ impl RootAccessTrie { EntityRoot::Var(Var::Context) => { let ty = &schema .get_action_id(&request_type.action.clone()) - .ok_or(MismatchedMissingEntityError { + .ok_or_else(|| MismatchedMissingEntityError { entity: request_type.action.clone(), })? .context; diff --git a/cedar-policy-validator/src/lib.rs b/cedar-policy-validator/src/lib.rs index d6782bf2e..63abdc23a 100644 --- a/cedar-policy-validator/src/lib.rs +++ b/cedar-policy-validator/src/lib.rs @@ -323,13 +323,13 @@ mod test { let policy_a_src = r#"permit(principal in foo_type::"a", action == Action::"actin", resource == bar_type::"b");"#; let policy_a = parser::parse_policy(Some(PolicyID::from_string("pola")), policy_a_src) .expect("Test Policy Should Parse"); - set.add_static(policy_a.clone()) + set.add_static(policy_a) .expect("Policy already present in PolicySet"); let policy_b_src = r#"permit(principal in foo_tye::"a", action == Action::"action", resource == br_type::"b");"#; let policy_b = parser::parse_policy(Some(PolicyID::from_string("polb")), policy_b_src) .expect("Test Policy Should Parse"); - set.add_static(policy_b.clone()) + set.add_static(policy_b) .expect("Policy already present in PolicySet"); let result = validator.validate(&set, ValidationMode::default()); @@ -508,7 +508,7 @@ mod test { // `result` contains the two prior error messages plus one new one assert_eq!(result.validation_errors().count(), 3); let invalid_action_err = ValidationError::invalid_action_application( - loc.clone(), + loc, PolicyID::from_string("link3"), false, false, diff --git a/cedar-policy-validator/src/schema.rs b/cedar-policy-validator/src/schema.rs index 8e0c88722..d93b012fb 100644 --- a/cedar-policy-validator/src/schema.rs +++ b/cedar-policy-validator/src/schema.rs @@ -2788,8 +2788,7 @@ pub(crate) mod test { } } ); - let schema = - ValidatorSchema::from_json_value(src.clone(), Extensions::all_available()).unwrap(); + let schema = ValidatorSchema::from_json_value(src, Extensions::all_available()).unwrap(); let mut attributes = assert_entity_type_exists(&schema, "Demo::User").attributes(); let (attr_name, attr_ty) = attributes.next().unwrap(); assert_eq!(attr_name, "id"); @@ -3511,7 +3510,7 @@ pub(crate) mod test { "actions": { }, } }); - let schema = ValidatorSchema::from_json_value(src.clone(), Extensions::all_available()); + let schema = ValidatorSchema::from_json_value(src, Extensions::all_available()); assert_matches!(schema, Err(SchemaError::JsonDeserialization(_))); let src: serde_json::Value = json!({ @@ -3521,7 +3520,7 @@ pub(crate) mod test { "actions": { }, } }); - let schema = ValidatorSchema::from_json_value(src.clone(), Extensions::all_available()); + let schema = ValidatorSchema::from_json_value(src, Extensions::all_available()); assert_matches!(schema, Err(SchemaError::JsonDeserialization(_))); let src: serde_json::Value = json!({ @@ -3535,7 +3534,7 @@ pub(crate) mod test { "actions": { }, } }); - let schema = ValidatorSchema::from_json_value(src.clone(), Extensions::all_available()); + let schema = ValidatorSchema::from_json_value(src, Extensions::all_available()); assert_matches!(schema, Err(SchemaError::JsonDeserialization(_))); let src: serde_json::Value = json!({ @@ -3549,7 +3548,7 @@ pub(crate) mod test { "actions": { }, } }); - let schema = ValidatorSchema::from_json_value(src.clone(), Extensions::all_available()); + let schema = ValidatorSchema::from_json_value(src, Extensions::all_available()); assert_matches!(schema, Err(SchemaError::JsonDeserialization(_))); let src: serde_json::Value = json!({ @@ -4650,7 +4649,7 @@ mod entity_tags { }, "actions": {} }}); - assert_matches!(ValidatorSchema::from_json_value(json.clone(), Extensions::all_available()), Ok(schema) => { + assert_matches!(ValidatorSchema::from_json_value(json, Extensions::all_available()), Ok(schema) => { let user = assert_entity_type_exists(&schema, "User"); assert_matches!(user.tag_type(), Some(Type::Set { element_type: Some(el_ty) }) => { assert_matches!(&**el_ty, Type::Primitive { primitive_type: Primitive::String }); @@ -5136,8 +5135,8 @@ action CreateList in Create appliesTo { #[test] fn empty_schema_principals_and_resources() { let empty: ValidatorSchema = "".parse().unwrap(); - assert!(empty.principals().collect::>().is_empty()); - assert!(empty.resources().collect::>().is_empty()); + assert!(empty.principals().next().is_none()); + assert!(empty.resources().next().is_none()); } #[test] @@ -5350,8 +5349,8 @@ action CreateList in Create appliesTo { #[test] fn empty_schema_principals_and_resources() { let empty: ValidatorSchema = "".parse().unwrap(); - assert!(empty.principals().collect::>().is_empty()); - assert!(empty.resources().collect::>().is_empty()); + assert!(empty.principals().next().is_none()); + assert!(empty.resources().next().is_none()); } #[test] diff --git a/cedar-policy-validator/src/typecheck/test/expr.rs b/cedar-policy-validator/src/typecheck/test/expr.rs index 665848213..73a88e875 100644 --- a/cedar-policy-validator/src/typecheck/test/expr.rs +++ b/cedar-policy-validator/src/typecheck/test/expr.rs @@ -1158,7 +1158,7 @@ fn less_than_typecheck_fails() { ValidationError::expected_one_of_types( get_loc(src, "\"\""), expr_id_placeholder(), - expected_types.clone(), + expected_types, Type::primitive_string(), None, ), diff --git a/cedar-policy-validator/src/typecheck/test/extensions.rs b/cedar-policy-validator/src/typecheck/test/extensions.rs index b58c660f0..3215b9bd0 100644 --- a/cedar-policy-validator/src/typecheck/test/extensions.rs +++ b/cedar-policy-validator/src/typecheck/test/extensions.rs @@ -77,7 +77,7 @@ fn ip_extension_typecheck_fails() { ); let src = "ip(\"127.0.0.1\").isIpv4(3)"; let expr = Expr::from_str(src).expect("parsing should succeed"); - let errors = assert_typecheck_fails_empty_schema(expr.clone(), Type::primitive_boolean()); + let errors = assert_typecheck_fails_empty_schema(expr, Type::primitive_boolean()); let type_error = assert_exactly_one_diagnostic(errors); assert_eq!( type_error, @@ -145,8 +145,7 @@ fn decimal_extension_typecheck_fails() { ); let src = "decimal(\"foo\")"; let expr = Expr::from_str(src).expect("parsing should succeed"); - let errors = - assert_typecheck_fails_empty_schema(expr.clone(), Type::extension(decimal_name.clone())); + let errors = assert_typecheck_fails_empty_schema(expr, Type::extension(decimal_name.clone())); let type_error = assert_exactly_one_diagnostic(errors); assert_eq!( type_error, @@ -158,7 +157,7 @@ fn decimal_extension_typecheck_fails() { ); let src = "decimal(\"1.23\").lessThan(3, 4)"; let expr = Expr::from_str(src).expect("parsing should succeed"); - let errors = assert_typecheck_fails_empty_schema(expr.clone(), Type::primitive_boolean()); + let errors = assert_typecheck_fails_empty_schema(expr, Type::primitive_boolean()); let type_error = assert_exactly_one_diagnostic(errors); assert_eq!( type_error, @@ -200,7 +199,7 @@ fn datetime_extension_typechecks() { assert_typechecks_empty_schema(expr, Type::extension(duration_name.clone())); let expr = Expr::from_str(r#"datetime("2024-10-28").toDate()"#).expect("parsing should succeed"); - assert_typechecks_empty_schema(expr, Type::extension(datetime_name.clone())); + assert_typechecks_empty_schema(expr, Type::extension(datetime_name)); let expr = Expr::from_str(r#"datetime("2024-10-28").toTime()"#).expect("parsing should succeed"); assert_typechecks_empty_schema(expr, Type::extension(duration_name.clone())); @@ -224,7 +223,7 @@ fn datetime_extension_typechecks() { assert_typechecks_empty_schema(expr, Type::primitive_boolean()); let expr = Expr::from_str("duration(\"1h\")").expect("parsing should succeed"); - assert_typechecks_empty_schema(expr, Type::extension(duration_name.clone())); + assert_typechecks_empty_schema(expr, Type::extension(duration_name)); let expr = Expr::from_str(r#"duration("1h").toMilliseconds()"#).expect("parsing should succeed"); assert_typechecks_empty_schema(expr, Type::primitive_long()); @@ -264,8 +263,7 @@ fn datetime_extension_typecheck_fails() { ); let src = "datetime(\"foo\")"; let expr = Expr::from_str(src).expect("parsing should succeed"); - let errors = - assert_typecheck_fails_empty_schema(expr.clone(), Type::extension(datetime_name.clone())); + let errors = assert_typecheck_fails_empty_schema(expr, Type::extension(datetime_name.clone())); let type_error = assert_exactly_one_diagnostic(errors); assert_eq!( type_error, @@ -292,8 +290,7 @@ fn datetime_extension_typecheck_fails() { ); let src = "duration(\"foo\")"; let expr = Expr::from_str(src).expect("parsing should succeed"); - let errors = - assert_typecheck_fails_empty_schema(expr.clone(), Type::extension(duration_name.clone())); + let errors = assert_typecheck_fails_empty_schema(expr, Type::extension(duration_name.clone())); let type_error = assert_exactly_one_diagnostic(errors); assert_eq!( type_error, @@ -306,8 +303,7 @@ fn datetime_extension_typecheck_fails() { let src = r#"datetime("2024-10-28").offset(3, 4)"#; let expr = Expr::from_str(src).expect("parsing should succeed"); - let errors = - assert_typecheck_fails_empty_schema(expr.clone(), Type::extension(datetime_name.clone())); + let errors = assert_typecheck_fails_empty_schema(expr, Type::extension(datetime_name.clone())); let type_error = assert_exactly_one_diagnostic(errors); assert_eq!( type_error, diff --git a/cedar-policy-validator/src/typecheck/test/namespace.rs b/cedar-policy-validator/src/typecheck/test/namespace.rs index 1766e0f01..e7ad81496 100644 --- a/cedar-policy-validator/src/typecheck/test/namespace.rs +++ b/cedar-policy-validator/src/typecheck/test/namespace.rs @@ -586,7 +586,7 @@ fn multi_namespace_action_eq() { r#"permit(principal, action, resource) when { NS1::Action::"B" == NS2::Action::"B" };"#, ) .unwrap(); - let warnings = assert_policy_typecheck_warns(schema.clone(), policy.clone()); + let warnings = assert_policy_typecheck_warns(schema, policy.clone()); let warning = assert_exactly_one_diagnostic(warnings); assert_eq!( warning, @@ -653,7 +653,7 @@ fn multi_namespace_action_in() { r#"permit(principal, action in NS4::Action::"Group", resource);"#, ) .unwrap(); - let warnings = assert_policy_typecheck_warns(schema.clone(), policy.clone()); + let warnings = assert_policy_typecheck_warns(schema, policy.clone()); let warning = assert_exactly_one_diagnostic(warnings); assert_eq!( warning, @@ -687,7 +687,7 @@ fn test_cedar_policy_642() { .unwrap(); assert_policy_typechecks( - schema.clone(), + schema, parse_policy( None, r#" diff --git a/cedar-policy-validator/src/typecheck/test/partial.rs b/cedar-policy-validator/src/typecheck/test/partial.rs index aee372abd..1238f623c 100644 --- a/cedar-policy-validator/src/typecheck/test/partial.rs +++ b/cedar-policy-validator/src/typecheck/test/partial.rs @@ -39,7 +39,7 @@ pub(crate) fn assert_partial_typecheck( let mut errors: HashSet = HashSet::new(); let mut warnings: HashSet = HashSet::new(); let typechecked = typechecker.typecheck_policy( - &Template::link_static_policy(policy.clone()).0, + &Template::link_static_policy(policy).0, &mut errors, &mut warnings, ); @@ -58,7 +58,7 @@ pub(crate) fn assert_partial_typecheck_fails( let mut errors: HashSet = HashSet::new(); let mut warnings: HashSet = HashSet::new(); let typechecked = typechecker.typecheck_policy( - &Template::link_static_policy(policy.clone()).0, + &Template::link_static_policy(policy).0, &mut errors, &mut warnings, ); @@ -77,7 +77,7 @@ pub(crate) fn assert_partial_typecheck_warns( let mut errors: HashSet = HashSet::new(); let mut warnings: HashSet = HashSet::new(); let typechecked = typechecker.typecheck_policy( - &Template::link_static_policy(policy.clone()).0, + &Template::link_static_policy(policy).0, &mut errors, &mut warnings, ); @@ -460,7 +460,7 @@ mod fails_empty_schema { let src = r#"permit(principal, action, resource) when { resource.bar && false };"#; let p = parse_policy(None, src).unwrap(); assert_typecheck_warns_empty_schema( - p.clone(), + p, [ValidationWarning::impossible_policy( get_loc(src, src), PolicyID::from_string("policy0"), @@ -473,7 +473,7 @@ mod fails_empty_schema { let src = r#"permit(principal, action, resource) when { {foo: 1}.bar };"#; let p = parse_policy(None, src).unwrap(); assert_typecheck_fails_empty_schema( - p.clone(), + p, [ValidationError::unsafe_attribute_access( get_loc(src, "{foo: 1}.bar"), PolicyID::from_string("policy0"), diff --git a/cedar-policy-validator/src/typecheck/test/policy.rs b/cedar-policy-validator/src/typecheck/test/policy.rs index 57693bee9..c10f19756 100644 --- a/cedar-policy-validator/src/typecheck/test/policy.rs +++ b/cedar-policy-validator/src/typecheck/test/policy.rs @@ -1138,7 +1138,7 @@ fn extended_has() { }; "#; let policy = parse_policy(None, src).unwrap(); - let errors = assert_policy_typecheck_fails(schema.clone(), policy); + let errors = assert_policy_typecheck_fails(schema, policy); let error = assert_exactly_one_diagnostic(errors); assert_eq!( error, diff --git a/cedar-policy-validator/src/typecheck/test/strict.rs b/cedar-policy-validator/src/typecheck/test/strict.rs index 98234012d..8ec6178ea 100644 --- a/cedar-policy-validator/src/typecheck/test/strict.rs +++ b/cedar-policy-validator/src/typecheck/test/strict.rs @@ -721,7 +721,7 @@ fn qualified_record_attr() { let src = "permit(principal, action, resource) when { context == {num_of_things: 1}};"; let p = parse_policy_or_template(None, src).unwrap(); - let errors = assert_policy_typecheck_fails(schema, p.clone()); + let errors = assert_policy_typecheck_fails(schema, p); let error = assert_exactly_one_diagnostic(errors); assert_eq!( error, diff --git a/cedar-policy-validator/src/typecheck/test/test_utils.rs b/cedar-policy-validator/src/typecheck/test/test_utils.rs index a2304f7b2..c45db4482 100644 --- a/cedar-policy-validator/src/typecheck/test/test_utils.rs +++ b/cedar-policy-validator/src/typecheck/test/test_utils.rs @@ -167,7 +167,7 @@ impl SchemaProvider for NamespaceDefinitionWithActionAttributes { } } -impl<'a> SchemaProvider for &'a str { +impl SchemaProvider for &str { fn schema(self) -> ValidatorSchema { ValidatorSchema::from_cedarschema_str(self, Extensions::all_available()) .unwrap_or_else(|e| panic!("failed to construct schema: {:?}", miette::Report::new(e))) diff --git a/cedar-policy-validator/src/types.rs b/cedar-policy-validator/src/types.rs index aaab3bd67..ade1f88b3 100644 --- a/cedar-policy-validator/src/types.rs +++ b/cedar-policy-validator/src/types.rs @@ -1851,7 +1851,6 @@ mod test { AsRef::::as_ref(s).into(), AttributeType::required_attribute(t.clone()) )) - .collect::>() ), entity_lub.get_attribute_types(&schema), "Incorrect computed record type for LUB for {mode:?}." @@ -2613,7 +2612,7 @@ mod test { assert_least_upper_bound( action_schema(), ValidationMode::Permissive, - action_view_ty.clone(), + action_view_ty, Type::any_record(), Err(LubHelp::EntityRecord), ); diff --git a/cedar-policy/src/api.rs b/cedar-policy/src/api.rs index c83ea6766..91c859d9d 100644 --- a/cedar-policy/src/api.rs +++ b/cedar-policy/src/api.rs @@ -2378,10 +2378,7 @@ impl Protobuf for PolicySet { fn decode(buf: impl prost::bytes::Buf) -> Result { let ast = proto::try_decode::(buf)? .expect("proto-encoded policy set should be a valid policy set"); - Ok( - PolicySet::from_ast(ast) - .expect("proto-encoded policy set should be a valid policy set"), - ) + Ok(Self::from_ast(ast).expect("proto-encoded policy set should be a valid policy set")) } } diff --git a/cedar-policy/src/ffi/utils.rs b/cedar-policy/src/ffi/utils.rs index 966f81e1d..49dfe10d1 100644 --- a/cedar-policy/src/ffi/utils.rs +++ b/cedar-policy/src/ffi/utils.rs @@ -296,6 +296,11 @@ impl Policy { } /// Get valid principals, actions, and resources. + /// + /// # Errors + /// + /// Returns an error result if `self` cannot be parsed as a + /// [`crate::Policy`] or if `s` cannot be parsed as a [`crate::Schema`]. pub fn get_valid_request_envs( self, s: Schema, @@ -375,6 +380,11 @@ impl Template { } /// Get valid principals, actions, and resources. + /// + /// # Errors + /// + /// Returns an error result if `self` cannot be parsed as a + /// [`crate::Template`] or if `s` cannot be parsed as a [`crate::Schema`]. pub fn get_valid_request_envs( self, s: Schema, From 06e9cb70a66dc08ce07e868f1f241146f6fa5e48 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 09:33:40 -0500 Subject: [PATCH 03/16] Bump the rust-dependencies group with 7 updates (#1375) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 42 ++++++++++++++++++------------------- cedar-policy-cli/Cargo.toml | 2 +- cedar-policy/Cargo.toml | 2 +- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 89222650b..dbdb44df5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -240,9 +240,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a68f1f47cdf0ec8ee4b941b2eee2a80cb796db73118c0dd09ac63fbe405be22" +checksum = "786a307d683a5bf92e6fd5fd69a7eb613751668d1d8d67d802846dfe367c62c8" dependencies = [ "memchr", "regex-automata", @@ -287,9 +287,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.3" +version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27f657647bcff5394bf56c7317665bbf790a137a50eaaa5c6bfbb9e27a518f2d" +checksum = "9157bbaa6b165880c27a4293a474c91cdcf265cc68cc829bf10be0964a391caf" dependencies = [ "shlex", ] @@ -320,7 +320,7 @@ dependencies = [ "serde_json", "serde_with", "smol_str", - "thiserror 2.0.6", + "thiserror 2.0.7", "tsify", "wasm-bindgen", ] @@ -345,7 +345,7 @@ dependencies = [ "serde", "serde_json", "tempfile", - "thiserror 2.0.6", + "thiserror 2.0.7", ] [[package]] @@ -374,7 +374,7 @@ dependencies = [ "serde_with", "smol_str", "stacker", - "thiserror 2.0.6", + "thiserror 2.0.7", "tsify", "wasm-bindgen", ] @@ -417,7 +417,7 @@ dependencies = [ "similar-asserts", "smol_str", "stacker", - "thiserror 2.0.6", + "thiserror 2.0.7", "tsify", "unicode-security", "wasm-bindgen", @@ -641,9 +641,9 @@ dependencies = [ [[package]] name = "crossbeam-deque" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -660,9 +660,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crunchy" @@ -2140,9 +2140,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "semver" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" dependencies = [ "serde", ] @@ -2404,9 +2404,9 @@ dependencies = [ [[package]] name = "term" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4df4175de05129f31b80458c6df371a15e7fc3fd367272e6bf938e5c351c7ea0" +checksum = "a3bb6001afcea98122260987f8b7b5da969ecad46dbf0b5453702f776b491a41" dependencies = [ "home", "windows-sys 0.52.0", @@ -2449,11 +2449,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.6" +version = "2.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec2a1820ebd077e2b90c4df007bebf344cd394098a13c563957d0afc83ea47" +checksum = "93605438cbd668185516ab499d589afb7ee1859ea3d5fc8f6b0755e1c7443767" dependencies = [ - "thiserror-impl 2.0.6", + "thiserror-impl 2.0.7", ] [[package]] @@ -2469,9 +2469,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.6" +version = "2.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d65750cab40f4ff1929fb1ba509e9914eb756131cef4210da8d5d700d26f6312" +checksum = "e1d8749b4531af2117677a5fcd12b1348a3fe2b81e36e61ffeac5c4aa3273e36" dependencies = [ "proc-macro2", "quote", diff --git a/cedar-policy-cli/Cargo.toml b/cedar-policy-cli/Cargo.toml index e64dd98ad..6c7a25142 100644 --- a/cedar-policy-cli/Cargo.toml +++ b/cedar-policy-cli/Cargo.toml @@ -20,7 +20,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" miette = { version = "7.4.0", features = ["fancy"] } thiserror = "2.0" -semver = "1.0.23" +semver = "1.0.24" prost = {version = "0.13", optional = true} [build-dependencies] diff --git a/cedar-policy/Cargo.toml b/cedar-policy/Cargo.toml index 1a89f778e..5558a8b63 100644 --- a/cedar-policy/Cargo.toml +++ b/cedar-policy/Cargo.toml @@ -31,7 +31,7 @@ prost = { version = "0.13", optional = true } serde-wasm-bindgen = { version = "0.6", optional = true } tsify = { version = "0.4.5", optional = true } wasm-bindgen = { version = "0.2.97", optional = true } -semver = "1.0.23" +semver = "1.0.24" lazy_static = "1.5.0" [features] From ed35ba7e2502c202a051e73e0bb8e4129244e73f Mon Sep 17 00:00:00 2001 From: shaobo-he-aws <130499339+shaobo-he-aws@users.noreply.github.com> Date: Mon, 16 Dec 2024 09:14:19 -0800 Subject: [PATCH 04/16] RFC 48 implementation (#1316) Signed-off-by: Shaobo He Co-authored-by: Craig Disselkoen --- cedar-policy-core/src/ast.rs | 2 + cedar-policy-core/src/ast/annotation.rs | 167 ++++++ cedar-policy-core/src/ast/id.rs | 2 + cedar-policy-core/src/ast/policy.rs | 121 +--- cedar-policy-core/src/ast/policy_set.rs | 3 +- cedar-policy-core/src/authorizer.rs | 2 + cedar-policy-core/src/est.rs | 62 +- cedar-policy-core/src/est/annotation.rs | 86 +++ cedar-policy-core/src/parser/node.rs | 3 +- .../src/cedar_schema/ast.rs | 59 +- .../src/cedar_schema/err.rs | 84 ++- .../src/cedar_schema/fmt.rs | 67 ++- .../src/cedar_schema/grammar.lalrpop | 67 ++- .../src/cedar_schema/test.rs | 420 ++++++++++++- .../src/cedar_schema/to_json_schema.rs | 136 +++-- cedar-policy-validator/src/json_schema.rs | 562 +++++++++++++++++- cedar-policy-validator/src/lib.rs | 5 +- cedar-policy-validator/src/rbac.rs | 40 +- cedar-policy-validator/src/schema.rs | 1 + .../src/schema/namespace_def.rs | 23 +- .../src/typecheck/test/expr.rs | 3 + cedar-policy/CHANGELOG.md | 1 + cedar-policy/src/ffi/utils.rs | 2 +- cedar-wasm/build-wasm.sh | 1 + 24 files changed, 1595 insertions(+), 324 deletions(-) create mode 100644 cedar-policy-core/src/ast/annotation.rs create mode 100644 cedar-policy-core/src/est/annotation.rs diff --git a/cedar-policy-core/src/ast.rs b/cedar-policy-core/src/ast.rs index 7e6833901..7b6cbe1eb 100644 --- a/cedar-policy-core/src/ast.rs +++ b/cedar-policy-core/src/ast.rs @@ -56,3 +56,5 @@ mod value; pub use value::*; mod expr_iterator; pub use expr_iterator::*; +mod annotation; +pub use annotation::*; diff --git a/cedar-policy-core/src/ast/annotation.rs b/cedar-policy-core/src/ast/annotation.rs new file mode 100644 index 000000000..ac7b2eb33 --- /dev/null +++ b/cedar-policy-core/src/ast/annotation.rs @@ -0,0 +1,167 @@ +/* + * Copyright Cedar Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use std::collections::BTreeMap; + +use educe::Educe; +use serde::{Deserialize, Serialize}; +use smol_str::SmolStr; + +use crate::parser::Loc; + +use super::AnyId; + +/// Struct which holds the annotations for a policy +#[derive(Serialize, Deserialize, Clone, Hash, Eq, PartialEq, PartialOrd, Ord, Debug)] +pub struct Annotations( + #[serde(default)] + #[serde(skip_serializing_if = "BTreeMap::is_empty")] + #[serde(with = "::serde_with::rust::maps_duplicate_key_is_error")] + BTreeMap, +); + +impl std::fmt::Display for Annotations { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for (k, v) in &self.0 { + writeln!(f, "@{k}({v})")? + } + Ok(()) + } +} + +impl Annotations { + /// Create a new empty `Annotations` (with no annotations) + pub fn new() -> Self { + Self(BTreeMap::new()) + } + + /// Get an annotation by key + pub fn get(&self, key: &AnyId) -> Option<&Annotation> { + self.0.get(key) + } + + /// Iterate over all annotations + pub fn iter(&self) -> impl Iterator { + self.0.iter() + } + + /// Tell if it's empty + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +/// Wraps the [`BTreeMap`] into an opaque type so we can change it later if need be +#[derive(Debug)] +pub struct IntoIter(std::collections::btree_map::IntoIter); + +impl Iterator for IntoIter { + type Item = (AnyId, Annotation); + + fn next(&mut self) -> Option { + self.0.next() + } +} + +impl IntoIterator for Annotations { + type Item = (AnyId, Annotation); + + type IntoIter = IntoIter; + + fn into_iter(self) -> Self::IntoIter { + IntoIter(self.0.into_iter()) + } +} + +impl Default for Annotations { + fn default() -> Self { + Self::new() + } +} + +impl FromIterator<(AnyId, Annotation)> for Annotations { + fn from_iter>(iter: T) -> Self { + Self(BTreeMap::from_iter(iter)) + } +} + +impl From> for Annotations { + fn from(value: BTreeMap) -> Self { + Self(value) + } +} + +/// Struct which holds the value of a particular annotation +#[derive(Educe, Clone, Debug, Serialize, Deserialize, Default)] +#[educe(Hash, PartialEq, Eq, PartialOrd, Ord)] +#[serde(transparent)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))] +pub struct Annotation { + /// Annotation value + pub val: SmolStr, + /// Source location. Note this is the location of _the entire key-value + /// pair_ for the annotation, not just `val` above + #[serde(skip)] + #[educe(Hash(ignore))] + #[educe(PartialEq(ignore))] + #[educe(PartialOrd(ignore))] + pub loc: Option, +} + +impl std::fmt::Display for Annotation { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "\"{}\"", self.val.escape_debug()) + } +} + +impl Annotation { + /// Construct an Annotation with an optional value. This function is used + /// to construct annotations from the CST and EST representation where a + /// value is not required, but an absent value is equivalent to `""`. + /// Here, a `None` constructs an annotation containing the value `""`.` + pub fn with_optional_value(val: Option, loc: Option) -> Self { + Self { + val: val.unwrap_or_default(), + loc, + } + } +} + +impl AsRef for Annotation { + fn as_ref(&self) -> &str { + &self.val + } +} + +#[cfg(feature = "protobufs")] +impl From<&crate::ast::proto::Annotation> for Annotation { + fn from(v: &crate::ast::proto::Annotation) -> Self { + Self { + val: v.val.clone().into(), + loc: None, + } + } +} + +#[cfg(feature = "protobufs")] +impl From<&Annotation> for crate::ast::proto::Annotation { + fn from(v: &Annotation) -> Self { + Self { + val: v.val.to_string(), + } + } +} diff --git a/cedar-policy-core/src/ast/id.rs b/cedar-policy-core/src/ast/id.rs index 37f230a46..5dc3c5f98 100644 --- a/cedar-policy-core/src/ast/id.rs +++ b/cedar-policy-core/src/ast/id.rs @@ -262,6 +262,8 @@ impl<'a> arbitrary::Arbitrary<'a> for UnreservedId { // // For now, internally, `AnyId`s are just owned `SmolString`s. #[derive(Serialize, Debug, PartialEq, Eq, Clone, Hash, PartialOrd, Ord)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))] pub struct AnyId(SmolStr); impl AnyId { diff --git a/cedar-policy-core/src/ast/policy.rs b/cedar-policy-core/src/ast/policy.rs index 22a75ceb1..a0f4f2c1e 100644 --- a/cedar-policy-core/src/ast/policy.rs +++ b/cedar-policy-core/src/ast/policy.rs @@ -16,13 +16,13 @@ use crate::ast::*; use crate::parser::Loc; +use annotation::{Annotation, Annotations}; use educe::Educe; use itertools::Itertools; use miette::Diagnostic; use nonempty::{nonempty, NonEmpty}; use serde::{Deserialize, Serialize}; -use smol_str::{SmolStr, ToSmolStr}; -use std::collections::BTreeMap; +use smol_str::SmolStr; use std::{collections::HashMap, sync::Arc}; use thiserror::Error; @@ -1197,9 +1197,7 @@ impl From for TemplateBody { impl std::fmt::Display for TemplateBody { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - for (k, v) in self.annotations.iter() { - writeln!(f, "@{}(\"{}\")", k, v.val.escape_debug())? - } + self.annotations.fmt(f)?; write!( f, "{}(\n {},\n {},\n {}\n) when {{\n {}\n}};", @@ -1296,119 +1294,6 @@ impl From<&Template> for proto::TemplateBody { } } -/// Struct which holds the annotations for a policy -#[derive(Serialize, Deserialize, Clone, Hash, Eq, PartialEq, PartialOrd, Ord, Debug)] -pub struct Annotations(BTreeMap); - -impl Annotations { - /// Create a new empty `Annotations` (with no annotations) - pub fn new() -> Self { - Self(BTreeMap::new()) - } - - /// Get an annotation by key - pub fn get(&self, key: &AnyId) -> Option<&Annotation> { - self.0.get(key) - } - - /// Iterate over all annotations - pub fn iter(&self) -> impl Iterator { - self.0.iter() - } -} - -/// Wraps the [`BTreeMap`]` into an opaque type so we can change it later if need be -#[derive(Debug)] -pub struct IntoIter(std::collections::btree_map::IntoIter); - -impl Iterator for IntoIter { - type Item = (AnyId, Annotation); - - fn next(&mut self) -> Option { - self.0.next() - } -} - -impl IntoIterator for Annotations { - type Item = (AnyId, Annotation); - - type IntoIter = IntoIter; - - fn into_iter(self) -> Self::IntoIter { - IntoIter(self.0.into_iter()) - } -} - -impl Default for Annotations { - fn default() -> Self { - Self::new() - } -} - -impl FromIterator<(AnyId, Annotation)> for Annotations { - fn from_iter>(iter: T) -> Self { - Self(BTreeMap::from_iter(iter)) - } -} - -impl From> for Annotations { - fn from(value: BTreeMap) -> Self { - Self(value) - } -} - -/// Struct which holds the value of a particular annotation -#[derive(Educe, Serialize, Deserialize, Clone, Debug)] -#[educe(PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct Annotation { - /// Annotation value - pub val: SmolStr, - /// Source location. Note this is the location of _the entire key-value - /// pair_ for the annotation, not just `val` above - #[educe(PartialEq(ignore))] - #[educe(Hash(ignore))] - #[educe(PartialOrd(ignore))] - pub loc: Option, -} - -impl Annotation { - /// Construct an Annotation with an optional value. This function is used - /// to construct annotations from the CST and EST representation where a - /// value is not required, but an absent value is equivalent to `""`. - /// Here, a `None` constructs an annotation containing the value `""`.` - pub fn with_optional_value(val: Option, loc: Option) -> Self { - Self { - val: val.unwrap_or_else(|| "".to_smolstr()), - loc, - } - } -} - -impl AsRef for Annotation { - fn as_ref(&self) -> &str { - &self.val - } -} - -#[cfg(feature = "protobufs")] -impl From<&proto::Annotation> for Annotation { - fn from(v: &proto::Annotation) -> Self { - Self { - val: v.val.clone().into(), - loc: None, - } - } -} - -#[cfg(feature = "protobufs")] -impl From<&Annotation> for proto::Annotation { - fn from(v: &Annotation) -> Self { - Self { - val: v.val.to_string(), - } - } -} - /// Template constraint on principal scope variables #[derive(Serialize, Deserialize, Clone, Hash, Eq, PartialEq, PartialOrd, Ord, Debug)] pub struct PrincipalConstraint { diff --git a/cedar-policy-core/src/ast/policy_set.rs b/cedar-policy-core/src/ast/policy_set.rs index e5a6d8d67..af844f8a5 100644 --- a/cedar-policy-core/src/ast/policy_set.rs +++ b/cedar-policy-core/src/ast/policy_set.rs @@ -597,7 +597,8 @@ mod test { use super::*; use crate::{ ast::{ - ActionConstraint, Annotations, Effect, Expr, PrincipalConstraint, ResourceConstraint, + annotation::Annotations, ActionConstraint, Effect, Expr, PrincipalConstraint, + ResourceConstraint, }, parser, }; diff --git a/cedar-policy-core/src/authorizer.rs b/cedar-policy-core/src/authorizer.rs index 9d3aaab60..bb9ffe88a 100644 --- a/cedar-policy-core/src/authorizer.rs +++ b/cedar-policy-core/src/authorizer.rs @@ -180,6 +180,8 @@ impl std::fmt::Debug for Authorizer { #[allow(clippy::panic)] #[cfg(test)] mod test { + use crate::ast::Annotations; + use super::*; use crate::parser; diff --git a/cedar-policy-core/src/est.rs b/cedar-policy-core/src/est.rs index bff81c477..6c7cadc94 100644 --- a/cedar-policy-core/src/est.rs +++ b/cedar-policy-core/src/est.rs @@ -24,16 +24,17 @@ mod policy_set; pub use policy_set::*; mod scope_constraints; pub use scope_constraints::*; +mod annotation; +pub use annotation::*; -use crate::ast; use crate::ast::EntityUID; +use crate::ast::{self, Annotation}; use crate::entities::json::{err::JsonDeserializationError, EntityUidJson}; use crate::parser::cst; use crate::parser::err::{parse_errors, ParseErrors, ToASTError, ToASTErrorKind}; use crate::parser::util::{flatten_tuple_2, flatten_tuple_4}; use serde::{Deserialize, Serialize}; use serde_with::serde_as; -use smol_str::SmolStr; use std::collections::{BTreeMap, HashMap}; #[cfg(feature = "wasm")] @@ -64,10 +65,8 @@ pub struct Policy { conditions: Vec, /// annotations #[serde(default)] - #[serde(skip_serializing_if = "BTreeMap::is_empty")] - #[serde_as(as = "serde_with::MapPreventDuplicates<_,_>")] - #[cfg_attr(feature = "wasm", tsify(type = "Record"))] - annotations: BTreeMap>, + #[serde(skip_serializing_if = "Annotations::is_empty")] + annotations: Annotations, } /// Serde JSON structure for a `when` or `unless` clause in the EST format @@ -151,7 +150,12 @@ impl TryFrom for Policy { fn try_from(policy: cst::Policy) -> Result { let maybe_effect = policy.effect.to_effect(); let maybe_scope = policy.extract_scope(); - let maybe_annotations = policy.get_ast_annotations(|v, _| v); + let maybe_annotations = policy.get_ast_annotations(|v, l| { + Some(Annotation { + val: v?, + loc: Some(l.clone()), + }) + }); let maybe_conditions = ParseErrors::transpose(policy.conds.into_iter().map(|node| { let (cond, loc) = node.into_inner(); let cond = cond.ok_or_else(|| { @@ -172,7 +176,7 @@ impl TryFrom for Policy { action: action.into(), resource: resource.into(), conditions, - annotations, + annotations: Annotations(annotations), }) } } @@ -261,8 +265,14 @@ impl Policy { id, None, self.annotations + .0 .into_iter() - .map(|(key, val)| (key, ast::Annotation::with_optional_value(val, None))) + .map(|(key, val)| { + ( + key, + ast::Annotation::with_optional_value(val.map(|v| v.val), None), + ) + }) .collect(), self.effect, self.principal.try_into()?, @@ -306,13 +316,14 @@ impl From for Policy { action: ast.action_constraint().clone().into(), resource: ast.resource_constraint().into(), conditions: vec![ast.non_scope_constraints().clone().into()], - annotations: ast - .annotations() - // When converting from AST to EST, we will always interpret an - // empty-string annotation as an explicit `""` rather than - // `null` (which is implicitly equivalent to `""`). - .map(|(k, v)| (k.clone(), Some(v.val.clone()))) - .collect(), + annotations: Annotations( + ast.annotations() + // When converting from AST to EST, we will always interpret an + // empty-string annotation as an explicit `""` rather than + // `null` (which is implicitly equivalent to `""`). + .map(|(k, v)| (k.clone(), Some(v.clone()))) + .collect(), + ), } } } @@ -326,13 +337,14 @@ impl From for Policy { action: ast.action_constraint().clone().into(), resource: ast.resource_constraint().clone().into(), conditions: vec![ast.non_scope_constraints().clone().into()], - annotations: ast - .annotations() - // When converting from AST to EST, we will always interpret an - // empty-string annotation as an explicit `""` rather than - // `null` (which is implicitly equivalent to `""`) - .map(|(k, v)| (k.clone(), Some(v.val.clone()))) - .collect(), + annotations: Annotations( + ast.annotations() + // When converting from AST to EST, we will always interpret an + // empty-string annotation as an explicit `""` rather than + // `null` (which is implicitly equivalent to `""`) + .map(|(k, v)| (k.clone(), Some(v.clone()))) + .collect(), + ), } } } @@ -345,10 +357,10 @@ impl From> for Clause { impl std::fmt::Display for Policy { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - for (k, v) in self.annotations.iter() { + for (k, v) in self.annotations.0.iter() { write!(f, "@{k}")?; if let Some(v) = v { - write!(f, "(\"{}\")", v.escape_debug())?; + write!(f, "({v})")?; } writeln!(f)?; } diff --git a/cedar-policy-core/src/est/annotation.rs b/cedar-policy-core/src/est/annotation.rs new file mode 100644 index 000000000..eb4df1e43 --- /dev/null +++ b/cedar-policy-core/src/est/annotation.rs @@ -0,0 +1,86 @@ +/* + * Copyright Cedar Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +use std::collections::BTreeMap; + +use serde::{Deserialize, Serialize}; + +use crate::ast::{self, Annotation, AnyId}; +#[cfg(feature = "wasm")] +extern crate tsify; + +/// Similar to [`ast::Annotations`] but allow annotation value to be `null` +#[derive(Serialize, Deserialize, Clone, Hash, Eq, PartialEq, PartialOrd, Ord, Debug)] +#[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +pub struct Annotations( + #[serde(default)] + #[serde(skip_serializing_if = "BTreeMap::is_empty")] + #[serde(with = "::serde_with::rust::maps_duplicate_key_is_error")] + #[cfg_attr(feature = "wasm", tsify(type = "Record"))] + pub BTreeMap>, +); + +impl Annotations { + /// Create a new empty `Annotations` (with no annotations) + pub fn new() -> Self { + Self(BTreeMap::new()) + } + /// Tell if it's empty + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +impl From for ast::Annotations { + fn from(value: Annotations) -> Self { + ast::Annotations::from_iter( + value + .0 + .into_iter() + .map(|(key, value)| (key, value.unwrap_or_default())), + ) + } +} + +impl From for Annotations { + fn from(value: ast::Annotations) -> Self { + Self( + value + .into_iter() + .map(|(key, value)| (key, Some(value))) + .collect(), + ) + } +} + +impl std::fmt::Display for Annotations { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for (k, v) in &self.0 { + if let Some(anno) = v { + writeln!(f, "@{k}({anno})")? + } else { + writeln!(f, "@{k}")? + } + } + Ok(()) + } +} + +impl Default for Annotations { + fn default() -> Self { + Self::new() + } +} diff --git a/cedar-policy-core/src/parser/node.rs b/cedar-policy-core/src/parser/node.rs index 3b626962c..5b63457be 100644 --- a/cedar-policy-core/src/parser/node.rs +++ b/cedar-policy-core/src/parser/node.rs @@ -25,13 +25,14 @@ use super::loc::Loc; /// Metadata for our syntax trees #[derive(Educe, Debug, Clone, Deserialize, Serialize)] -#[educe(PartialEq, Eq, Hash)] +#[educe(PartialEq, Eq, Hash, Ord, PartialOrd)] pub struct Node { /// Main data represented pub node: T, /// Source location #[educe(PartialEq(ignore))] + #[educe(PartialOrd(ignore))] #[educe(Hash(ignore))] pub loc: Loc, } diff --git a/cedar-policy-validator/src/cedar_schema/ast.rs b/cedar-policy-validator/src/cedar_schema/ast.rs index 6bac99ddc..9279c28a6 100644 --- a/cedar-policy-validator/src/cedar_schema/ast.rs +++ b/cedar-policy-validator/src/cedar_schema/ast.rs @@ -14,10 +14,10 @@ * limitations under the License. */ -use std::iter::once; +use std::{collections::BTreeMap, iter::once}; use cedar_policy_core::{ - ast::{Id, InternalName}, + ast::{Annotation, Annotations, AnyId, Id, InternalName}, parser::{Loc, Node}, }; use itertools::{Either, Itertools}; @@ -29,11 +29,54 @@ use smol_str::ToSmolStr; use crate::json_schema; +use super::err::UserError; + pub const BUILTIN_TYPES: [&str; 3] = ["Long", "String", "Bool"]; pub(super) const CEDAR_NAMESPACE: &str = "__cedar"; -pub type Schema = Vec>; +/// A struct that can be annotated, e.g., entity types. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct Annotated { + /// The struct that's optionally annotated + pub data: T, + /// Annotations + pub annotations: Annotations, +} + +pub type Schema = Vec>; + +pub fn deduplicate_annotations( + data: T, + annotations: Vec, Option>)>>, +) -> Result, UserError> { + let mut unique_annotations: BTreeMap, Option>> = BTreeMap::new(); + for annotation in annotations { + let (key, value) = annotation.node; + if let Some((old_key, _)) = unique_annotations.get_key_value(&key) { + return Err(UserError::DuplicateAnnotations( + key.node, + Node::with_source_loc((), old_key.loc.clone()), + Node::with_source_loc((), key.loc), + )); + } else { + unique_annotations.insert(key, value); + } + } + Ok(Annotated { + data, + annotations: unique_annotations + .into_iter() + .map(|(key, value)| { + let (val, loc) = match value { + Some(n) => (Some(n.node), Some(n.loc)), + None => (None, None), + }; + (key.node, Annotation::with_optional_value(val, loc)) + }) + .collect(), + }) +} /// A path is a non empty list of identifiers that forms a namespace + type #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -169,9 +212,9 @@ impl QualName { #[derive(Debug, Clone)] pub struct Namespace { /// The name of this namespace. If [`None`], then this is the unqualified namespace - pub name: Option>, + pub name: Option, /// The [`Declaration`]s contained in this namespace - pub decls: Vec>, + pub decls: Vec>>, } impl Namespace { @@ -215,7 +258,7 @@ pub struct EntityDecl { /// Entity Types this type is allowed to be related to via the `in` relation pub member_of_types: Vec, /// Attributes this entity has - pub attrs: Vec>, + pub attrs: Vec>>, /// Tag type for this entity (`None` means no tags on this entity) pub tags: Option>, } @@ -228,7 +271,7 @@ pub enum Type { /// A [`Path`] that could either refer to a Common Type or an Entity Type Ident(Path), /// A Record - Record(Vec>), + Record(Vec>>), } /// Primitive Type Definitions @@ -297,7 +340,7 @@ pub enum AppDecl { /// Constraints on the `principal` or `resource` PR(PRAppDecl), /// Constraints on the `context` - Context(Either>>), + Context(Either>>>), } /// An action declaration diff --git a/cedar-policy-validator/src/cedar_schema/err.rs b/cedar-policy-validator/src/cedar_schema/err.rs index bbe7dcc48..394f759f7 100644 --- a/cedar-policy-validator/src/cedar_schema/err.rs +++ b/cedar-policy-validator/src/cedar_schema/err.rs @@ -23,6 +23,7 @@ use std::{ }; use cedar_policy_core::{ + ast::AnyId, impl_diagnostic_from_source_loc_field, impl_diagnostic_from_two_source_loc_fields, impl_diagnostic_from_two_source_loc_opt_fields, parser::{ @@ -43,21 +44,35 @@ use super::ast::PR; #[derive(Debug, Clone, PartialEq, Eq, Error)] pub enum UserError { #[error("An empty list was passed")] - EmptyList, + EmptyList(Node<()>), #[error("Invalid escape codes")] - StringEscape(NonEmpty), + StringEscape(Node>), #[error("`{0}` is a reserved identifier")] - ReservedIdentifierUsed(SmolStr), + ReservedIdentifierUsed(Node), + #[error("duplicate annotations: `{}`", .0)] + DuplicateAnnotations(AnyId, Node<()>, Node<()>), +} + +impl UserError { + // Extract a primary source span locating the error. + pub(crate) fn primary_source_span(&self) -> SourceSpan { + match self { + Self::EmptyList(n) => n.loc.span, + Self::StringEscape(n) => n.loc.span, + Self::ReservedIdentifierUsed(n) => n.loc.span, + // use the first occurrence as the primary source span + Self::DuplicateAnnotations(_, n, _) => n.loc.span, + } + } } pub(crate) type RawLocation = usize; pub(crate) type RawToken<'a> = lalr::lexer::Token<'a>; -pub(crate) type RawUserError = Node; -pub(crate) type RawParseError<'a> = lalr::ParseError, RawUserError>; -pub(crate) type RawErrorRecovery<'a> = lalr::ErrorRecovery, RawUserError>; +pub(crate) type RawParseError<'a> = lalr::ParseError, UserError>; +pub(crate) type RawErrorRecovery<'a> = lalr::ErrorRecovery, UserError>; -type OwnedRawParseError = lalr::ParseError; +type OwnedRawParseError = lalr::ParseError; lazy_static! { static ref SCHEMA_TOKEN_CONFIG: ExpectedTokenConfig = ExpectedTokenConfig { @@ -102,7 +117,7 @@ lazy_static! { #[derive(Clone, Debug, PartialEq, Eq)] pub struct ParseError { /// Error generated by lalrpop - err: OwnedRawParseError, + pub(crate) err: OwnedRawParseError, /// Source code src: Arc, } @@ -133,7 +148,7 @@ impl ParseError { OwnedRawParseError::ExtraToken { token: (token_start, _, token_end), } => SourceSpan::from(*token_start..*token_end), - OwnedRawParseError::User { error } => error.loc.span, + OwnedRawParseError::User { error } => error.primary_source_span(), } } } @@ -152,9 +167,7 @@ impl Display for ParseError { token: (_, token, _), .. } => write!(f, "extra token `{token}`"), - OwnedRawParseError::User { - error: Node { node, .. }, - } => write!(f, "{node}"), + OwnedRawParseError::User { error } => write!(f, "{error}"), } } } @@ -168,27 +181,42 @@ impl Diagnostic for ParseError { fn labels(&self) -> Option + '_>> { let primary_source_span = self.primary_source_span(); - let Self { err, .. } = self; - let labeled_span = match err { - OwnedRawParseError::InvalidToken { .. } => LabeledSpan::underline(primary_source_span), - OwnedRawParseError::UnrecognizedEof { expected, .. } => LabeledSpan::new_with_span( - expected_to_string(expected, &SCHEMA_TOKEN_CONFIG), - primary_source_span, - ), - OwnedRawParseError::UnrecognizedToken { expected, .. } => LabeledSpan::new_with_span( - expected_to_string(expected, &SCHEMA_TOKEN_CONFIG), - primary_source_span, - ), - OwnedRawParseError::ExtraToken { .. } => LabeledSpan::underline(primary_source_span), - OwnedRawParseError::User { .. } => LabeledSpan::underline(primary_source_span), - }; - Some(Box::new(std::iter::once(labeled_span))) + match &self.err { + OwnedRawParseError::InvalidToken { .. } => Some(Box::new(std::iter::once( + LabeledSpan::underline(primary_source_span), + ))), + OwnedRawParseError::UnrecognizedEof { expected, .. } => { + Some(Box::new(std::iter::once(LabeledSpan::new_with_span( + expected_to_string(expected, &SCHEMA_TOKEN_CONFIG), + primary_source_span, + )))) + } + OwnedRawParseError::UnrecognizedToken { expected, .. } => { + Some(Box::new(std::iter::once(LabeledSpan::new_with_span( + expected_to_string(expected, &SCHEMA_TOKEN_CONFIG), + primary_source_span, + )))) + } + OwnedRawParseError::ExtraToken { .. } => Some(Box::new(std::iter::once( + LabeledSpan::underline(primary_source_span), + ))), + OwnedRawParseError::User { + error: UserError::DuplicateAnnotations(_, n1, n2), + } => Some(Box::new( + std::iter::once(n1.loc.span) + .chain(std::iter::once(n2.loc.span)) + .map(LabeledSpan::underline), + )), + OwnedRawParseError::User { .. } => Some(Box::new(std::iter::once( + LabeledSpan::underline(primary_source_span), + ))), + } } } /// Multiple parse errors. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct ParseErrors(Box>); +pub struct ParseErrors(pub(crate) Box>); impl ParseErrors { pub fn new(first: ParseError, tail: impl IntoIterator) -> Self { diff --git a/cedar-policy-validator/src/cedar_schema/fmt.rs b/cedar-policy-validator/src/cedar_schema/fmt.rs index e23546d15..5f20f11e1 100644 --- a/cedar-policy-validator/src/cedar_schema/fmt.rs +++ b/cedar-policy-validator/src/cedar_schema/fmt.rs @@ -32,7 +32,7 @@ impl Display for json_schema::Fragment { for (ns, def) in &self.0 { match ns { None => write!(f, "{def}")?, - Some(ns) => write!(f, "namespace {ns} {{\n{def}}}\n")?, + Some(ns) => write!(f, "{}namespace {ns} {{\n{}}}\n", def.annotations, def)?, } } Ok(()) @@ -42,13 +42,13 @@ impl Display for json_schema::Fragment { impl Display for json_schema::NamespaceDefinition { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { for (n, ty) in &self.common_types { - writeln!(f, "type {n} = {ty};")? + writeln!(f, "{}type {n} = {};", ty.annotations, ty.ty)? } for (n, ty) in &self.entity_types { - writeln!(f, "entity {n}{ty};")? + writeln!(f, "{}entity {n}{};", ty.annotations, ty)? } for (n, a) in &self.actions { - writeln!(f, "action \"{}\"{a};", n.escape_debug())? + writeln!(f, "{}action \"{}\"{};", a.annotations, n.escape_debug(), a)? } Ok(()) } @@ -80,13 +80,14 @@ impl Display for json_schema::RecordType { for (i, (n, ty)) in self.attributes.iter().enumerate() { write!( f, - "\"{}\"{}: {}", + "{}\"{}\"{}: {}", + ty.annotations, n.escape_debug(), if ty.required { "" } else { "?" }, ty.ty )?; if i < (self.attributes.len() - 1) { - write!(f, ", ")?; + writeln!(f, ", ")?; } } write!(f, "}}")?; @@ -242,6 +243,17 @@ mod tests { use crate::cedar_schema::parser::parse_cedar_schema_fragment; + #[track_caller] + fn test_round_trip(src: &str) { + let (cedar_schema, _) = + parse_cedar_schema_fragment(src, Extensions::none()).expect("should parse"); + let printed_cedar_schema = cedar_schema.to_cedarschema().expect("should convert"); + let (parsed_cedar_schema, _) = + parse_cedar_schema_fragment(&printed_cedar_schema, Extensions::none()) + .expect("should parse"); + assert_eq!(cedar_schema, parsed_cedar_schema); + } + #[test] fn rfc_example() { let src = "entity User = { @@ -250,12 +262,41 @@ mod tests { entity Document = { owner: User, } tags Set;"; - let (cedar_schema, _) = - parse_cedar_schema_fragment(src, Extensions::none()).expect("should parse"); - let printed_cedar_schema = cedar_schema.to_cedarschema().expect("should convert"); - let (parsed_cedar_schema, _) = - parse_cedar_schema_fragment(&printed_cedar_schema, Extensions::none()) - .expect("should parse"); - assert_eq!(cedar_schema, parsed_cedar_schema); + test_round_trip(src); + } + + #[test] + fn annotations() { + let src = r#"@doc("this is the namespace") +namespace TinyTodo { + @doc("a common type representing a task") + type Task = { + @doc("task id") + "id": Long, + "name": String, + "state": String, + }; + @doc("a common type representing a set of tasks") + type Tasks = Set; + + @doc("an entity type representing a list") + @docComment("any entity type is a child of type `Application`") + entity List in [Application] = { + @doc("editors of a list") + "editors": Team, + "name": String, + "owner": User, + @doc("readers of a list") + "readers": Team, + "tasks": Tasks, + }; + + @doc("actions that a user can operate on a list") + action DeleteList, GetList, UpdateList appliesTo { + principal: [User], + resource: [List] + }; +}"#; + test_round_trip(src); } } diff --git a/cedar-policy-validator/src/cedar_schema/grammar.lalrpop b/cedar-policy-validator/src/cedar_schema/grammar.lalrpop index 42de91c22..3db5e48aa 100644 --- a/cedar-policy-validator/src/cedar_schema/grammar.lalrpop +++ b/cedar-policy-validator/src/cedar_schema/grammar.lalrpop @@ -16,9 +16,9 @@ use std::str::FromStr; use std::sync::Arc; -use crate::cedar_schema::err::{RawErrorRecovery, RawUserError, UserError}; +use crate::cedar_schema::err::{RawErrorRecovery, UserError}; use cedar_policy_core::parser::{Node, Loc, unescape::to_unescaped_string, cst::Ref}; -use cedar_policy_core::ast::Id; +use cedar_policy_core::ast::{Id, AnyId, Annotations}; use smol_str::SmolStr; use smol_str::ToSmolStr; use crate::cedar_schema::ast::{ @@ -35,9 +35,13 @@ use crate::cedar_schema::ast::{ TypeDecl, PrimitiveType, QualName, - PRAppDecl}; + PRAppDecl, + deduplicate_annotations, + Annotated, +}; use nonempty::{NonEmpty, nonempty}; use itertools::Either; +use std::collections::BTreeMap; use lalrpop_util::{ParseError, ErrorRecovery}; @@ -48,7 +52,7 @@ use lalrpop_util::{ParseError, ErrorRecovery}; grammar<'err, 's>(errors: &'err mut Vec>, src: &'s Arc); extern { - type Error = RawUserError; + type Error = UserError; } match { @@ -82,21 +86,43 @@ match { // other tokens ",", ";", ":", "::", "{", "}", "[", "]", - "<", ">", "=", "?", + "<", ">", "=", "?", "@", "(", ")", } +#[inline] +AnyIdent: Node = { + + // `IDENTIFIER` should produce a valid `AnyId` + => Node::with_source_loc(AnyId::from_str(id).unwrap(), Loc::new(l..r, Arc::clone(src))), +} + +// Annotations := {'@' IDENTIFIER '(' String ')'} +Annotation: Node<(Node, Option>)> = { + "@" ")")?> => Node::with_source_loc((key, value), Loc::new(l..r, Arc::clone(src))) +} + +Annotated: Annotated = { + =>? { + Ok(deduplicate_annotations(e, annotations)?) + }, +} // Schema := {Namespace} pub Schema: ASchema = { => ns, } +#[inline] +Namedspace: Namespace = { + NAMESPACE "{" *> "}" + => Namespace { name: Some(p), decls}, +} + // Namespace := 'namespace' Path '{' {Decl} '}' -Namespace: Node = { - NAMESPACE "{" "}" - => Node::with_source_loc(Namespace { name: Some(Node::with_source_loc(p, Loc::new(l..r, Arc::clone(src)))), decls}, Loc::new(l..r, Arc::clone(src))), - => Node::with_source_loc(Namespace {name: None, decls: vec![decl]}, Loc::new(l..r, Arc::clone(src))), +Namespace: Annotated = { + > => ns, + > => Annotated {data: Namespace {name: None, decls: vec![decl]}, annotations: Annotations::new()}, } // Decl := Entity | Action | TypeDecl @@ -129,7 +155,7 @@ AppDecls: Node>> = { ":" ","? =>? NonEmpty::collect(ets.into_iter()).ok_or(ParseError::User { - error: Node::with_source_loc(UserError::EmptyList, Loc::new(l..r, Arc::clone(src)))}) + error: UserError::EmptyList(Node::with_source_loc((), Loc::new(l..r, Arc::clone(src))))}) .map(|ets| Node::with_source_loc( nonempty![Node::with_source_loc(AppDecl::PR(PRAppDecl { kind:pr, entity_tys: ets}), Loc::new(l..r, Arc::clone(src)))], @@ -137,7 +163,7 @@ AppDecls: Node>> = { ":" "," =>? NonEmpty::collect(ets.into_iter()).ok_or(ParseError::User { - error: Node::with_source_loc(UserError::EmptyList, Loc::new(l..r, Arc::clone(src)))}) + error: UserError::EmptyList(Node::with_source_loc((), Loc::new(l..r, Arc::clone(src))))}) .map(|ets| { let (mut ds, _) = ds.into_inner(); @@ -183,15 +209,14 @@ pub Type: Node = { => Node::with_source_loc(SType::Record(ds.unwrap_or_default()), Loc::new(l..r, Arc::clone(src))), } -// AttrDecls := Name ['?'] ':' Type [',' | ',' AttrDecls] -AttrDecls: Vec> = { - ":" ","? - => vec![Node::with_source_loc(AttrDecl { name, required: required.is_none(), ty}, Loc::new(l..r, Arc::clone(src)))], - ":" "," - => {ds.insert(0, Node::with_source_loc(AttrDecl { name, required: required.is_none(), ty}, Loc::new(l..r, Arc::clone(src)))); ds}, +// AttrDecls := Annotation* Name ['?'] ':' Type [',' | ',' AttrDecls] +AttrDecls: Vec>> = { + ":" ","? + =>? Ok(deduplicate_annotations(AttrDecl { name, required: required.is_none(), ty}, annotations).map(|decl| vec![Node::with_source_loc(decl, Loc::new(l..r, Arc::clone(src)))])?), + ":" "," + =>? {ds.insert(0, deduplicate_annotations(AttrDecl { name, required: required.is_none(), ty}, annotations).map(|decl| Node::with_source_loc(decl, Loc::new(l..r, Arc::clone(src))))?); Ok(ds)}, } - Comma: Vec = { => e.into_iter().collect(), ",")+> => { @@ -232,14 +257,14 @@ Ident: Node = { => Node::with_source_loc("type".parse().unwrap(), Loc::new(l..r, Arc::clone(src))), IN =>? Err(ParseError::User { - error: Node::with_source_loc(UserError::ReservedIdentifierUsed("in".into()), Loc::new(l..r, Arc::clone(src))) + error: UserError::ReservedIdentifierUsed(Node::with_source_loc("in".into(), Loc::new(l..r, Arc::clone(src)))) }), =>? Id::from_str(i) .map(|id : Id| Node::with_source_loc(id, Loc::new(l..r, Arc::clone(src)))) .map_err(|err : cedar_policy_core::parser::err::ParseErrors| ParseError::User { - error: Node::with_source_loc(UserError::ReservedIdentifierUsed(i.to_smolstr()), Loc::new(l..r, Arc::clone(src))) + error: UserError::ReservedIdentifierUsed(Node::with_source_loc(i.to_smolstr(), Loc::new(l..r, Arc::clone(src)))) } ) } @@ -247,7 +272,7 @@ Ident: Node = { STR: Node = { =>? to_unescaped_string(&s[1..(s.len() - 1)]).map_or_else(|e| Err(ParseError::User { - error: Node::with_source_loc(UserError::StringEscape(e), Loc::new(l..r, Arc::clone(src))), + error: UserError::StringEscape(Node::with_source_loc(e, Loc::new(l..r, Arc::clone(src)))), }), |v| Ok(Node::with_source_loc(v, Loc::new(l..r, Arc::clone(src))))), } diff --git a/cedar-policy-validator/src/cedar_schema/test.rs b/cedar-policy-validator/src/cedar_schema/test.rs index 158a602ba..bf72a6abb 100644 --- a/cedar-policy-validator/src/cedar_schema/test.rs +++ b/cedar-policy-validator/src/cedar_schema/test.rs @@ -23,8 +23,8 @@ mod demo_tests { iter::{empty, once}, }; - use cedar_policy_core::extensions::Extensions; use cedar_policy_core::test_utils::{expect_err, ExpectedErrorMessageBuilder}; + use cedar_policy_core::{est::Annotations, extensions::Extensions}; use cool_asserts::assert_matches; use smol_str::ToSmolStr; @@ -213,7 +213,7 @@ mod demo_tests { json_schema::Fragment::from_cedarschema_str(src, Extensions::all_available()).unwrap(); let unqual = schema.0.get(&None).unwrap(); let foo = unqual.actions.get("Foo").unwrap(); - assert_matches!(foo, + assert_matches!(&foo, json_schema::ActionType { applies_to : Some(json_schema::ApplySpec { resource_types, @@ -336,6 +336,7 @@ mod demo_tests { attributes: None, applies_to: None, member_of: None, + annotations: Annotations::new(), }; let namespace = json_schema::NamespaceDefinition::new(empty(), once(("foo".to_smolstr(), action))); @@ -437,6 +438,7 @@ namespace Baz {action "Foo" appliesTo { member_of_types: vec![], shape: json_schema::AttributesOrContext::default(), tags: None, + annotations: Annotations::new(), }, )]), actions: BTreeMap::from([( @@ -449,8 +451,10 @@ namespace Baz {action "Foo" appliesTo { context: json_schema::AttributesOrContext::default(), }), member_of: None, + annotations: Annotations::new(), }, )]), + annotations: Annotations::new(), }; let fragment = json_schema::Fragment(BTreeMap::from([(None, namespace)])); let src = fragment.to_cedarschema().unwrap(); @@ -544,7 +548,7 @@ namespace Baz {action "Foo" appliesTo { &vec!["UserGroup".parse().unwrap(), "Team".parse().unwrap()] ); // UserGroup - let usergroup = github + let usergroup = &github .entity_types .get(&"UserGroup".parse().unwrap()) .expect("No `UserGroup`"); @@ -733,7 +737,7 @@ namespace Baz {action "Foo" appliesTo { }), ); assert_has_type( - attributes.get("viewACL").unwrap(), + &attributes.get("viewACL").unwrap(), json_schema::Type::Type(json_schema::TypeVariant::EntityOrCommon { type_name: "DocumentShare".parse().unwrap(), }), @@ -878,7 +882,7 @@ namespace Baz {action "Foo" appliesTo { json_schema::Fragment::from_cedarschema_str(src, Extensions::all_available()).unwrap(); assert_eq!(warnings.collect::>(), vec![]); let service = fragment.0.get(&Some("Service".parse().unwrap())).unwrap(); - let resource = service + let resource = &service .entity_types .get(&"Resource".parse().unwrap()) .unwrap(); @@ -886,8 +890,8 @@ namespace Baz {action "Foo" appliesTo { attributes, additional_attributes: false, }))) => { - assert_matches!(attributes.get("tag"), Some(json_schema::TypeOfAttribute { ty, required: true }) => { - assert_matches!(ty, json_schema::Type::Type(json_schema::TypeVariant::EntityOrCommon { type_name }) => { + assert_matches!(attributes.get("tag"), Some(json_schema::TypeOfAttribute { ty, required: true, .. }) => { + assert_matches!(&ty, json_schema::Type::Type(json_schema::TypeVariant::EntityOrCommon { type_name }) => { assert_eq!(type_name, &"AWS::Tag".parse().unwrap()); }); }); @@ -917,7 +921,7 @@ namespace Baz {action "Foo" appliesTo { assert_labeled_span("type t =", "expected `{`, identifier, or `Set`"); assert_labeled_span( "entity User {", - "expected `}`, identifier, or string literal", + "expected `@`, `}`, identifier, or string literal", ); assert_labeled_span("entity User { name:", "expected `{`, identifier, or `Set`"); } @@ -1417,18 +1421,18 @@ mod translator_tests { let (frag, _) = json_schema::Fragment::from_cedarschema_str(src, Extensions::all_available()).unwrap(); let demo = frag.0.get(&Some("Demo".parse().unwrap())).unwrap(); - let user = demo.entity_types.get(&"User".parse().unwrap()).unwrap(); + let user = &demo.entity_types.get(&"User".parse().unwrap()).unwrap(); assert_matches!(&user.shape, json_schema::AttributesOrContext(json_schema::Type::Type(json_schema::TypeVariant::Record(json_schema::RecordType { attributes, additional_attributes: false, }))) => { - assert_matches!(attributes.get("name"), Some(json_schema::TypeOfAttribute { ty, required: true }) => { + assert_matches!(attributes.get("name"), Some(json_schema::TypeOfAttribute { ty, required: true, .. }) => { let expected = json_schema::Type::Type(json_schema::TypeVariant::EntityOrCommon { type_name: "id".parse().unwrap(), }); assert_eq!(ty, &expected); }); - assert_matches!(attributes.get("email"), Some(json_schema::TypeOfAttribute { ty, required: true }) => { + assert_matches!(attributes.get("email"), Some(json_schema::TypeOfAttribute { ty, required: true, .. }) => { let expected = json_schema::Type::Type(json_schema::TypeVariant::EntityOrCommon { type_name: "email_address".parse().unwrap(), }); @@ -1984,6 +1988,266 @@ mod translator_tests { .map(|_| ()); assert_matches!(schema, Err(errs) if matches!(errs.iter().next().unwrap(), ToJsonSchemaError::ReservedSchemaKeyword(_))); } + + #[track_caller] + fn test_translation(src: &str, json_value: serde_json::Value) { + let (schema, _) = cedar_schema_to_json_schema( + parse_schema(src).expect("should parse Cedar schema"), + Extensions::none(), + ) + .expect("should translate to JSON schema"); + assert_eq!(serde_json::to_value(schema).unwrap(), json_value); + } + #[test] + fn annotations() { + // namespace annotations + test_translation( + r#" + @a1 + @a2("") + @a3("foo") + namespace N { + entity E; + } + "#, + serde_json::json!({ + "N": { + "entityTypes": { + "E": {} + }, + "actions": {}, + "annotations": { + "a1": "", + "a2": "", + "a3": "foo", + } + } + }), + ); + + // common type annotations + test_translation( + r#" + @comment("A->B") + type A = B; + @comment("B->A") + type B = A; + "#, + serde_json::json!({ + "": { + "entityTypes": {}, + "actions": {}, + "commonTypes": { + "A": { + "type": "EntityOrCommon", + "name": "B", + "annotations": { + "comment": "A->B", + } + }, + "B": { + "type": "EntityOrCommon", + "name": "A", + "annotations": { + "comment": "B->A", + } + } + } + } + }), + ); + + // entity type annotations + test_translation( + r#" + @a1 + @a2("") + @a3("foo") + namespace N { + @ae1("๐ŸŒ•") + @ae2("moon") + entity Moon; + } + @ae("๐ŸŒŽ") + entity Earth; + "#, + serde_json::json!({ + "": { + "entityTypes": { + "Earth": { + "annotations": { + "ae": "๐ŸŒŽ", + } + } + }, + "actions": {}, + }, + "N": { + "entityTypes": { + "Moon": { + "annotations": { + "ae1": "๐ŸŒ•", + "ae2": "moon", + } + } + }, + "actions": {}, + "annotations": { + "a1": "", + "a2": "", + "a3": "foo", + } + } + }), + ); + test_translation( + r#" + @ae("๐ŸŽ๐Ÿ") + entity Apple1, Apple2; + "#, + serde_json::json!({ + "": { + "entityTypes": { + "Apple1": { + "annotations": { + "ae": "๐ŸŽ๐Ÿ", + } + }, + "Apple2": { + "annotations": { + "ae": "๐ŸŽ๐Ÿ", + } + } + }, + "actions": {}, + } + }), + ); + + // action annotations + test_translation( + r#" + @a1 + @a2("") + @a3("foo") + namespace N { + @ae1("๐ŸŒ•") + @ae2("moon") + entity Moon; + } + @a("๐ŸŒŒ") + action "๐Ÿš€","๐Ÿ›ธ" appliesTo { + principal: [Astronaut, ET], + resource: Earth, + }; + "#, + serde_json::json!({ + "": { + "entityTypes": {}, + "actions": { + "๐Ÿš€": { + "annotations": { + "a": "๐ŸŒŒ", + }, + "appliesTo": { + "principalTypes": ["Astronaut", "ET"], + "resourceTypes": ["Earth"], + } + }, + "๐Ÿ›ธ": { + "annotations": { + "a": "๐ŸŒŒ", + }, + "appliesTo": { + "principalTypes": ["Astronaut", "ET"], + "resourceTypes": ["Earth"], + } + } + }, + }, + "N": { + "entityTypes": { + "Moon": { + "annotations": { + "ae1": "๐ŸŒ•", + "ae2": "moon", + } + } + }, + "actions": {}, + "annotations": { + "a1": "", + "a2": "", + "a3": "foo", + } + } + }), + ); + + // attribute annotations + test_translation( + r#" + type Stars = { + @a1 + "๐ŸŒ•": Long, + @a2 + "๐ŸŒŽ": Long, + @a3 + "๐Ÿ›ฐ๏ธ": { + @a4("Rocket") + "๐Ÿš€": Long, + "๐ŸŒŒ": Long, + } + }; + "#, + serde_json::json!({ + "": { + "entityTypes": {}, + "actions": {}, + "commonTypes": { + "Stars": { + "type": "Record", + "attributes": { + "๐ŸŒ•": { + "type": "EntityOrCommon", + "name": "Long", + "annotations": { + "a1": "", + } + }, + "๐ŸŒŽ": { + "type": "EntityOrCommon", + "name": "Long", + "annotations": { + "a2": "", + } + }, + "๐Ÿ›ฐ๏ธ": { + "type": "Record", + "annotations": { + "a3": "", + }, + "attributes": { + "๐Ÿš€": { + "type": "EntityOrCommon", + "name": "Long", + "annotations": { + "a4": "Rocket", + } + }, + "๐ŸŒŒ": { + "type": "EntityOrCommon", + "name": "Long", + } + } + }, + }, + }, + } + } + }), + ); + } } #[cfg(test)] @@ -2340,7 +2604,7 @@ mod entity_tags { assert!(warnings.is_empty()); let entity_type = frag.0.get(&None).unwrap().entity_types.get(&"E".parse().unwrap()).unwrap(); assert_matches!(&entity_type.tags, Some(json_schema::Type::Type(json_schema::TypeVariant::Record(rty))) => { - assert_matches!(rty.attributes.get("foo"), Some(json_schema::TypeOfAttribute { ty, required }) => { + assert_matches!(rty.attributes.get("foo"), Some(json_schema::TypeOfAttribute { ty, required, .. }) => { assert_matches!(ty, json_schema::Type::Type(json_schema::TypeVariant::EntityOrCommon { type_name }) => { assert_eq!(&format!("{type_name}"), "String"); }); @@ -2368,3 +2632,135 @@ mod entity_tags { }); } } + +// RFC 48 test cases +#[cfg(test)] +mod annotations { + use cool_asserts::assert_matches; + + use crate::cedar_schema::parser::parse_schema; + + #[test] + fn no_keys() { + assert_matches!( + parse_schema( + r#" + @doc("This entity defines our central user type") +entity User { + @manager + manager : User, + @team + team : String +}; + "# + ), + Ok(_) + ); + } + + #[test] + fn duplicate_keys() { + assert_matches!( + parse_schema( + r#" + @doc("This entity defines our central user type") + @doc +entity User { + @manager + manager : User, + @team + team : String +}; + "# + ), + Err(errs) => { + assert_eq!(errs.0.as_ref().first().to_string(), "duplicate annotations: `doc`"); + } + ); + } + + #[test] + fn rfc_examples() { + // basic + assert_matches!( + parse_schema( + r#" + @doc("This entity defines our central user type") +entity User { + manager : User, + team : String +}; + "# + ), + Ok(_) + ); + // basic + namespace + assert_matches!( + parse_schema( + r#" + @doc("this is namespace foo") + namespace foo { +@doc("This entity defines our central user type") +entity User { + manager : User, + team : String +}; + } + "# + ), + Ok(_) + ); + // entity attribute annotation + assert_matches!( + parse_schema( + r#" +@doc("This entity defines our central user type") +entity User { + manager : User, + + @doc("Which team user belongs to") + @docLink("https://schemaDocs.example.com/User/team") + team : String +}; +"# + ), + Ok(_) + ); + // full example + assert_matches!( + parse_schema( + r#" + @doc("this is the namespace") +namespace TinyTodo { + @doc("a common type representing a task") + type Task = { + "id": Long, + "name": String, + "state": String, + }; + @doc("a common type representing a set of tasks") + type Tasks = Set; + + @doc1("an entity type representing a list") + @doc2("any entity type is a child of type `Application`") + entity List in [Application] = { + @doc("editors of a list") + "editors": Team, + "name": String, + "owner": User, + @doc("readers of a list") + "readers": Team, + "tasks": Tasks, + }; + + @doc("actions that a user can operate on a list") + action DeleteList, GetList, UpdateList appliesTo { + principal: [User], + resource: [List] + }; +}"# + ), + Ok(_) + ); + } +} diff --git a/cedar-policy-validator/src/cedar_schema/to_json_schema.rs b/cedar-policy-validator/src/cedar_schema/to_json_schema.rs index 0d7ff29df..f696c2f98 100644 --- a/cedar-policy-validator/src/cedar_schema/to_json_schema.rs +++ b/cedar-policy-validator/src/cedar_schema/to_json_schema.rs @@ -19,7 +19,7 @@ use std::collections::HashMap; use cedar_policy_core::{ - ast::{Id, Name, UnreservedId}, + ast::{Annotations, Id, Name, UnreservedId}, extensions::Extensions, parser::{Loc, Node}, }; @@ -30,12 +30,16 @@ use std::collections::hash_map::Entry; use super::{ ast::{ - ActionDecl, AppDecl, AttrDecl, Decl, Declaration, EntityDecl, Namespace, PRAppDecl, Path, - QualName, Schema, Type, TypeDecl, BUILTIN_TYPES, PR, + ActionDecl, Annotated, AppDecl, AttrDecl, Decl, Declaration, EntityDecl, Namespace, + PRAppDecl, Path, QualName, Schema, Type, TypeDecl, BUILTIN_TYPES, PR, }, err::{schema_warnings, SchemaWarning, ToJsonSchemaError, ToJsonSchemaErrors}, }; -use crate::{cedar_schema, json_schema, RawName}; +use crate::{ + cedar_schema, + json_schema::{self, CommonType}, + RawName, +}; impl From for RawName { fn from(p: cedar_schema::Path) -> Self { @@ -69,14 +73,13 @@ pub fn cedar_schema_to_json_schema( // namespaces with matching non-empty names, so that all definitions from // that namespace make it into the JSON schema structure under that // namespace's key. - let (qualified_namespaces, unqualified_namespace) = - split_unqualified_namespace(schema.into_iter().map(|n| n.node)); + let (qualified_namespaces, unqualified_namespace) = split_unqualified_namespace(schema); // Create a single iterator for all namespaces let all_namespaces = qualified_namespaces .chain(unqualified_namespace) .collect::>(); - let names = build_namespace_bindings(all_namespaces.iter())?; + let names = build_namespace_bindings(all_namespaces.iter().map(|ns| &ns.data))?; let warnings = compute_namespace_warnings(&names, extensions); let fragment = collect_all_errors(all_namespaces.into_iter().map(convert_namespace))?.collect(); Ok(( @@ -117,16 +120,19 @@ pub fn cedar_type_to_json_type(ty: Node) -> json_schema::Type { // Split namespaces into two groups: named namespaces and the implicit unqualified namespace // The rhs of the tuple will be [`None`] if there are no items in the unqualified namespace. fn split_unqualified_namespace( - namespaces: impl IntoIterator, -) -> (impl Iterator, Option) { + namespaces: impl IntoIterator>, +) -> ( + impl Iterator>, + Option>, +) { // First split every namespace into those with explicit names and those without let (qualified, unqualified): (Vec<_>, Vec<_>) = - namespaces.into_iter().partition(|n| n.name.is_some()); + namespaces.into_iter().partition(|n| n.data.name.is_some()); // Now combine all the decls in namespaces without names into one unqualified namespace let mut unqualified_decls = vec![]; for mut unqualified_namespace in unqualified.into_iter() { - unqualified_decls.append(&mut unqualified_namespace.decls); + unqualified_decls.append(&mut unqualified_namespace.data.decls); } if unqualified_decls.is_empty() { @@ -136,33 +142,42 @@ fn split_unqualified_namespace( name: None, decls: unqualified_decls, }; - (qualified.into_iter(), Some(unqual)) + ( + qualified.into_iter(), + Some(Annotated { + data: unqual, + annotations: Annotations::new(), + }), + ) } } /// Converts a CST namespace to a JSON namespace fn convert_namespace( - namespace: Namespace, + namespace: Annotated, ) -> Result<(Option, json_schema::NamespaceDefinition), ToJsonSchemaErrors> { let ns_name = namespace + .data .name .clone() .map(|p| { - let internal_name = RawName::from(p.node).qualify_with(None); // namespace names are always written already-fully-qualified in the Cedar schema syntax + let internal_name = RawName::from(p.clone()).qualify_with(None); // namespace names are always written already-fully-qualified in the Cedar schema syntax Name::try_from(internal_name) - .map_err(|e| ToJsonSchemaError::reserved_name(e.name(), p.loc)) + .map_err(|e| ToJsonSchemaError::reserved_name(e.name(), p.loc().clone())) }) .transpose()?; let def = namespace.try_into()?; Ok((ns_name, def)) } -impl TryFrom for json_schema::NamespaceDefinition { +impl TryFrom> for json_schema::NamespaceDefinition { type Error = ToJsonSchemaErrors; - fn try_from(n: Namespace) -> Result, Self::Error> { + fn try_from( + n: Annotated, + ) -> Result, Self::Error> { // Partition the decls into entities, actions, and common types - let (entity_types, action, common_types) = into_partition_decls(n.decls); + let (entity_types, action, common_types) = into_partition_decls(n.data.decls); // Convert entity type decls, collecting all errors let entity_types = collect_all_errors(entity_types.into_iter().map(convert_entity_decl))? @@ -178,12 +193,18 @@ impl TryFrom for json_schema::NamespaceDefinition { let common_types = common_types .into_iter() .map(|decl| { - let name_loc = decl.name.loc.clone(); - let id = UnreservedId::try_from(decl.name.node) + let name_loc = decl.data.name.loc.clone(); + let id = UnreservedId::try_from(decl.data.name.node) .map_err(|e| ToJsonSchemaError::reserved_name(e.name(), name_loc.clone()))?; let ctid = json_schema::CommonTypeId::new(id) .map_err(|e| ToJsonSchemaError::reserved_keyword(e.id, name_loc))?; - Ok((ctid, cedar_type_to_json_type(decl.def))) + Ok(( + ctid, + CommonType { + ty: cedar_type_to_json_type(decl.data.def), + annotations: decl.annotations.into(), + }, + )) }) .collect::>()?; @@ -191,19 +212,20 @@ impl TryFrom for json_schema::NamespaceDefinition { common_types, entity_types, actions, + annotations: n.annotations.into(), }) } } /// Converts action type decls fn convert_action_decl( - a: ActionDecl, + a: Annotated, ) -> Result)>, ToJsonSchemaErrors> { let ActionDecl { names, parents, app_decls, - } = a; + } = a.data; // Create the internal type from the 'applies_to' clause and 'member_of' let applies_to = app_decls .map(|decls| convert_app_decls(&names.first().node, &names.first().loc, decls)) @@ -218,6 +240,7 @@ fn convert_action_decl( attributes: None, // Action attributes are currently unsupported in the Cedar schema format applies_to: Some(applies_to), member_of, + annotations: a.annotations.into(), }; // Then map that type across all of the bound names Ok(names.into_iter().map(move |name| (name.node, ty.clone()))) @@ -336,21 +359,28 @@ fn convert_id(node: Node) -> Result { /// Convert Entity declarations fn convert_entity_decl( - e: EntityDecl, + e: Annotated, ) -> Result< impl Iterator)>, ToJsonSchemaErrors, > { // First build up the defined entity type let etype = json_schema::EntityType { - member_of_types: e.member_of_types.into_iter().map(RawName::from).collect(), - shape: convert_attr_decls(e.attrs), - tags: e.tags.map(cedar_type_to_json_type), + member_of_types: e + .data + .member_of_types + .into_iter() + .map(RawName::from) + .collect(), + shape: convert_attr_decls(e.data.attrs), + tags: e.data.tags.map(cedar_type_to_json_type), + annotations: e.annotations.into(), }; // Then map over all of the bound names collect_all_errors( - e.names + e.data + .names .into_iter() .map(move |name| -> Result<_, ToJsonSchemaErrors> { Ok((convert_id(name)?, etype.clone())) @@ -360,7 +390,7 @@ fn convert_entity_decl( /// Create a [`json_schema::AttributesOrContext`] from a series of `AttrDecl`s fn convert_attr_decls( - attrs: impl IntoIterator>, + attrs: impl IntoIterator>>, ) -> json_schema::AttributesOrContext { json_schema::RecordType { attributes: attrs @@ -374,7 +404,7 @@ fn convert_attr_decls( /// Create a context decl fn convert_context_decl( - decl: Either>>, + decl: Either>>>, ) -> json_schema::AttributesOrContext { json_schema::AttributesOrContext(match decl { Either::Left(p) => json_schema::Type::CommonTypeRef { @@ -393,12 +423,15 @@ fn convert_context_decl( } /// Convert an attribute type from an `AttrDecl` -fn convert_attr_decl(attr: AttrDecl) -> (SmolStr, json_schema::TypeOfAttribute) { +fn convert_attr_decl( + attr: Annotated, +) -> (SmolStr, json_schema::TypeOfAttribute) { ( - attr.name.node, + attr.data.name.node, json_schema::TypeOfAttribute { - ty: cedar_type_to_json_type(attr.ty), - required: attr.required, + ty: cedar_type_to_json_type(attr.data.ty), + required: attr.data.required, + annotations: attr.annotations.into(), }, ) } @@ -444,9 +477,9 @@ impl NamespaceRecord { .name .clone() .map(|n| { - let internal_name = RawName::from(n.node).qualify_with(None); // namespace names are already fully-qualified + let internal_name = RawName::from(n.clone()).qualify_with(None); // namespace names are already fully-qualified Name::try_from(internal_name) - .map_err(|e| ToJsonSchemaError::reserved_name(e.name(), n.loc)) + .map_err(|e| ToJsonSchemaError::reserved_name(e.name(), n.loc().clone())) }) .transpose()?; let (entities, actions, types) = partition_decls(&namespace.decls); @@ -474,7 +507,7 @@ impl NamespaceRecord { let record = NamespaceRecord { entities, common_types, - loc: namespace.name.as_ref().map(|n| n.loc.clone()), + loc: namespace.name.as_ref().map(|n| n.loc().clone()), }; Ok((ns, record)) @@ -594,14 +627,14 @@ fn update_namespace_record( } fn partition_decls( - decls: &[Node], + decls: &[Annotated>], ) -> (Vec<&EntityDecl>, Vec<&ActionDecl>, Vec<&TypeDecl>) { let mut entities = vec![]; let mut actions = vec![]; let mut types = vec![]; for decl in decls.iter() { - match &decl.node { + match &decl.data.node { Declaration::Entity(e) => entities.push(e), Declaration::Action(a) => actions.push(a), Declaration::Type(t) => types.push(t), @@ -612,17 +645,30 @@ fn partition_decls( } fn into_partition_decls( - decls: Vec>, -) -> (Vec, Vec, Vec) { + decls: Vec>>, +) -> ( + Vec>, + Vec>, + Vec>, +) { let mut entities = vec![]; let mut actions = vec![]; let mut types = vec![]; for decl in decls.into_iter() { - match decl.node { - Declaration::Entity(e) => entities.push(e), - Declaration::Action(a) => actions.push(a), - Declaration::Type(t) => types.push(t), + match decl.data.node { + Declaration::Entity(e) => entities.push(Annotated { + data: e, + annotations: decl.annotations, + }), + Declaration::Action(a) => actions.push(Annotated { + data: a, + annotations: decl.annotations, + }), + Declaration::Type(t) => types.push(Annotated { + data: t, + annotations: decl.annotations, + }), } } diff --git a/cedar-policy-validator/src/json_schema.rs b/cedar-policy-validator/src/json_schema.rs index cb4c20e67..f51b2f6a7 100644 --- a/cedar-policy-validator/src/json_schema.rs +++ b/cedar-policy-validator/src/json_schema.rs @@ -19,6 +19,7 @@ use cedar_policy_core::{ ast::{Eid, EntityUID, InternalName, Name, UnreservedId}, entities::CedarValueJson, + est::Annotations, extensions::Extensions, FromNormalizedStr, }; @@ -46,6 +47,19 @@ use crate::{ AllDefs, CedarSchemaError, CedarSchemaParseError, ConditionalName, RawName, ReferenceType, }; +/// Represents the definition of a common type in the schema. +#[derive(Debug, Clone, Serialize, PartialEq, Eq, Deserialize)] +#[serde(bound(deserialize = "N: Deserialize<'de> + From"))] +pub struct CommonType { + /// The referred type + #[serde(flatten)] + pub ty: Type, + /// Annotations + #[serde(default)] + #[serde(skip_serializing_if = "Annotations::is_empty")] + pub annotations: Annotations, +} + /// A [`Fragment`] is split into multiple namespace definitions, and is just a /// map from namespace name to namespace definition (i.e., definitions of common /// types, entity types, and actions in that namespace). @@ -293,11 +307,15 @@ pub struct NamespaceDefinition { #[serde(default)] #[serde(skip_serializing_if = "BTreeMap::is_empty")] #[serde(with = "::serde_with::rust::maps_duplicate_key_is_error")] - pub common_types: BTreeMap>, + pub common_types: BTreeMap>, #[serde(with = "::serde_with::rust::maps_duplicate_key_is_error")] pub entity_types: BTreeMap>, #[serde(with = "::serde_with::rust::maps_duplicate_key_is_error")] pub actions: BTreeMap>, + /// Annotations + #[serde(default)] + #[serde(skip_serializing_if = "Annotations::is_empty")] + pub annotations: Annotations, } impl NamespaceDefinition { @@ -309,6 +327,7 @@ impl NamespaceDefinition { common_types: BTreeMap::new(), entity_types: entity_types.into_iter().collect(), actions: actions.into_iter().collect(), + annotations: Annotations::new(), } } } @@ -323,7 +342,15 @@ impl NamespaceDefinition { common_types: self .common_types .into_iter() - .map(|(k, v)| (k, v.conditionally_qualify_type_references(ns))) + .map(|(k, v)| { + ( + k, + CommonType { + ty: v.ty.conditionally_qualify_type_references(ns), + annotations: v.annotations, + }, + ) + }) .collect(), entity_types: self .entity_types @@ -335,6 +362,7 @@ impl NamespaceDefinition { .into_iter() .map(|(k, v)| (k, v.conditionally_qualify_type_references(ns))) .collect(), + annotations: self.annotations, } } } @@ -354,7 +382,15 @@ impl NamespaceDefinition { common_types: self .common_types .into_iter() - .map(|(k, v)| Ok((k, v.fully_qualify_type_references(all_defs)?))) + .map(|(k, v)| { + Ok(( + k, + CommonType { + ty: v.ty.fully_qualify_type_references(all_defs)?, + annotations: v.annotations, + }, + )) + }) .collect::>()?, entity_types: self .entity_types @@ -366,6 +402,7 @@ impl NamespaceDefinition { .into_iter() .map(|(k, v)| Ok((k, v.fully_qualify_type_references(all_defs)?))) .collect::>()?, + annotations: self.annotations, }) } } @@ -398,6 +435,10 @@ pub struct EntityType { #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub tags: Option>, + /// Annotations + #[serde(default)] + #[serde(skip_serializing_if = "Annotations::is_empty")] + pub annotations: Annotations, } impl EntityType { @@ -416,6 +457,7 @@ impl EntityType { tags: self .tags .map(|ty| ty.conditionally_qualify_type_references(ns)), + annotations: self.annotations, } } } @@ -442,6 +484,7 @@ impl EntityType { .tags .map(|ty| ty.fully_qualify_type_references(all_defs)) .transpose()?, + annotations: self.annotations, }) } } @@ -548,6 +591,10 @@ pub struct ActionType { #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] pub member_of: Option>>, + /// Annotations + #[serde(default)] + #[serde(skip_serializing_if = "Annotations::is_empty")] + pub annotations: Annotations, } impl ActionType { @@ -566,6 +613,7 @@ impl ActionType { .map(|aeuid| aeuid.conditionally_qualify_type_references(ns)) .collect() }), + annotations: self.annotations, } } } @@ -595,6 +643,7 @@ impl ActionType { .collect::>() }) .transpose()?, + annotations: self.annotations, }) } } @@ -1220,15 +1269,26 @@ impl<'de, N: Deserialize<'de> + From> TypeVisitor { attributes: attributes .0 .into_iter() - .map(|(k, TypeOfAttribute { ty, required })| { - ( + .map( + |( k, TypeOfAttribute { - ty: ty.into_n(), + ty, required, + annotations, }, - ) - }) + )| { + ( + k, + TypeOfAttribute { + ty: ty.into_n(), + + required, + annotations, + }, + ) + }, + ) .collect(), additional_attributes, }))) @@ -1479,12 +1539,21 @@ impl TypeVariant { additional_attributes, }) => TypeVariant::Record(RecordType { attributes: BTreeMap::from_iter(attributes.into_iter().map( - |(attr, TypeOfAttribute { ty, required })| { + |( + attr, + TypeOfAttribute { + ty, + required, + annotations, + }, + )| { ( attr, TypeOfAttribute { ty: ty.conditionally_qualify_type_references(ns), + required, + annotations, }, ) }, @@ -1552,15 +1621,25 @@ impl TypeVariant { }) => Ok(TypeVariant::Record(RecordType { attributes: attributes .into_iter() - .map(|(attr, TypeOfAttribute { ty, required })| { - Ok(( + .map( + |( attr, TypeOfAttribute { - ty: ty.fully_qualify_type_references(all_defs)?, + ty, required, + annotations, }, - )) - }) + )| { + Ok(( + attr, + TypeOfAttribute { + ty: ty.fully_qualify_type_references(all_defs)?, + required, + annotations, + }, + )) + }, + ) .collect::, TypeNotDefinedError>>()?, additional_attributes, })), @@ -1592,7 +1671,12 @@ impl<'a> arbitrary::Arbitrary<'a> for Type { let attr_names: BTreeSet = u.arbitrary()?; attr_names .into_iter() - .map(|attr_name| Ok((attr_name.into(), u.arbitrary()?))) + .map(|attr_name| { + Ok(( + attr_name.into(), + u.arbitrary::>()?.into(), + )) + }) .collect::>()? }; TypeVariant::Record(RecordType { @@ -1645,6 +1729,10 @@ pub struct TypeOfAttribute { /// Underlying type of the attribute #[serde(flatten)] pub ty: Type, + /// Annotations + #[serde(default)] + #[serde(skip_serializing_if = "Annotations::is_empty")] + pub annotations: Annotations, /// Whether the attribute is required #[serde(default = "record_attribute_required_default")] #[serde(skip_serializing_if = "is_record_attribute_required_default")] @@ -1655,7 +1743,9 @@ impl TypeOfAttribute { fn into_n>(self) -> TypeOfAttribute { TypeOfAttribute { ty: self.ty.into_n(), + required: self.required, + annotations: self.annotations, } } @@ -1667,6 +1757,7 @@ impl TypeOfAttribute { TypeOfAttribute { ty: self.ty.conditionally_qualify_type_references(ns), required: self.required, + annotations: self.annotations, } } } @@ -1685,6 +1776,7 @@ impl TypeOfAttribute { Ok(TypeOfAttribute { ty: self.ty.fully_qualify_type_references(all_defs)?, required: self.required, + annotations: self.annotations, }) } } @@ -1693,8 +1785,9 @@ impl TypeOfAttribute { impl<'a> arbitrary::Arbitrary<'a> for TypeOfAttribute { fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { Ok(Self { - ty: u.arbitrary()?, + ty: u.arbitrary::>()?, required: u.arbitrary()?, + annotations: cedar_policy_core::est::Annotations::new(), }) } @@ -2136,7 +2229,7 @@ mod test { expect_err( src, &miette::Report::new(e), - &ExpectedErrorMessageBuilder::error(r#"unknown field `User`, expected one of `commonTypes`, `entityTypes`, `actions` at line 3 column 35"#) + &ExpectedErrorMessageBuilder::error(r#"unknown field `User`, expected one of `commonTypes`, `entityTypes`, `actions`, `annotations` at line 3 column 35"#) .help("JSON formatted schema must specify a namespace. If you want to use the empty namespace, explicitly specify it with `{ \"\": {..} }`") .build()); }); @@ -2561,11 +2654,11 @@ mod entity_tags { fn basic() { let json = example_json_schema(); assert_matches!(Fragment::from_json_value(json), Ok(frag) => { - let user = frag.0.get(&None).unwrap().entity_types.get(&"User".parse().unwrap()).unwrap(); + let user = &frag.0.get(&None).unwrap().entity_types.get(&"User".parse().unwrap()).unwrap(); assert_matches!(&user.tags, Some(Type::Type(TypeVariant::Set { element })) => { assert_matches!(&**element, Type::Type(TypeVariant::String)); // TODO: why is this `TypeVariant::String` in this case but `EntityOrCommon { "String" }` in all the other cases in this test? Do we accept common types as the element type for sets? }); - let doc = frag.0.get(&None).unwrap().entity_types.get(&"Document".parse().unwrap()).unwrap(); + let doc = &frag.0.get(&None).unwrap().entity_types.get(&"Document".parse().unwrap()).unwrap(); assert_matches!(&doc.tags, Some(Type::Type(TypeVariant::Set { element })) => { assert_matches!(&**element, Type::Type(TypeVariant::String)); // TODO: why is this `TypeVariant::String` in this case but `EntityOrCommon { "String" }` in all the other cases in this test? Do we accept common types as the element type for sets? }); @@ -2595,7 +2688,7 @@ mod entity_tags { "actions": {} }}); assert_matches!(Fragment::from_json_value(json), Ok(frag) => { - let user = frag.0.get(&None).unwrap().entity_types.get(&"User".parse().unwrap()).unwrap(); + let user = &frag.0.get(&None).unwrap().entity_types.get(&"User".parse().unwrap()).unwrap(); assert_matches!(&user.tags, Some(Type::CommonTypeRef { type_name }) => { assert_eq!(&format!("{type_name}"), "T"); }); @@ -2622,7 +2715,7 @@ mod entity_tags { "actions": {} }}); assert_matches!(Fragment::from_json_value(json), Ok(frag) => { - let user = frag.0.get(&None).unwrap().entity_types.get(&"User".parse().unwrap()).unwrap(); + let user = &frag.0.get(&None).unwrap().entity_types.get(&"User".parse().unwrap()).unwrap(); assert_matches!(&user.tags, Some(Type::Type(TypeVariant::Entity{ name })) => { assert_eq!(&format!("{name}"), "User"); }); @@ -2679,7 +2772,9 @@ mod test_json_roundtrip { common_types: BTreeMap::new(), entity_types: BTreeMap::new(), actions: BTreeMap::new(), - }, + annotations: Annotations::new(), + } + .into(), )])); roundtrip(fragment); } @@ -2692,7 +2787,9 @@ mod test_json_roundtrip { common_types: BTreeMap::new(), entity_types: BTreeMap::new(), actions: BTreeMap::new(), - }, + annotations: Annotations::new(), + } + .into(), )])); roundtrip(fragment); } @@ -2712,7 +2809,9 @@ mod test_json_roundtrip { additional_attributes: false, }))), tags: None, - }, + annotations: Annotations::new(), + } + .into(), )]), actions: BTreeMap::from([( "action".into(), @@ -2729,9 +2828,13 @@ mod test_json_roundtrip { ))), }), member_of: None, - }, + annotations: Annotations::new(), + } + .into(), )]), - }, + annotations: Annotations::new(), + } + .into(), )])); roundtrip(fragment); } @@ -2754,10 +2857,14 @@ mod test_json_roundtrip { }, ))), tags: None, - }, + annotations: Annotations::new(), + } + .into(), )]), actions: BTreeMap::new(), - }, + annotations: Annotations::new(), + } + .into(), ), ( None, @@ -2779,9 +2886,13 @@ mod test_json_roundtrip { ))), }), member_of: None, - }, + annotations: Annotations::new(), + } + .into(), )]), - }, + annotations: Annotations::new(), + } + .into(), ), ])); roundtrip(fragment); @@ -2819,7 +2930,7 @@ mod test_duplicates_error { "Foo": { "entityTypes" : { "Bar": {}, - "Bar": {}, + "Bar": {} }, "actions": {} } @@ -2933,3 +3044,392 @@ mod test_duplicates_error { Fragment::from_json_str(src).unwrap(); } } + +#[cfg(test)] +mod annotations { + use crate::RawName; + use cool_asserts::assert_matches; + + use super::Fragment; + + #[test] + fn basic() { + let src = serde_json::json!( + { + "" : { + "entityTypes": {}, + "actions": {}, + "annotations": { + "doc": "this is a doc" + } + } + }); + let schema: Result, _> = serde_json::from_value(src); + assert_matches!(schema, Ok(_)); + + let src = serde_json::json!( + { + "" : { + "entityTypes": { + "a": { + "annotations": { + "a": "", + // null is also allowed like ESTs + "d": null, + "b": "c", + }, + "shape": { + "type": "Long", + } + } + }, + "actions": {}, + "annotations": { + "doc": "this is a doc" + } + } + }); + let schema: Result, _> = serde_json::from_value(src); + assert_matches!(schema, Ok(_)); + + let src = serde_json::json!( + { + "" : { + "entityTypes": { + "a": { + "annotations": { + "a": "", + "b": "c", + }, + "shape": { + "type": "Long", + } + } + }, + "actions": { + "a": { + "annotations": { + "doc": "this is a doc" + }, + "appliesTo": { + "principalTypes": ["A"], + "resourceTypes": ["B"], + } + }, + }, + "annotations": { + "doc": "this is a doc" + } + } + }); + let schema: Result, _> = serde_json::from_value(src); + assert_matches!(schema, Ok(_)); + + let src = serde_json::json!({ + "": { + "entityTypes": {}, + "actions": {}, + "commonTypes": { + "Task": { + "annotations": { + "doc": "a common type representing a task" + }, + "type": "Record", + "attributes": { + "id": { + "type": "Long", + "annotations": { + "doc": "task id" + } + }, + "name": { + "type": "String" + }, + "state": { + "type": "String" + } + } + }}}}); + let schema: Result, _> = serde_json::from_value(src); + assert_matches!(schema, Ok(_)); + + let src = serde_json::json!({ + "": { + "entityTypes": { + "User" : { + "shape" : { + "type" : "Record", + "attributes" : { + "name" : { + "annotations": { + "a": null, + }, + "type" : "String" + }, + "age" : { + "type" : "Long" + } + } + } + } + }, + "actions": {}, + "commonTypes": {} + }}); + let schema: Result, _> = serde_json::from_value(src); + assert_matches!(schema, Ok(_)); + + // nested record + let src = serde_json::json!({ + "": { + "entityTypes": { + "User" : { + "shape" : { + "type" : "Record", + "attributes" : { + "name" : { + "annotations": { + "first_layer": "b" + }, + "type" : "Record", + "attributes": { + "a": { + "type": "Record", + "annotations": { + "second_layer": "d" + }, + "attributes": { + "...": { + "annotations": { + "last_layer": null, + }, + "type": "Long" + } + } + } + } + }, + "age" : { + "type" : "Long" + } + } + } + } + }, + "actions": {}, + "commonTypes": {} + }}); + let schema: Result, _> = serde_json::from_value(src); + assert_matches!(schema, Ok(_)); + } + + #[track_caller] + fn test_unknown_fields(src: serde_json::Value, field: &str, expected: &str) { + let schema: Result, _> = serde_json::from_value(src); + assert_matches!(schema, Err(errs) => { + assert_eq!(errs.to_string(), format!("unknown field {field}, expected one of {expected}")); + }); + } + + const ENTITY_TYPE_EXPECTED_ATTRIBUTES: &str = "`memberOfTypes`, `shape`, `tags`, `annotations`"; + const NAMESPACE_EXPECTED_ATTRIBUTES: &str = + "`commonTypes`, `entityTypes`, `actions`, `annotations`"; + const ATTRIBUTE_TYPE_EXPECTED_ATTRIBUTES: &str = + "`type`, `element`, `attributes`, `additionalAttributes`, `name`"; + const APPLIES_TO_EXPECTED_ATTRIBUTES: &str = "`resourceTypes`, `principalTypes`, `context`"; + + #[test] + fn unknown_fields() { + let src = serde_json::json!( + { + "": { + "entityTypes": { + "UserGroup": { + "shape44": { + "type": "Record", + "attributes": {} + }, + "memberOfTypes": [ + "UserGroup" + ] + }}, + "actions": {}, + }}); + test_unknown_fields(src, "`shape44`", ENTITY_TYPE_EXPECTED_ATTRIBUTES); + + let src = serde_json::json!( + { + "": { + "entityTypes": {}, + "actions": {}, + "commonTypes": { + "C": { + "type": "Set", + "element": { + "annotations": { + "doc": "this is a doc" + }, + "type": "Long" + } + } + }}}); + test_unknown_fields(src, "`annotations`", ATTRIBUTE_TYPE_EXPECTED_ATTRIBUTES); + + let src = serde_json::json!( + { + "": { + "entityTypes": {}, + "actions": {}, + "commonTypes": { + "C": { + "type": "Long", + "foo": 1, + "annotations": { + "doc": "this is a doc" + }, + }}}}); + test_unknown_fields(src, "`foo`", ATTRIBUTE_TYPE_EXPECTED_ATTRIBUTES); + + let src = serde_json::json!( + { + "": { + "entityTypes": {}, + "actions": {}, + "commonTypes": { + "C": { + "type": "Record", + "attributes": { + "a": { + "annotations": { + "doc": "this is a doc" + }, + "type": "Long", + "foo": 2, + "required": true, + } + }, + }}}}); + test_unknown_fields(src, "`foo`", ATTRIBUTE_TYPE_EXPECTED_ATTRIBUTES); + + let src = serde_json::json!( + { + "": { + "entityTypes": {}, + "actions": {}, + "commonTypes": { + "C": { + "type": "Record", + "attributes": { + "a": { + "annotations": { + "doc": "this is a doc" + }, + "type": "Record", + "attributes": { + "b": { + "annotations": { + "doc": "this is a doc" + }, + "type": "Long", + "bar": 3, + }, + }, + "required": true, + } + }, + }}}}); + test_unknown_fields(src, "`bar`", ATTRIBUTE_TYPE_EXPECTED_ATTRIBUTES); + + let src = serde_json::json!( + { + "": { + "entityTypes": { + "UserGroup": { + "shape": { + "annotations": { + "doc": "this is a doc" + }, + "type": "Record", + "attributes": {} + }, + "memberOfTypes": [ + "UserGroup" + ] + }}, + "actions": {}, + }}); + test_unknown_fields(src, "`annotations`", ATTRIBUTE_TYPE_EXPECTED_ATTRIBUTES); + + let src = serde_json::json!( + { + "": { + "entityTypes": {}, + "actions": { + "a": { + "appliesTo": { + "annotations": { + "doc": "this is a doc" + }, + "principalTypes": ["A"], + "resourceTypes": ["B"], + } + }, + }, + }}); + test_unknown_fields(src, "`annotations`", APPLIES_TO_EXPECTED_ATTRIBUTES); + + let src = serde_json::json!( + { + "" : { + "entityTypes": {}, + "actions": {}, + "foo": "", + "annotations": { + "doc": "this is a doc" + } + } + }); + test_unknown_fields(src, "`foo`", NAMESPACE_EXPECTED_ATTRIBUTES); + + let src = serde_json::json!( + { + "" : { + "entityTypes": {}, + "actions": {}, + "commonTypes": { + "a": { + "type": "Long", + "annotations": { + "foo": "" + }, + "bar": 1 + } + } + } + }); + test_unknown_fields(src, "`bar`", ATTRIBUTE_TYPE_EXPECTED_ATTRIBUTES); + + let src = serde_json::json!( + { + "" : { + "entityTypes": {}, + "actions": {}, + "commonTypes": { + "a": { + "type": "Record", + "annotations": { + "foo": "" + }, + "attributes": { + "a": { + "bar": 1, + "type": "Long" + } + } + } + } + } + }); + test_unknown_fields(src, "`bar`", ATTRIBUTE_TYPE_EXPECTED_ATTRIBUTES); + } +} diff --git a/cedar-policy-validator/src/lib.rs b/cedar-policy-validator/src/lib.rs index 63abdc23a..9a44d451d 100644 --- a/cedar-policy-validator/src/lib.rs +++ b/cedar-policy-validator/src/lib.rs @@ -59,7 +59,6 @@ pub use str_checks::confusable_string_checks; pub mod cedar_schema; pub mod typecheck; use typecheck::Typechecker; - pub mod types; /// Used to select how a policy will be validated. @@ -276,6 +275,7 @@ mod test { use super::*; use cedar_policy_core::{ ast::{self, PolicyID}, + est::Annotations, parser::{self, Loc}, }; @@ -293,6 +293,7 @@ mod test { member_of_types: vec![], shape: json_schema::AttributesOrContext::default(), tags: None, + annotations: Annotations::new(), }, ), ( @@ -301,6 +302,7 @@ mod test { member_of_types: vec![], shape: json_schema::AttributesOrContext::default(), tags: None, + annotations: Annotations::new(), }, ), ], @@ -314,6 +316,7 @@ mod test { }), member_of: None, attributes: None, + annotations: Annotations::new(), }, )], ); diff --git a/cedar-policy-validator/src/rbac.rs b/cedar-policy-validator/src/rbac.rs index b27ad86f5..04ca08156 100644 --- a/cedar-policy-validator/src/rbac.rs +++ b/cedar-policy-validator/src/rbac.rs @@ -397,10 +397,8 @@ mod test { use std::collections::{HashMap, HashSet}; use cedar_policy_core::{ - ast::{ - Annotations, Effect, Eid, EntityUID, Expr, PolicyID, PrincipalConstraint, - ResourceConstraint, - }, + ast::{Effect, Eid, EntityUID, Expr, PolicyID, PrincipalConstraint, ResourceConstraint}, + est::Annotations, parser::{parse_policy, parse_policy_or_template}, test_utils::{expect_err, ExpectedErrorMessageBuilder}, }; @@ -488,6 +486,7 @@ mod test { member_of_types: vec![], shape: json_schema::AttributesOrContext::default(), tags: None, + annotations: Annotations::new(), }, )], [], @@ -496,7 +495,7 @@ mod test { let policy = Template::new( PolicyID::from_string("policy0"), None, - Annotations::new(), + ast::Annotations::new(), Effect::Permit, PrincipalConstraint::any(), ActionConstraint::any(), @@ -523,6 +522,7 @@ mod test { member_of_types: vec![], shape: json_schema::AttributesOrContext::default(), tags: None, + annotations: Annotations::new(), }, )], [], @@ -575,6 +575,7 @@ mod test { applies_to: None, member_of: None, attributes: None, + annotations: Annotations::new(), }, )], ); @@ -584,7 +585,7 @@ mod test { let policy = Template::new( PolicyID::from_string("policy0"), None, - Annotations::new(), + ast::Annotations::new(), Effect::Permit, PrincipalConstraint::any(), ActionConstraint::is_eq(entity), @@ -609,6 +610,7 @@ mod test { member_of_types: vec![], shape: json_schema::AttributesOrContext::default(), tags: None, + annotations: Annotations::new(), }, )], [], @@ -634,6 +636,7 @@ mod test { member_of_types: vec![], shape: json_schema::AttributesOrContext::default(), tags: None, + annotations: Annotations::new(), }, )], [], @@ -659,6 +662,7 @@ mod test { member_of_types: vec![], shape: json_schema::AttributesOrContext::default(), tags: None, + annotations: Annotations::new(), }, )], [], @@ -704,6 +708,7 @@ mod test { applies_to: None, member_of: None, attributes: None, + annotations: Annotations::new(), }, )], ); @@ -736,6 +741,7 @@ mod test { applies_to: None, member_of: None, attributes: None, + annotations: Annotations::new(), }, )], ); @@ -810,7 +816,7 @@ mod test { let policy = Template::new( PolicyID::from_string("policy0"), None, - Annotations::new(), + ast::Annotations::new(), Effect::Permit, PrincipalConstraint::any(), ActionConstraint::is_eq(entity), @@ -871,7 +877,7 @@ mod test { let policy = Template::new( PolicyID::from_string("policy0"), None, - Annotations::new(), + ast::Annotations::new(), Effect::Permit, PrincipalConstraint::is_eq(Arc::new(EntityUID::from_components( entity_type, @@ -935,6 +941,7 @@ mod test { applies_to: None, member_of: None, attributes: None, + annotations: Annotations::new(), }, )], ); @@ -962,6 +969,7 @@ mod test { applies_to: None, member_of: None, attributes: None, + annotations: Annotations::new(), }, )], ); @@ -989,6 +997,7 @@ mod test { applies_to: None, member_of: None, attributes: None, + annotations: Annotations::new(), }, )], ); @@ -1015,6 +1024,7 @@ mod test { member_of_types: vec![], shape: json_schema::AttributesOrContext::default(), tags: None, + annotations: Annotations::new(), }, )], [], @@ -1049,6 +1059,7 @@ mod test { member_of_types: vec![], shape: json_schema::AttributesOrContext::default(), tags: None, + annotations: Annotations::new(), }, ), ( @@ -1057,6 +1068,7 @@ mod test { member_of_types: vec![], shape: json_schema::AttributesOrContext::default(), tags: None, + annotations: Annotations::new(), }, ), ], @@ -1070,6 +1082,7 @@ mod test { }), member_of: Some(vec![]), attributes: None, + annotations: Annotations::new(), }, )], ) @@ -1136,7 +1149,7 @@ mod test { let policy = Template::new( PolicyID::from_string("policy0"), None, - Annotations::new(), + ast::Annotations::new(), Effect::Permit, PrincipalConstraint::is_eq(Arc::new(principal)), ActionConstraint::is_eq(action), @@ -1440,6 +1453,7 @@ mod test { member_of_types: vec![], shape: json_schema::AttributesOrContext::default(), tags: None, + annotations: Annotations::new(), }, ), ( @@ -1448,6 +1462,7 @@ mod test { member_of_types: vec![resource_parent_type.parse().unwrap()], shape: json_schema::AttributesOrContext::default(), tags: None, + annotations: Annotations::new(), }, ), ( @@ -1456,6 +1471,7 @@ mod test { member_of_types: vec![resource_grandparent_type.parse().unwrap()], shape: json_schema::AttributesOrContext::default(), tags: None, + annotations: Annotations::new(), }, ), ( @@ -1464,6 +1480,7 @@ mod test { member_of_types: vec![], shape: json_schema::AttributesOrContext::default(), tags: None, + annotations: Annotations::new(), }, ), ], @@ -1481,6 +1498,7 @@ mod test { action_parent_name.into(), )]), attributes: None, + annotations: Annotations::new(), }, ), ( @@ -1492,6 +1510,7 @@ mod test { action_grandparent_name.into(), )]), attributes: None, + annotations: Annotations::new(), }, ), ( @@ -1500,6 +1519,7 @@ mod test { applies_to: None, member_of: Some(vec![]), attributes: None, + annotations: Annotations::new(), }, ), ], @@ -1509,7 +1529,7 @@ mod test { let policy = Template::new( PolicyID::from_string("policy0"), None, - Annotations::new(), + ast::Annotations::new(), Effect::Permit, PrincipalConstraint::any(), ActionConstraint::is_in([action_grandparent_euid]), diff --git a/cedar-policy-validator/src/schema.rs b/cedar-policy-validator/src/schema.rs index d93b012fb..c3241fc83 100644 --- a/cedar-policy-validator/src/schema.rs +++ b/cedar-policy-validator/src/schema.rs @@ -1361,6 +1361,7 @@ impl<'a> CommonTypeResolver<'a> { json_schema::TypeOfAttribute { required: attr_ty.required, ty: Self::resolve_type(resolve_table, attr_ty.ty)?, + annotations: attr_ty.annotations, }, )) }) diff --git a/cedar-policy-validator/src/schema/namespace_def.rs b/cedar-policy-validator/src/schema/namespace_def.rs index 357008e19..5c98bf685 100644 --- a/cedar-policy-validator/src/schema/namespace_def.rs +++ b/cedar-policy-validator/src/schema/namespace_def.rs @@ -132,8 +132,13 @@ impl ValidatorNamespaceDef { // Convert the common types, actions and entity types from the schema // file into the representation used by the validator. - let common_types = - CommonTypeDefs::from_raw_common_types(namespace_def.common_types, namespace.as_ref())?; + let common_types = CommonTypeDefs::from_raw_common_types( + namespace_def + .common_types + .into_iter() + .map(|(key, value)| (key, value.ty)), + namespace.as_ref(), + )?; let actions = ActionsDef::from_raw_actions(namespace_def.actions, namespace.as_ref(), extensions)?; let entity_types = @@ -276,10 +281,10 @@ impl CommonTypeDefs { /// structures used by the schema format to those used internally by the /// validator. pub(crate) fn from_raw_common_types( - schema_file_type_def: BTreeMap>, + schema_file_type_def: impl IntoIterator)>, schema_namespace: Option<&InternalName>, ) -> crate::err::Result { - let mut defs = HashMap::with_capacity(schema_file_type_def.len()); + let mut defs = HashMap::new(); for (id, schema_ty) in schema_file_type_def { let name = RawName::new_from_unreserved(id.into()).qualify_with(schema_namespace); // the declaration name is always (unconditionally) prefixed by the current/active namespace match defs.entry(name) { @@ -390,10 +395,10 @@ impl EntityTypesDef { /// structures used by the schema format to those used internally by the /// validator. pub(crate) fn from_raw_entity_types( - schema_files_types: BTreeMap>, + schema_files_types: impl IntoIterator)>, schema_namespace: Option<&InternalName>, ) -> crate::err::Result { - let mut defs: HashMap = HashMap::with_capacity(schema_files_types.len()); + let mut defs: HashMap = HashMap::new(); for (id, entity_type) in schema_files_types { let ety = internal_name_to_entity_type( RawName::new_from_unreserved(id).qualify_with(schema_namespace), // the declaration name is always (unconditionally) prefixed by the current/active namespace @@ -580,11 +585,11 @@ impl ActionsDef { /// Construct an [`ActionsDef`] by converting the structures used by the /// schema format to those used internally by the validator. pub(crate) fn from_raw_actions( - schema_file_actions: BTreeMap>, + schema_file_actions: impl IntoIterator)>, schema_namespace: Option<&InternalName>, extensions: &Extensions<'_>, ) -> crate::err::Result { - let mut actions = HashMap::with_capacity(schema_file_actions.len()); + let mut actions = HashMap::new(); for (action_id_str, action_type) in schema_file_actions { let action_uid = json_schema::ActionEntityUID::default_type(action_id_str.clone()) .qualify_with(schema_namespace); // the declaration name is always (unconditionally) prefixed by the current/active namespace @@ -1023,7 +1028,7 @@ pub(crate) fn try_record_type_into_validator_type( Err(UnsupportedFeatureError(UnsupportedFeature::OpenRecordsAndEntities).into()) } else { Ok( - parse_record_attributes(rty.attributes, extensions)?.map(move |attrs| { + parse_record_attributes(rty.attributes.into_iter(), extensions)?.map(move |attrs| { Type::record_with_attributes( attrs, if rty.additional_attributes { diff --git a/cedar-policy-validator/src/typecheck/test/expr.rs b/cedar-policy-validator/src/typecheck/test/expr.rs index 73a88e875..5a80d8a08 100644 --- a/cedar-policy-validator/src/typecheck/test/expr.rs +++ b/cedar-policy-validator/src/typecheck/test/expr.rs @@ -22,6 +22,7 @@ use std::{str::FromStr, vec}; use cedar_policy_core::{ ast::{BinaryOp, EntityUID, Expr, Name, Pattern, PatternElem, SlotId, Value, Var}, + est::Annotations, extensions::Extensions, }; use itertools::Itertools; @@ -67,6 +68,7 @@ fn slot_in_typechecks() { member_of_types: vec![], shape: json_schema::AttributesOrContext::default(), tags: None, + annotations: Annotations::new(), }; let schema = json_schema::NamespaceDefinition::new([("typename".parse().unwrap(), etype)], []); assert_typechecks_for_mode( @@ -97,6 +99,7 @@ fn slot_equals_typechecks() { member_of_types: vec![], shape: json_schema::AttributesOrContext::default(), tags: None, + annotations: Annotations::new(), }; // These don't typecheck in strict mode because the test_util expression // typechecker doesn't have access to a schema, so it can't link diff --git a/cedar-policy/CHANGELOG.md b/cedar-policy/CHANGELOG.md index 919be6a1f..9a0c76a93 100644 --- a/cedar-policy/CHANGELOG.md +++ b/cedar-policy/CHANGELOG.md @@ -15,6 +15,7 @@ Cedar Language Version: TBD ### Added +- Implemented [RFC 48 (schema annotations)](https://github.com/cedar-policy/rfcs/blob/main/text/0048-schema-annotations.md) (#1316) - New `.isEmpty()` operator on sets (#1358, resolving #1356) - Added protobuf schemas and (de)serialization code using on `prost` crate behind the experimental `protobufs` flag. - Added protobuf and JSON generation code to `cedar-policy-cli`. diff --git a/cedar-policy/src/ffi/utils.rs b/cedar-policy/src/ffi/utils.rs index 49dfe10d1..60e153870 100644 --- a/cedar-policy/src/ffi/utils.rs +++ b/cedar-policy/src/ffi/utils.rs @@ -1196,7 +1196,7 @@ mod test { &ExpectedErrorMessageBuilder::error("failed to parse schema from string") .exactly_one_underline_with_label( "permit", - "expected `action`, `entity`, `namespace`, or `type`", + "expected `@`, `action`, `entity`, `namespace`, or `type`", ) .source("error parsing schema: unexpected token `permit`") .build(), diff --git a/cedar-wasm/build-wasm.sh b/cedar-wasm/build-wasm.sh index 4b2a6d071..dde967091 100755 --- a/cedar-wasm/build-wasm.sh +++ b/cedar-wasm/build-wasm.sh @@ -96,6 +96,7 @@ process_types_file() { echo "type SmolStr = string;" >> "$types_file" echo "export type TypeOfAttribute = Type & { required?: boolean };" >> "$types_file" + echo "export type CommonType = Type & { annotations?: Annotations };" >> "$types_file" } check_types_file() { From e52d2f18c0176994c844ea14fe5c7b67b90a56f2 Mon Sep 17 00:00:00 2001 From: shaobo-he-aws <130499339+shaobo-he-aws@users.noreply.github.com> Date: Thu, 19 Dec 2024 08:29:45 -0800 Subject: [PATCH 05/16] Add implementations of the `Arbitrary` trait for annotations (#1382) Signed-off-by: Shaobo He --- cedar-policy-core/src/ast/annotation.rs | 15 +++++++++++++++ cedar-policy-core/src/est/annotation.rs | 1 + cedar-policy-validator/src/json_schema.rs | 7 ++++--- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/cedar-policy-core/src/ast/annotation.rs b/cedar-policy-core/src/ast/annotation.rs index ac7b2eb33..b7899d19d 100644 --- a/cedar-policy-core/src/ast/annotation.rs +++ b/cedar-policy-core/src/ast/annotation.rs @@ -26,6 +26,7 @@ use super::AnyId; /// Struct which holds the annotations for a policy #[derive(Serialize, Deserialize, Clone, Hash, Eq, PartialEq, PartialOrd, Ord, Debug)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct Annotations( #[serde(default)] #[serde(skip_serializing_if = "BTreeMap::is_empty")] @@ -165,3 +166,17 @@ impl From<&Annotation> for crate::ast::proto::Annotation { } } } + +#[cfg(feature = "arbitrary")] +impl<'a> arbitrary::Arbitrary<'a> for Annotation { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + Ok(Self { + val: u.arbitrary::<&str>()?.into(), + loc: None, + }) + } + + fn size_hint(depth: usize) -> (usize, Option) { + <&str as arbitrary::Arbitrary>::size_hint(depth) + } +} diff --git a/cedar-policy-core/src/est/annotation.rs b/cedar-policy-core/src/est/annotation.rs index eb4df1e43..847e78fb7 100644 --- a/cedar-policy-core/src/est/annotation.rs +++ b/cedar-policy-core/src/est/annotation.rs @@ -25,6 +25,7 @@ extern crate tsify; /// Similar to [`ast::Annotations`] but allow annotation value to be `null` #[derive(Serialize, Deserialize, Clone, Hash, Eq, PartialEq, PartialOrd, Ord, Debug)] #[cfg_attr(feature = "wasm", derive(tsify::Tsify))] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct Annotations( #[serde(default)] #[serde(skip_serializing_if = "BTreeMap::is_empty")] diff --git a/cedar-policy-validator/src/json_schema.rs b/cedar-policy-validator/src/json_schema.rs index f51b2f6a7..8fcfbe285 100644 --- a/cedar-policy-validator/src/json_schema.rs +++ b/cedar-policy-validator/src/json_schema.rs @@ -1787,15 +1787,16 @@ impl<'a> arbitrary::Arbitrary<'a> for TypeOfAttribute { Ok(Self { ty: u.arbitrary::>()?, required: u.arbitrary()?, - annotations: cedar_policy_core::est::Annotations::new(), + annotations: u.arbitrary()?, }) } fn size_hint(depth: usize) -> (usize, Option) { - arbitrary::size_hint::and( + arbitrary::size_hint::and_all(&[ as arbitrary::Arbitrary>::size_hint(depth), ::size_hint(depth), - ) + ::size_hint(depth), + ]) } } From 99bb87bbbb2a82efbdb0ed65d76dd46aafb112b4 Mon Sep 17 00:00:00 2001 From: John Kastner <130772734+john-h-kastner-aws@users.noreply.github.com> Date: Thu, 19 Dec 2024 14:31:45 -0500 Subject: [PATCH 06/16] Avoid building unnecessary maps on type error (#1381) Signed-off-by: John Kastner --- cedar-policy-core/src/ast/extension.rs | 1 + cedar-policy-core/src/evaluator.rs | 10 ++++++---- cedar-policy-core/src/extensions.rs | 7 ++++++- cedar-policy-validator/src/diagnostics.rs | 4 ++-- .../src/diagnostics/validation_errors.rs | 2 +- cedar-policy-validator/src/typecheck.rs | 4 ++-- .../src/typecheck/test/expr.rs | 16 +++++++-------- .../src/typecheck/test/namespace.rs | 2 +- .../src/typecheck/test/partial.rs | 20 +++++++++---------- .../src/typecheck/test/policy.rs | 2 +- .../src/typecheck/test/test_utils.rs | 2 +- cedar-policy-validator/src/types.rs | 2 +- 12 files changed, 39 insertions(+), 33 deletions(-) diff --git a/cedar-policy-core/src/ast/extension.rs b/cedar-policy-core/src/ast/extension.rs index d8b8e05e9..db1555d05 100644 --- a/cedar-policy-core/src/ast/extension.rs +++ b/cedar-policy-core/src/ast/extension.rs @@ -34,6 +34,7 @@ mod names { lazy_static::lazy_static! { /// Extension type names that support operator overloading + // INVARIANT: this set must not be empty. pub static ref TYPES_WITH_OPERATOR_OVERLOADING : BTreeSet = BTreeSet::from_iter( [Name::parse_unqualified_name("datetime").expect("valid identifier"), diff --git a/cedar-policy-core/src/evaluator.rs b/cedar-policy-core/src/evaluator.rs index 6f91d810c..a8b30e520 100644 --- a/cedar-policy-core/src/evaluator.rs +++ b/cedar-policy-core/src/evaluator.rs @@ -31,7 +31,7 @@ pub use err::EvaluationError; pub(crate) use err::*; use evaluation_errors::*; use itertools::Either; -use nonempty::{nonempty, NonEmpty}; +use nonempty::nonempty; use smol_str::SmolStr; #[cfg(not(target_arch = "wasm32"))] @@ -427,9 +427,11 @@ impl<'e> Evaluator<'e> { (ValueKind::ExtensionValue(x), _) if x.supports_operator_overloading() => Err(EvaluationError::type_error_single(Type::Extension { name: x.typename() }, &arg2)), (_, ValueKind::ExtensionValue(y)) if y.supports_operator_overloading() => Err(EvaluationError::type_error_single(Type::Extension { name: y.typename() }, &arg1)), (ValueKind::ExtensionValue(x), ValueKind::ExtensionValue(y)) if x.typename() == y.typename() => Err(EvaluationError::type_error_with_advice(Extensions::types_with_operator_overloading().map(|name| Type::Extension { name} ), &arg1, "Only extension types `datetime` and `duration` support operator overloading".to_string())), - // PANIC SAFETY: we're collecting a non-empty vec - #[allow(clippy::unwrap_used)] - _ => Err(EvaluationError::type_error_with_advice(NonEmpty::collect(Extensions::types_with_operator_overloading().map(|name| Type::Extension { name} ).into_iter().chain(std::iter::once(Type::Long))).unwrap(), &arg1, "Only `Long` and extension types `datetime`, `duration` support comparison".to_string())) + _ => { + let mut expected_types = Extensions::types_with_operator_overloading().map(|name| Type::Extension { name }); + expected_types.push(Type::Long); + Err(EvaluationError::type_error_with_advice(expected_types, &arg1, "Only `Long` and extension types `datetime`, `duration` support comparison".to_string())) + } } } BinaryOp::Add | BinaryOp::Sub | BinaryOp::Mul => { diff --git a/cedar-policy-core/src/extensions.rs b/cedar-policy-core/src/extensions.rs index 0419899dd..57bf8587c 100644 --- a/cedar-policy-core/src/extensions.rs +++ b/cedar-policy-core/src/extensions.rs @@ -99,12 +99,17 @@ impl Extensions<'static> { &EXTENSIONS_NONE } - /// Iterate over extension types that support operator overloading + /// Obtain the non-empty vector of types supporting operator overloading pub fn types_with_operator_overloading() -> NonEmpty { // PANIC SAFETY: There are more than one element in `TYPES_WITH_OPERATOR_OVERLOADING` #[allow(clippy::unwrap_used)] NonEmpty::collect(TYPES_WITH_OPERATOR_OVERLOADING.iter().cloned()).unwrap() } + + /// Iterate over extension types that support operator overloading + pub fn iter_type_with_operator_overloading() -> impl Iterator { + TYPES_WITH_OPERATOR_OVERLOADING.iter() + } } impl<'a> Extensions<'a> { diff --git a/cedar-policy-validator/src/diagnostics.rs b/cedar-policy-validator/src/diagnostics.rs index 4ee0fd200..7d11f3ebe 100644 --- a/cedar-policy-validator/src/diagnostics.rs +++ b/cedar-policy-validator/src/diagnostics.rs @@ -225,14 +225,14 @@ impl ValidationError { pub(crate) fn expected_one_of_types( source_loc: Option, policy_id: PolicyID, - expected: impl IntoIterator, + expected: Vec, actual: Type, help: Option, ) -> Self { validation_errors::UnexpectedType { source_loc, policy_id, - expected: expected.into_iter().collect::>(), + expected, actual, help, } diff --git a/cedar-policy-validator/src/diagnostics/validation_errors.rs b/cedar-policy-validator/src/diagnostics/validation_errors.rs index d4485ee2f..ec578017c 100644 --- a/cedar-policy-validator/src/diagnostics/validation_errors.rs +++ b/cedar-policy-validator/src/diagnostics/validation_errors.rs @@ -174,7 +174,7 @@ pub struct UnexpectedType { /// Policy ID where the error occurred pub policy_id: PolicyID, /// Type(s) which were expected - pub expected: BTreeSet, + pub expected: Vec, /// Type which was encountered pub actual: Type, /// Optional help for resolving the error diff --git a/cedar-policy-validator/src/typecheck.rs b/cedar-policy-validator/src/typecheck.rs index b488e465d..36e87e63e 100644 --- a/cedar-policy-validator/src/typecheck.rs +++ b/cedar-policy-validator/src/typecheck.rs @@ -1245,8 +1245,8 @@ impl<'a> Typechecker<'a> { } BinaryOp::Less | BinaryOp::LessEq => { - let expected_types = Extensions::types_with_operator_overloading() - .into_iter() + let expected_types = Extensions::iter_type_with_operator_overloading() + .cloned() .map(Type::extension) .chain(std::iter::once(Type::primitive_long())) .collect_vec(); diff --git a/cedar-policy-validator/src/typecheck/test/expr.rs b/cedar-policy-validator/src/typecheck/test/expr.rs index 5a80d8a08..f29799352 100644 --- a/cedar-policy-validator/src/typecheck/test/expr.rs +++ b/cedar-policy-validator/src/typecheck/test/expr.rs @@ -653,7 +653,7 @@ fn has_typecheck_fails() { ValidationError::expected_one_of_types( get_loc(src, "true"), expr_id_placeholder(), - [Type::any_entity_reference(), Type::any_record()], + vec![Type::any_entity_reference(), Type::any_record()], Type::singleton_boolean(true), None, ) @@ -701,7 +701,7 @@ fn record_get_attr_typecheck_fails() { ValidationError::expected_one_of_types( get_loc(src, "2"), expr_id_placeholder(), - [Type::any_entity_reference(), Type::any_record()], + vec![Type::any_entity_reference(), Type::any_record()], Type::primitive_long(), None, ) @@ -821,7 +821,7 @@ fn in_typecheck_fails() { ValidationError::expected_one_of_types( get_loc(src, "true"), expr_id_placeholder(), - [ + vec![ Type::set(Type::any_entity_reference()), Type::any_entity_reference(), ], @@ -1115,12 +1115,10 @@ fn less_than_typechecks() { #[test] fn less_than_typecheck_fails() { - let expected_types = std::iter::once(Type::primitive_long()) - .chain( - Extensions::types_with_operator_overloading() - .into_iter() - .map(Type::extension), - ) + let expected_types = Extensions::types_with_operator_overloading() + .into_iter() + .map(Type::extension) + .chain(std::iter::once(Type::primitive_long())) .collect_vec(); let src = "true < false"; let errors = diff --git a/cedar-policy-validator/src/typecheck/test/namespace.rs b/cedar-policy-validator/src/typecheck/test/namespace.rs index e7ad81496..c3d5c0a81 100644 --- a/cedar-policy-validator/src/typecheck/test/namespace.rs +++ b/cedar-policy-validator/src/typecheck/test/namespace.rs @@ -148,7 +148,7 @@ fn namespaced_entity_can_type_error() { ValidationError::expected_one_of_types( get_loc(src, r#"N::S::Foo::"alice""#), expr_id_placeholder(), - std::iter::once(Type::primitive_long()), + vec![Type::primitive_long()], Type::named_entity_reference_from_str("N::S::Foo"), None, ) diff --git a/cedar-policy-validator/src/typecheck/test/partial.rs b/cedar-policy-validator/src/typecheck/test/partial.rs index 1238f623c..eeda516cf 100644 --- a/cedar-policy-validator/src/typecheck/test/partial.rs +++ b/cedar-policy-validator/src/typecheck/test/partial.rs @@ -405,11 +405,11 @@ mod fails_empty_schema { [ValidationError::expected_one_of_types( get_loc(src, r#""a""#), PolicyID::from_string("policy0"), - std::iter::once(Type::primitive_long()).chain( - Extensions::types_with_operator_overloading() - .into_iter() - .map(Type::extension), - ), + Extensions::types_with_operator_overloading() + .into_iter() + .map(Type::extension) + .chain(std::iter::once(Type::primitive_long())) + .collect(), Type::primitive_string(), None, )], @@ -653,11 +653,11 @@ mod fail_partial_schema { [ValidationError::expected_one_of_types( get_loc(src, "principal.name"), PolicyID::from_string("policy0"), - std::iter::once(Type::primitive_long()).chain( - Extensions::types_with_operator_overloading() - .into_iter() - .map(Type::extension), - ), + Extensions::types_with_operator_overloading() + .into_iter() + .map(Type::extension) + .chain(std::iter::once(Type::primitive_long())) + .collect(), Type::primitive_string(), None, )], diff --git a/cedar-policy-validator/src/typecheck/test/policy.rs b/cedar-policy-validator/src/typecheck/test/policy.rs index c10f19756..92b7510ad 100644 --- a/cedar-policy-validator/src/typecheck/test/policy.rs +++ b/cedar-policy-validator/src/typecheck/test/policy.rs @@ -845,7 +845,7 @@ fn type_error_is_not_reported_for_every_cross_product_element() { ValidationError::expected_one_of_types( get_loc(src, "true"), PolicyID::from_string("0"), - std::iter::once(Type::primitive_long()), + vec![Type::primitive_long()], Type::True, None, ) diff --git a/cedar-policy-validator/src/typecheck/test/test_utils.rs b/cedar-policy-validator/src/typecheck/test/test_utils.rs index c45db4482..db94a61b6 100644 --- a/cedar-policy-validator/src/typecheck/test/test_utils.rs +++ b/cedar-policy-validator/src/typecheck/test/test_utils.rs @@ -124,7 +124,7 @@ pub(crate) fn assert_types_eq(schema: &ValidatorSchema, expected: &Type, actual: /// Assert that every `T` in `actual` appears in `expected`, and vice versa. #[track_caller] -pub(crate) fn assert_sets_equal( +pub(crate) fn assert_sets_equal( expected: impl IntoIterator, actual: impl IntoIterator, ) { diff --git a/cedar-policy-validator/src/types.rs b/cedar-policy-validator/src/types.rs index ade1f88b3..da4a162f2 100644 --- a/cedar-policy-validator/src/types.rs +++ b/cedar-policy-validator/src/types.rs @@ -675,7 +675,7 @@ impl Type { pub(crate) fn support_operator_overloading(&self) -> bool { match self { Self::ExtensionType { name } => { - Extensions::types_with_operator_overloading().contains(name) + Extensions::iter_type_with_operator_overloading().contains(name) } _ => false, } From 6f28be2ecc3132b29cd3fec76ec00fc6e21a7ee3 Mon Sep 17 00:00:00 2001 From: shaobo-he-aws <130499339+shaobo-he-aws@users.noreply.github.com> Date: Fri, 20 Dec 2024 11:40:03 -0800 Subject: [PATCH 07/16] Do not allow annotations on the empty namespace (#1386) Signed-off-by: Shaobo He --- cedar-policy-validator/src/json_schema.rs | 53 ++++++++++++++++------- 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/cedar-policy-validator/src/json_schema.rs b/cedar-policy-validator/src/json_schema.rs index 8fcfbe285..f05a9cc10 100644 --- a/cedar-policy-validator/src/json_schema.rs +++ b/cedar-policy-validator/src/json_schema.rs @@ -107,6 +107,11 @@ where raw.into_iter() .map(|(key, value)| { let key = if key.is_empty() { + if !value.annotations.is_empty() { + Err(serde::de::Error::custom(format!( + "annotations are not allowed on the empty namespace" + )))? + } None } else { Some(Name::from_normalized_str(&key).map_err(|err| { @@ -3054,7 +3059,7 @@ mod annotations { use super::Fragment; #[test] - fn basic() { + fn empty_namespace() { let src = serde_json::json!( { "" : { @@ -3066,11 +3071,29 @@ mod annotations { } }); let schema: Result, _> = serde_json::from_value(src); + assert_matches!(schema, Err(err) => { + assert_eq!(&err.to_string(), "annotations are not allowed on the empty namespace"); + }); + } + + #[test] + fn basic() { + let src = serde_json::json!( + { + "N" : { + "entityTypes": {}, + "actions": {}, + "annotations": { + "doc": "this is a doc" + } + } + }); + let schema: Result, _> = serde_json::from_value(src); assert_matches!(schema, Ok(_)); let src = serde_json::json!( { - "" : { + "N" : { "entityTypes": { "a": { "annotations": { @@ -3095,7 +3118,7 @@ mod annotations { let src = serde_json::json!( { - "" : { + "N" : { "entityTypes": { "a": { "annotations": { @@ -3127,7 +3150,7 @@ mod annotations { assert_matches!(schema, Ok(_)); let src = serde_json::json!({ - "": { + "N": { "entityTypes": {}, "actions": {}, "commonTypes": { @@ -3155,7 +3178,7 @@ mod annotations { assert_matches!(schema, Ok(_)); let src = serde_json::json!({ - "": { + "N": { "entityTypes": { "User" : { "shape" : { @@ -3182,7 +3205,7 @@ mod annotations { // nested record let src = serde_json::json!({ - "": { + "N": { "entityTypes": { "User" : { "shape" : { @@ -3243,7 +3266,7 @@ mod annotations { fn unknown_fields() { let src = serde_json::json!( { - "": { + "N": { "entityTypes": { "UserGroup": { "shape44": { @@ -3260,7 +3283,7 @@ mod annotations { let src = serde_json::json!( { - "": { + "N": { "entityTypes": {}, "actions": {}, "commonTypes": { @@ -3278,7 +3301,7 @@ mod annotations { let src = serde_json::json!( { - "": { + "N": { "entityTypes": {}, "actions": {}, "commonTypes": { @@ -3293,7 +3316,7 @@ mod annotations { let src = serde_json::json!( { - "": { + "N": { "entityTypes": {}, "actions": {}, "commonTypes": { @@ -3314,7 +3337,7 @@ mod annotations { let src = serde_json::json!( { - "": { + "N": { "entityTypes": {}, "actions": {}, "commonTypes": { @@ -3343,7 +3366,7 @@ mod annotations { let src = serde_json::json!( { - "": { + "N": { "entityTypes": { "UserGroup": { "shape": { @@ -3363,7 +3386,7 @@ mod annotations { let src = serde_json::json!( { - "": { + "N": { "entityTypes": {}, "actions": { "a": { @@ -3381,7 +3404,7 @@ mod annotations { let src = serde_json::json!( { - "" : { + "N" : { "entityTypes": {}, "actions": {}, "foo": "", @@ -3412,7 +3435,7 @@ mod annotations { let src = serde_json::json!( { - "" : { + "N" : { "entityTypes": {}, "actions": {}, "commonTypes": { From 78abd25aaff314d0dd27839f4f8f6cf46718f768 Mon Sep 17 00:00:00 2001 From: John Kastner <130772734+john-h-kastner-aws@users.noreply.github.com> Date: Fri, 27 Dec 2024 09:33:04 -0500 Subject: [PATCH 08/16] Avoid some clones constructing error enum (#1384) Signed-off-by: John Kastner --- .../src/cedar_schema/err.rs | 18 +++++++-------- .../src/cedar_schema/to_json_schema.rs | 23 ++++++++----------- 2 files changed, 19 insertions(+), 22 deletions(-) diff --git a/cedar-policy-validator/src/cedar_schema/err.rs b/cedar-policy-validator/src/cedar_schema/err.rs index 394f759f7..d5b65b2bf 100644 --- a/cedar-policy-validator/src/cedar_schema/err.rs +++ b/cedar-policy-validator/src/cedar_schema/err.rs @@ -437,7 +437,7 @@ pub enum ToJsonSchemaError { } impl ToJsonSchemaError { - pub(crate) fn duplicate_context(name: impl ToSmolStr, loc1: Loc, loc2: Loc) -> Self { + pub(crate) fn duplicate_context(name: &impl ToSmolStr, loc1: Loc, loc2: Loc) -> Self { Self::DuplicateContext(DuplicateContext { name: name.to_smolstr(), loc1, @@ -445,7 +445,7 @@ impl ToJsonSchemaError { }) } - pub(crate) fn duplicate_decls(decl: impl ToSmolStr, loc1: Loc, loc2: Loc) -> Self { + pub(crate) fn duplicate_decls(decl: &impl ToSmolStr, loc1: Loc, loc2: Loc) -> Self { Self::DuplicateDeclarations(DuplicateDeclarations { decl: decl.to_smolstr(), loc1, @@ -454,7 +454,7 @@ impl ToJsonSchemaError { } pub(crate) fn duplicate_namespace( - namespace_id: impl ToSmolStr, + namespace_id: &impl ToSmolStr, loc1: Option, loc2: Option, ) -> Self { @@ -465,7 +465,7 @@ impl ToJsonSchemaError { }) } - pub(crate) fn duplicate_principal(name: impl ToSmolStr, loc1: Loc, loc2: Loc) -> Self { + pub(crate) fn duplicate_principal(name: &impl ToSmolStr, loc1: Loc, loc2: Loc) -> Self { Self::DuplicatePrincipalOrResource(DuplicatePrincipalOrResource { name: name.to_smolstr(), kind: PR::Principal, @@ -474,7 +474,7 @@ impl ToJsonSchemaError { }) } - pub(crate) fn duplicate_resource(name: impl ToSmolStr, loc1: Loc, loc2: Loc) -> Self { + pub(crate) fn duplicate_resource(name: &impl ToSmolStr, loc1: Loc, loc2: Loc) -> Self { Self::DuplicatePrincipalOrResource(DuplicatePrincipalOrResource { name: name.to_smolstr(), kind: PR::Resource, @@ -483,7 +483,7 @@ impl ToJsonSchemaError { }) } - pub(crate) fn no_principal(name: impl ToSmolStr, loc: Loc) -> Self { + pub(crate) fn no_principal(name: &impl ToSmolStr, loc: Loc) -> Self { Self::NoPrincipalOrResource(NoPrincipalOrResource { kind: PR::Principal, name: name.to_smolstr(), @@ -491,7 +491,7 @@ impl ToJsonSchemaError { }) } - pub(crate) fn no_resource(name: impl ToSmolStr, loc: Loc) -> Self { + pub(crate) fn no_resource(name: &impl ToSmolStr, loc: Loc) -> Self { Self::NoPrincipalOrResource(NoPrincipalOrResource { kind: PR::Resource, name: name.to_smolstr(), @@ -499,14 +499,14 @@ impl ToJsonSchemaError { }) } - pub(crate) fn reserved_name(name: impl ToSmolStr, loc: Loc) -> Self { + pub(crate) fn reserved_name(name: &impl ToSmolStr, loc: Loc) -> Self { Self::ReservedName(ReservedName { name: name.to_smolstr(), loc, }) } - pub(crate) fn reserved_keyword(keyword: impl ToSmolStr, loc: Loc) -> Self { + pub(crate) fn reserved_keyword(keyword: &impl ToSmolStr, loc: Loc) -> Self { Self::ReservedSchemaKeyword(ReservedSchemaKeyword { keyword: keyword.to_smolstr(), loc, diff --git a/cedar-policy-validator/src/cedar_schema/to_json_schema.rs b/cedar-policy-validator/src/cedar_schema/to_json_schema.rs index f696c2f98..d84190c71 100644 --- a/cedar-policy-validator/src/cedar_schema/to_json_schema.rs +++ b/cedar-policy-validator/src/cedar_schema/to_json_schema.rs @@ -197,7 +197,7 @@ impl TryFrom> for json_schema::NamespaceDefinition let id = UnreservedId::try_from(decl.data.name.node) .map_err(|e| ToJsonSchemaError::reserved_name(e.name(), name_loc.clone()))?; let ctid = json_schema::CommonTypeId::new(id) - .map_err(|e| ToJsonSchemaError::reserved_keyword(e.id, name_loc))?; + .map_err(|e| ToJsonSchemaError::reserved_keyword(&e.id, name_loc))?; Ok(( ctid, CommonType { @@ -273,7 +273,7 @@ fn convert_app_decls( } => match context { Some(existing_context) => { return Err(ToJsonSchemaError::duplicate_context( - name.clone(), + name, existing_context.loc, loc, ) @@ -300,7 +300,7 @@ fn convert_app_decls( } => match principal_types { Some(existing_tys) => { return Err(ToJsonSchemaError::duplicate_principal( - name.clone(), + name, existing_tys.loc, loc, ) @@ -325,12 +325,9 @@ fn convert_app_decls( loc, } => match resource_types { Some(existing_tys) => { - return Err(ToJsonSchemaError::duplicate_resource( - name.clone(), - existing_tys.loc, - loc, - ) - .into()); + return Err( + ToJsonSchemaError::duplicate_resource(name, existing_tys.loc, loc).into(), + ); } None => { resource_types = Some(Node::with_source_loc( @@ -344,10 +341,10 @@ fn convert_app_decls( Ok(json_schema::ApplySpec { resource_types: resource_types .map(|node| node.node) - .ok_or_else(|| ToJsonSchemaError::no_resource(name.clone(), name_loc.clone()))?, + .ok_or_else(|| ToJsonSchemaError::no_resource(&name, name_loc.clone()))?, principal_types: principal_types .map(|node| node.node) - .ok_or_else(|| ToJsonSchemaError::no_principal(name.clone(), name_loc.clone()))?, + .ok_or_else(|| ToJsonSchemaError::no_principal(&name, name_loc.clone()))?, context: context.map(|c| c.node).unwrap_or_default(), }) } @@ -524,7 +521,7 @@ where for (key, node) in i { match map.entry(key.clone()) { Entry::Occupied(entry) => Err(ToJsonSchemaError::duplicate_decls( - key, + &key, entry.get().loc.clone(), node.loc, )), @@ -614,7 +611,7 @@ fn update_namespace_record( ) -> Result<(), ToJsonSchemaErrors> { match map.entry(name.clone()) { Entry::Occupied(entry) => Err(ToJsonSchemaError::duplicate_namespace( - name.map_or("".into(), |n| n.to_smolstr()), + &name.map_or("".into(), |n| n.to_smolstr()), record.loc, entry.get().loc.clone(), ) From 2e8cbf11d09fb6bf2eae3af8d958a3a1596a6aaa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Dec 2024 09:19:56 -0500 Subject: [PATCH 09/16] Bump the rust-dependencies group across 1 directory with 26 updates (#1396) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 144 +++++++++++++----------------- cedar-policy-cli/Cargo.toml | 4 +- cedar-policy-core/Cargo.toml | 2 +- cedar-policy-validator/Cargo.toml | 2 +- cedar-policy/Cargo.toml | 4 +- 5 files changed, 67 insertions(+), 89 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dbdb44df5..b90336ba5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -98,9 +98,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.94" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" +checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" [[package]] name = "arbitrary" @@ -184,30 +184,15 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" -[[package]] -name = "bit-set" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" -dependencies = [ - "bit-vec 0.6.3", -] - [[package]] name = "bit-set" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ - "bit-vec 0.8.0", + "bit-vec", ] -[[package]] -name = "bit-vec" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" - [[package]] name = "bit-vec" version = "0.8.0" @@ -287,9 +272,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.4" +version = "1.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9157bbaa6b165880c27a4293a474c91cdcf265cc68cc829bf10be0964a391caf" +checksum = "8d6dbb628b8f8555f86d0323c2eb39e3ec81901f4b83e091db8a6a76d316a333" dependencies = [ "shlex", ] @@ -320,7 +305,7 @@ dependencies = [ "serde_json", "serde_with", "smol_str", - "thiserror 2.0.7", + "thiserror 2.0.9", "tsify", "wasm-bindgen", ] @@ -345,7 +330,7 @@ dependencies = [ "serde", "serde_json", "tempfile", - "thiserror 2.0.7", + "thiserror 2.0.9", ] [[package]] @@ -374,7 +359,7 @@ dependencies = [ "serde_with", "smol_str", "stacker", - "thiserror 2.0.7", + "thiserror 2.0.9", "tsify", "wasm-bindgen", ] @@ -417,7 +402,7 @@ dependencies = [ "similar-asserts", "smol_str", "stacker", - "thiserror 2.0.7", + "thiserror 2.0.9", "tsify", "unicode-security", "wasm-bindgen", @@ -559,14 +544,14 @@ checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "console" -version = "0.15.8" +version = "0.15.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +checksum = "ea3c6ecd8059b57859df5c69830340ed3c41d30e3da0c1cbed90a96ac853041b" dependencies = [ "encode_unicode", - "lazy_static", "libc", - "windows-sys 0.52.0", + "once_cell", + "windows-sys 0.59.0", ] [[package]] @@ -814,9 +799,9 @@ dependencies = [ [[package]] name = "encode_unicode" -version = "0.3.6" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" [[package]] name = "enum-ordinalize" @@ -868,9 +853,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "float-cmp" -version = "0.9.0" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" dependencies = [ "num-traits", ] @@ -1014,9 +999,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "glob" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] name = "globset" @@ -1376,7 +1361,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06093b57658c723a21da679530e061a8c25340fa5a6f98e313b542268c7e2a1f" dependencies = [ "ascii-canvas", - "bit-set 0.8.0", + "bit-set", "ena", "itertools 0.13.0", "lalrpop-util", @@ -1409,15 +1394,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.168" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" - -[[package]] -name = "libm" -version = "0.2.11" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "linked-hash-map" @@ -1537,9 +1516,9 @@ dependencies = [ [[package]] name = "miniz_oxide" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" dependencies = [ "adler2", ] @@ -1587,14 +1566,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", - "libm", ] [[package]] name = "object" -version = "0.36.5" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] @@ -1734,9 +1712,9 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "predicates" -version = "3.1.2" +version = "3.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97" +checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" dependencies = [ "anstyle", "difflib", @@ -1748,15 +1726,15 @@ dependencies = [ [[package]] name = "predicates-core" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae8177bee8e75d6846599c6b9ff679ed51e882816914eec639944d7c9aa11931" +checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" [[package]] name = "predicates-tree" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41b740d195ed3166cd147c8047ec98db0e22ec019eb8eeb76d343b795304fb13" +checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" dependencies = [ "predicates-core", "termtree", @@ -1803,12 +1781,12 @@ dependencies = [ [[package]] name = "proptest" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c2511913b88df1637da85cc8d96ec8e43a3f8bb8ccb71ee1ac240d6f3df58d" +checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50" dependencies = [ - "bit-set 0.5.3", - "bit-vec 0.6.3", + "bit-set", + "bit-vec", "bitflags", "lazy_static", "num-traits", @@ -1890,9 +1868,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quote" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] @@ -2095,9 +2073,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" [[package]] name = "rusty-fork" @@ -2149,9 +2127,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.216" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] @@ -2169,9 +2147,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.216" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", @@ -2191,9 +2169,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.133" +version = "1.0.134" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" +checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d" dependencies = [ "indexmap 2.7.0", "itoa", @@ -2213,9 +2191,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.11.0" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817" +checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" dependencies = [ "base64", "chrono", @@ -2231,9 +2209,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.11.0" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d" +checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" dependencies = [ "darling", "proc-macro2", @@ -2369,9 +2347,9 @@ checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" [[package]] name = "syn" -version = "2.0.90" +version = "2.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" +checksum = "9c786062daee0d6db1132800e623df74274a0a87322d8e183338e01b3d98d058" dependencies = [ "proc-macro2", "quote", @@ -2424,9 +2402,9 @@ dependencies = [ [[package]] name = "termtree" -version = "0.4.1" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" [[package]] name = "textwrap" @@ -2449,11 +2427,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.7" +version = "2.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93605438cbd668185516ab499d589afb7ee1859ea3d5fc8f6b0755e1c7443767" +checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc" dependencies = [ - "thiserror-impl 2.0.7", + "thiserror-impl 2.0.9", ] [[package]] @@ -2469,9 +2447,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.7" +version = "2.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d8749b4531af2117677a5fcd12b1348a3fe2b81e36e61ffeac5c4aa3273e36" +checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" dependencies = [ "proc-macro2", "quote", @@ -2537,9 +2515,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8" dependencies = [ "tinyvec_macros", ] diff --git a/cedar-policy-cli/Cargo.toml b/cedar-policy-cli/Cargo.toml index 6c7a25142..5f55c704d 100644 --- a/cedar-policy-cli/Cargo.toml +++ b/cedar-policy-cli/Cargo.toml @@ -37,8 +37,8 @@ protobufs = ["dep:prost", "dep:prost-build", "cedar-policy/protobufs", "cedar-po [dev-dependencies] assert_cmd = "2.0" tempfile = "3" -glob = "0.3.1" -predicates = "3.1.0" +glob = "0.3.2" +predicates = "3.1.3" rstest = "0.23.0" # We override the name of the binary for src/main.rs, which otherwise would be diff --git a/cedar-policy-core/Cargo.toml b/cedar-policy-core/Cargo.toml index 9c0fa7ba7..31992c6de 100644 --- a/cedar-policy-core/Cargo.toml +++ b/cedar-policy-core/Cargo.toml @@ -14,7 +14,7 @@ repository.workspace = true [dependencies] serde = { version = "1.0", features = ["derive", "rc"] } -serde_with = { version = "3.0", features = ["json"] } +serde_with = { version = "3.12", features = ["json"] } serde_json = "1.0" lalrpop-util = { version = "0.22.0", features = ["lexer"] } lazy_static = "1.4" diff --git a/cedar-policy-validator/Cargo.toml b/cedar-policy-validator/Cargo.toml index 6a4b35483..bc42f34a5 100644 --- a/cedar-policy-validator/Cargo.toml +++ b/cedar-policy-validator/Cargo.toml @@ -14,7 +14,7 @@ repository.workspace = true cedar-policy-core = { version = "=4.3.0", path = "../cedar-policy-core" } serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0", features = ["preserve_order"] } -serde_with = "3.0" +serde_with = "3.12" miette = "7.4.0" thiserror = "2.0" itertools = "0.13" diff --git a/cedar-policy/Cargo.toml b/cedar-policy/Cargo.toml index 5558a8b63..6dfddb30e 100644 --- a/cedar-policy/Cargo.toml +++ b/cedar-policy/Cargo.toml @@ -23,7 +23,7 @@ miette = "7.4.0" thiserror = "2.0" smol_str = { version = "0.3", features = ["serde"] } dhat = { version = "0.3.2", optional = true } -serde_with = "3.3.0" +serde_with = "3.12.0" nonempty = "0.10" prost = { version = "0.13", optional = true } @@ -72,7 +72,7 @@ cedar-policy-core = { version = "=4.3.0", features = [ # NON-CRYPTOGRAPHIC random number generators oorandom = "11.1" -proptest = "1.0.0" +proptest = "1.6.0" [[bench]] name = "cedar_benchmarks" From 9e1086c8dcc6ec249aba3d8d445d458bbfcd8359 Mon Sep 17 00:00:00 2001 From: shaobo-he-aws <130499339+shaobo-he-aws@users.noreply.github.com> Date: Mon, 30 Dec 2024 09:41:47 -0800 Subject: [PATCH 10/16] Apply clippy auto-fixes (#1376) Signed-off-by: Shaobo He --- cedar-policy-cli/tests/sample.rs | 22 +-- cedar-policy-core/src/ast/policy_set.rs | 24 +-- cedar-policy-core/src/entities.rs | 29 ++- cedar-policy-core/src/evaluator.rs | 6 +- cedar-policy-core/src/extensions/datetime.rs | 1 + cedar-policy-core/src/extensions/ipaddr.rs | 1 + cedar-policy-core/src/parser.rs | 1 + cedar-policy-core/src/parser/cst_to_ast.rs | 1 + cedar-policy-core/src/parser/text_to_cst.rs | 7 +- .../src/cedar_schema/test.rs | 3 +- cedar-policy-validator/src/json_schema.rs | 27 +-- cedar-policy-validator/src/schema.rs | 2 + cedar-policy-validator/src/typecheck/test.rs | 1 + cedar-policy/src/ffi/check_parse.rs | 36 ++-- cedar-policy/src/prop_test_policy_set.rs | 2 +- cedar-policy/src/tests.rs | 171 ++++++++---------- 16 files changed, 148 insertions(+), 186 deletions(-) diff --git a/cedar-policy-cli/tests/sample.rs b/cedar-policy-cli/tests/sample.rs index bf2c14d23..ae36bca5a 100644 --- a/cedar-policy-cli/tests/sample.rs +++ b/cedar-policy-cli/tests/sample.rs @@ -106,7 +106,7 @@ fn run_link_test( links_file: impl Into, template_id: impl Into, linked_id: impl Into, - env: HashMap, + env: impl IntoIterator, expected: CedarExitCode, ) { let cmd = LinkArgs { @@ -117,7 +117,9 @@ fn run_link_test( }, template_id: template_id.into(), new_id: linked_id.into(), - arguments: Arguments { data: env }, + arguments: Arguments { + data: HashMap::from_iter(env), + }, }; let output = link(&cmd); assert_eq!(output, expected); @@ -792,9 +794,7 @@ fn test_link_samples() { &linked_file_name, "AccessVacation", "AliceAccess", - [(SlotId::principal(), "User::\"alice\"".to_string())] - .into_iter() - .collect(), + [(SlotId::principal(), "User::\"alice\"".to_string())], CedarExitCode::Failure, ); @@ -803,9 +803,7 @@ fn test_link_samples() { &linked_file_name, "AccessVacation", "AliceAccess", - [(SlotId::principal(), "invalid".to_string())] - .into_iter() - .collect(), + [(SlotId::principal(), "invalid".to_string())], CedarExitCode::Failure, ); @@ -814,9 +812,7 @@ fn test_link_samples() { &linked_file_name, "AccessVacation", "AliceAccess", - [(SlotId::principal(), "User::\"alice\"".to_string())] - .into_iter() - .collect(), + [(SlotId::principal(), "User::\"alice\"".to_string())], CedarExitCode::Success, ); @@ -845,9 +841,7 @@ fn test_link_samples() { &linked_file_name, "AccessVacation", "BobAccess", - [(SlotId::principal(), "User::\"bob\"".to_string())] - .into_iter() - .collect(), + [(SlotId::principal(), "User::\"bob\"".to_string())], CedarExitCode::Success, ); diff --git a/cedar-policy-core/src/ast/policy_set.rs b/cedar-policy-core/src/ast/policy_set.rs index af844f8a5..1508c7922 100644 --- a/cedar-policy-core/src/ast/policy_set.rs +++ b/cedar-policy-core/src/ast/policy_set.rs @@ -630,11 +630,10 @@ mod test { .expect("Failed to parse"); pset.add_template(template).expect("Add failed"); - let env: HashMap = [( + let env: HashMap = std::iter::once(( SlotId::principal(), r#"Test::"test""#.parse().expect("Failed to parse"), - )] - .into_iter() + )) .collect(); let r = pset.link(PolicyID::from_string("t"), PolicyID::from_string("id"), env); @@ -669,11 +668,10 @@ mod test { ) .expect("Failed to parse"), ); - let env1: HashMap = [( + let env1: HashMap = std::iter::once(( SlotId::principal(), r#"Test::"test1""#.parse().expect("Failed to parse"), - )] - .into_iter() + )) .collect(); let p1 = Template::link(Arc::clone(&template), PolicyID::from_string("link"), env1) @@ -686,11 +684,10 @@ mod test { "Adding link should implicitly add the template" ); - let env2: HashMap = [( + let env2: HashMap = std::iter::once(( SlotId::principal(), r#"Test::"test2""#.parse().expect("Failed to parse"), - )] - .into_iter() + )) .collect(); let p2 = Template::link( @@ -717,11 +714,10 @@ mod test { ) .expect("Failed to parse"), ); - let env3: HashMap = [( + let env3: HashMap = std::iter::once(( SlotId::resource(), r#"Test::"test3""#.parse().expect("Failed to parse"), - )] - .into_iter() + )) .collect(); let p4 = Template::link( @@ -781,9 +777,7 @@ mod test { set.link( PolicyID::from_string("template"), PolicyID::from_string("id"), - [(SlotId::principal(), EntityUID::with_eid("eid"))] - .into_iter() - .collect(), + std::iter::once((SlotId::principal(), EntityUID::with_eid("eid"))).collect(), ) .expect("Linking failed!"); assert_eq!(set.static_policies().count(), 1); diff --git a/cedar-policy-core/src/entities.rs b/cedar-policy-core/src/entities.rs index b5c039b70..6ea78cef0 100644 --- a/cedar-policy-core/src/entities.rs +++ b/cedar-policy-core/src/entities.rs @@ -508,6 +508,7 @@ pub enum TCComputation { #[cfg(test)] // PANIC SAFETY unit tests #[allow(clippy::panic)] +#[allow(clippy::cognitive_complexity)] mod json_parsing_tests { use super::*; @@ -1731,13 +1732,13 @@ mod json_parsing_tests { } /// helper function - fn test_entities() -> (Entity, Entity, Entity, Entity) { - ( + fn test_entities() -> [Entity; 4] { + [ Entity::with_uid(EntityUID::with_eid("test_principal")), Entity::with_uid(EntityUID::with_eid("test_action")), Entity::with_uid(EntityUID::with_eid("test_resource")), Entity::with_uid(EntityUID::with_eid("test")), - ) + ] } /// Test that we can take an Entities, write it to JSON, parse that JSON @@ -1750,9 +1751,8 @@ mod json_parsing_tests { roundtrip(&empty_entities).expect("should roundtrip without errors") ); - let (e0, e1, e2, e3) = test_entities(); let entities = Entities::from_entities( - [e0, e1, e2, e3], + test_entities(), None::<&NoEntitiesSchema>, TCComputation::ComputeNow, Extensions::none(), @@ -1960,17 +1960,18 @@ mod json_parsing_tests { // PANIC SAFETY: Unit Test Code #[allow(clippy::panic)] +#[allow(clippy::cognitive_complexity)] #[cfg(test)] -// PANIC SAFETY unit tests -#[allow(clippy::panic)] mod entities_tests { use super::*; #[test] fn empty_entities() { let e = Entities::new(); - let es = e.iter().collect::>(); - assert!(es.is_empty(), "This vec should be empty"); + assert!( + e.iter().next().is_none(), + "The entity store should be empty" + ); } /// helper function @@ -2051,6 +2052,7 @@ mod entities_tests { // PANIC SAFETY: Unit Test Code #[allow(clippy::panic)] +#[allow(clippy::cognitive_complexity)] #[cfg(test)] mod schema_based_parsing_tests { use super::json::NullEntityTypeDescription; @@ -2079,9 +2081,7 @@ mod schema_based_parsing_tests { r#"Action::"view""# => Some(Arc::new(Entity::new_with_attr_partial_value( action.clone(), [(SmolStr::from("foo"), PartialValue::from(34))], - [r#"Action::"readOnly""#.parse().expect("valid uid")] - .into_iter() - .collect(), + std::iter::once(r#"Action::"readOnly""#.parse().expect("valid uid")).collect(), ))), r#"Action::"readOnly""# => Some(Arc::new(Entity::with_uid( r#"Action::"readOnly""#.parse().expect("valid uid"), @@ -2175,11 +2175,10 @@ mod schema_based_parsing_tests { ( "inner3".into(), AttributeType::required(SchemaType::Record { - attrs: [( + attrs: std::iter::once(( "innerinner".into(), AttributeType::required(employee_ty()), - )] - .into_iter() + )) .collect(), open_attrs: false, }), diff --git a/cedar-policy-core/src/evaluator.rs b/cedar-policy-core/src/evaluator.rs index a8b30e520..864eaca9e 100644 --- a/cedar-policy-core/src/evaluator.rs +++ b/cedar-policy-core/src/evaluator.rs @@ -988,6 +988,7 @@ fn stack_size_check() -> Result<()> { // PANIC SAFETY: Unit Test Code #[allow(clippy::panic)] +#[allow(clippy::cognitive_complexity)] #[cfg(test)] pub(crate) mod test { use std::str::FromStr; @@ -5033,9 +5034,8 @@ pub(crate) mod test { Either::Right(expr) => { println!("{expr}"); assert!(expr.contains_unknown()); - let m: HashMap<_, _> = [("principal".into(), Value::from(euid))] - .into_iter() - .collect(); + let m: HashMap<_, _> = + std::iter::once(("principal".into(), Value::from(euid))).collect(); let new_expr = expr.substitute_typed(&m).unwrap(); assert_eq!( e.partial_interpret(&new_expr, &HashMap::new()) diff --git a/cedar-policy-core/src/extensions/datetime.rs b/cedar-policy-core/src/extensions/datetime.rs index 31e43acaf..5b97b4852 100644 --- a/cedar-policy-core/src/extensions/datetime.rs +++ b/cedar-policy-core/src/extensions/datetime.rs @@ -719,6 +719,7 @@ pub fn extension() -> Extension { } #[cfg(test)] +#[allow(clippy::cognitive_complexity)] mod tests { use std::{str::FromStr, sync::Arc}; diff --git a/cedar-policy-core/src/extensions/ipaddr.rs b/cedar-policy-core/src/extensions/ipaddr.rs index a5c623043..590f962bd 100644 --- a/cedar-policy-core/src/extensions/ipaddr.rs +++ b/cedar-policy-core/src/extensions/ipaddr.rs @@ -444,6 +444,7 @@ pub fn extension() -> Extension { // PANIC SAFETY: Unit Test Code #[allow(clippy::panic)] #[cfg(test)] +#[allow(clippy::cognitive_complexity)] mod tests { use super::*; use crate::ast::{Expr, Type, Value}; diff --git a/cedar-policy-core/src/parser.rs b/cedar-policy-core/src/parser.rs index 428adb6b4..b6d06dc0e 100644 --- a/cedar-policy-core/src/parser.rs +++ b/cedar-policy-core/src/parser.rs @@ -327,6 +327,7 @@ pub(crate) mod test_utils { // PANIC SAFETY: Unit Test Code #[allow(clippy::panic, clippy::indexing_slicing)] +#[allow(clippy::cognitive_complexity)] #[cfg(test)] /// Tests for the top-level parsing APIs mod tests { diff --git a/cedar-policy-core/src/parser/cst_to_ast.rs b/cedar-policy-core/src/parser/cst_to_ast.rs index bb329ea4c..882ce4480 100644 --- a/cedar-policy-core/src/parser/cst_to_ast.rs +++ b/cedar-policy-core/src/parser/cst_to_ast.rs @@ -2046,6 +2046,7 @@ fn construct_expr_record(kvs: Vec<(SmolStr, ast::Expr)>, loc: Loc) -> Result>(); - assert!(success.len() == 2); + assert_eq!(policies.0.into_iter().filter_map(|p| p.node).count(), 2); } #[test] diff --git a/cedar-policy-validator/src/cedar_schema/test.rs b/cedar-policy-validator/src/cedar_schema/test.rs index bf72a6abb..4cf7628c5 100644 --- a/cedar-policy-validator/src/cedar_schema/test.rs +++ b/cedar-policy-validator/src/cedar_schema/test.rs @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +#![allow(clippy::cognitive_complexity)] // PANIC SAFETY: unit tests #[allow(clippy::panic)] @@ -737,7 +738,7 @@ namespace Baz {action "Foo" appliesTo { }), ); assert_has_type( - &attributes.get("viewACL").unwrap(), + attributes.get("viewACL").unwrap(), json_schema::Type::Type(json_schema::TypeVariant::EntityOrCommon { type_name: "DocumentShare".parse().unwrap(), }), diff --git a/cedar-policy-validator/src/json_schema.rs b/cedar-policy-validator/src/json_schema.rs index f05a9cc10..d5825c690 100644 --- a/cedar-policy-validator/src/json_schema.rs +++ b/cedar-policy-validator/src/json_schema.rs @@ -2779,8 +2779,7 @@ mod test_json_roundtrip { entity_types: BTreeMap::new(), actions: BTreeMap::new(), annotations: Annotations::new(), - } - .into(), + }, )])); roundtrip(fragment); } @@ -2794,8 +2793,7 @@ mod test_json_roundtrip { entity_types: BTreeMap::new(), actions: BTreeMap::new(), annotations: Annotations::new(), - } - .into(), + }, )])); roundtrip(fragment); } @@ -2816,8 +2814,7 @@ mod test_json_roundtrip { }))), tags: None, annotations: Annotations::new(), - } - .into(), + }, )]), actions: BTreeMap::from([( "action".into(), @@ -2835,12 +2832,10 @@ mod test_json_roundtrip { }), member_of: None, annotations: Annotations::new(), - } - .into(), + }, )]), annotations: Annotations::new(), - } - .into(), + }, )])); roundtrip(fragment); } @@ -2864,13 +2859,11 @@ mod test_json_roundtrip { ))), tags: None, annotations: Annotations::new(), - } - .into(), + }, )]), actions: BTreeMap::new(), annotations: Annotations::new(), - } - .into(), + }, ), ( None, @@ -2893,12 +2886,10 @@ mod test_json_roundtrip { }), member_of: None, annotations: Annotations::new(), - } - .into(), + }, )]), annotations: Annotations::new(), - } - .into(), + }, ), ])); roundtrip(fragment); diff --git a/cedar-policy-validator/src/schema.rs b/cedar-policy-validator/src/schema.rs index c3241fc83..c668b04d1 100644 --- a/cedar-policy-validator/src/schema.rs +++ b/cedar-policy-validator/src/schema.rs @@ -3588,6 +3588,7 @@ pub(crate) mod test { mod test_579; // located in separate file test_579.rs #[cfg(test)] +#[allow(clippy::cognitive_complexity)] mod test_rfc70 { use super::test::utils::*; use super::ValidatorSchema; @@ -4578,6 +4579,7 @@ mod test_rfc70 { /// Tests involving entity tags (RFC 82) #[cfg(test)] +#[allow(clippy::cognitive_complexity)] mod entity_tags { use super::{test::utils::*, *}; use cedar_policy_core::{ diff --git a/cedar-policy-validator/src/typecheck/test.rs b/cedar-policy-validator/src/typecheck/test.rs index 813a73e68..ce7343260 100644 --- a/cedar-policy-validator/src/typecheck/test.rs +++ b/cedar-policy-validator/src/typecheck/test.rs @@ -19,6 +19,7 @@ #![allow(clippy::panic)] // PANIC SAFETY unit tests #![allow(clippy::indexing_slicing)] +#![allow(clippy::cognitive_complexity)] pub(crate) mod test_utils; diff --git a/cedar-policy/src/ffi/check_parse.rs b/cedar-policy/src/ffi/check_parse.rs index 28e45f2b7..eb18f080f 100644 --- a/cedar-policy/src/ffi/check_parse.rs +++ b/cedar-policy/src/ffi/check_parse.rs @@ -260,12 +260,12 @@ mod test { use serde_json::json; #[track_caller] - fn assert_check_parse_is_ok(parse_result: CheckParseAnswer) { + fn assert_check_parse_is_ok(parse_result: &CheckParseAnswer) { assert_matches!(parse_result, CheckParseAnswer::Success); } #[track_caller] - fn assert_check_parse_is_err(parse_result: CheckParseAnswer) -> Vec { + fn assert_check_parse_is_err(parse_result: &CheckParseAnswer) -> &[DetailedError] { assert_matches!( parse_result, CheckParseAnswer::Failure { errors } => errors @@ -278,7 +278,7 @@ mod test { "staticPolicies": "permit(principal, action, resource);" }); let answer = serde_json::from_value(check_parse_policy_set_json(call).unwrap()).unwrap(); - assert_check_parse_is_ok(answer); + assert_check_parse_is_ok(&answer); } #[test] @@ -287,7 +287,7 @@ mod test { "staticPolicies": "forbid(principal, action, resource); permit(principal == User::\"alice\", action == Action::\"view\", resource in Albums::\"alice_albums\");" }); let answer = serde_json::from_value(check_parse_policy_set_json(call).unwrap()).unwrap(); - assert_check_parse_is_ok(answer); + assert_check_parse_is_ok(&answer); } #[test] @@ -296,9 +296,9 @@ mod test { "staticPolicies": "forbid(principal, action, resource);permit(2pac, action, resource)" }); let answer = serde_json::from_value(check_parse_policy_set_json(call).unwrap()).unwrap(); - let errs = assert_check_parse_is_err(answer); + let errs = assert_check_parse_is_err(&answer); assert_exactly_one_error( - &errs, + errs, "failed to parse policies from string: unexpected token `2`", None, ); @@ -312,14 +312,14 @@ mod test { } }); let answer = serde_json::from_value(check_parse_policy_set_json(call).unwrap()).unwrap(); - assert_check_parse_is_ok(answer); + assert_check_parse_is_ok(&answer); } #[test] fn check_parse_schema_succeeds_empty_schema() { let call = json!({}); let answer = serde_json::from_value(check_parse_schema_json(call).unwrap()).unwrap(); - assert_check_parse_is_ok(answer); + assert_check_parse_is_ok(&answer); } #[test] @@ -331,7 +331,7 @@ mod test { } }); let answer = serde_json::from_value(check_parse_schema_json(call).unwrap()).unwrap(); - assert_check_parse_is_ok(answer); + assert_check_parse_is_ok(&answer); } #[test] @@ -342,9 +342,9 @@ mod test { } }); let answer = serde_json::from_value(check_parse_schema_json(call).unwrap()).unwrap(); - let errs = assert_check_parse_is_err(answer); + let errs = assert_check_parse_is_err(&answer); assert_exactly_one_error( - &errs, + errs, "failed to parse schema from JSON: missing field `actions`", None, ); @@ -389,7 +389,7 @@ mod test { } }); let answer = serde_json::from_value(check_parse_entities_json(call).unwrap()).unwrap(); - assert_check_parse_is_ok(answer); + assert_check_parse_is_ok(&answer); } #[test] @@ -410,7 +410,7 @@ mod test { ] }); let answer = serde_json::from_value(check_parse_entities_json(call).unwrap()).unwrap(); - assert_check_parse_is_ok(answer); + assert_check_parse_is_ok(&answer); } #[test] @@ -445,9 +445,9 @@ mod test { } }); let answer = serde_json::from_value(check_parse_entities_json(call).unwrap()).unwrap(); - let errs = assert_check_parse_is_err(answer); + let errs = assert_check_parse_is_err(&answer); assert_exactly_one_error( - &errs, + errs, "error during entity deserialization: in uid field of , expected a literal entity reference, but got `\"TheNamespace::User::\\\"alice\\\"\"`", Some("literal entity references can be made with `{ \"type\": \"SomeType\", \"id\": \"SomeId\" }`") ); @@ -491,7 +491,7 @@ mod test { }); let answer = serde_json::from_value(check_parse_context_json(call).unwrap()).unwrap(); - assert_check_parse_is_ok(answer); + assert_check_parse_is_ok(&answer); } #[test] @@ -531,7 +531,7 @@ mod test { } }); let answer = serde_json::from_value(check_parse_context_json(call).unwrap()).unwrap(); - let errs = assert_check_parse_is_err(answer); - assert_exactly_one_error(&errs, "while parsing context, expected the record to have an attribute `referrer`, but it does not", None); + let errs = assert_check_parse_is_err(&answer); + assert_exactly_one_error(errs, "while parsing context, expected the record to have an attribute `referrer`, but it does not", None); } } diff --git a/cedar-policy/src/prop_test_policy_set.rs b/cedar-policy/src/prop_test_policy_set.rs index 6772311f6..4d81a632f 100644 --- a/cedar-policy/src/prop_test_policy_set.rs +++ b/cedar-policy/src/prop_test_policy_set.rs @@ -112,7 +112,7 @@ impl PolicySetModel { panic!("template to link map should have Vec for existing template") } }; - assert!(self.link_to_template_map.get(policy_name).is_none()); + assert!(!self.link_to_template_map.contains_key(policy_name)); self.link_to_template_map .insert(policy_name.to_owned(), template_name.clone()); } diff --git a/cedar-policy/src/tests.rs b/cedar-policy/src/tests.rs index e8b0fb779..7ed580b05 100644 --- a/cedar-policy/src/tests.rs +++ b/cedar-policy/src/tests.rs @@ -17,6 +17,7 @@ #![cfg(test)] // PANIC SAFETY unit tests #![allow(clippy::panic)] +#![allow(clippy::cognitive_complexity, clippy::too_many_lines)] use super::*; @@ -4858,7 +4859,7 @@ mod policy_set_est_tests { #[test] fn test_partition_fold_err() { let even_or_odd = |s: &str| { - i64::from_str_radix(s, 10).map(|i| { + s.parse::().map(|i| { if i % 2 == 0 { Either::Left(i) } else { @@ -5074,10 +5075,11 @@ mod policy_set_est_tests { r#"User::"John""#.parse().unwrap() )])) ); - if let Err(_) = policyset + if policyset .get_linked_policies(PolicyId::new("template")) .unwrap() .exactly_one() + .is_err() { panic!("Should have exactly one"); }; @@ -6323,30 +6325,25 @@ mod reserved_keywords_in_policies { } #[track_caller] - fn assert_valid_expression(src: String) { - assert_matches!(Expression::from_str(&src), Ok(_)); + fn assert_valid_expression(src: &str) { + assert_matches!(Expression::from_str(src), Ok(_)); } #[track_caller] - fn assert_invalid_expression(src: String, error: String, underline: String) { - let expected_err = ExpectedErrorMessageBuilder::error(&error) - .exactly_one_underline(&underline) + fn assert_invalid_expression(src: &str, error: &str, underline: &str) { + let expected_err = ExpectedErrorMessageBuilder::error(error) + .exactly_one_underline(underline) .build(); - assert_matches!(Expression::from_str(&src), Err(err) => expect_err(&*src, &Report::new(err), &expected_err)); + assert_matches!(Expression::from_str(src), Err(err) => expect_err(src, &Report::new(err), &expected_err)); } #[track_caller] - fn assert_invalid_expression_with_help( - src: String, - error: String, - underline: String, - help: String, - ) { - let expected_err = ExpectedErrorMessageBuilder::error(&error) - .exactly_one_underline(&underline) - .help(&help) + fn assert_invalid_expression_with_help(src: &str, error: &str, underline: &str, help: &str) { + let expected_err = ExpectedErrorMessageBuilder::error(error) + .exactly_one_underline(underline) + .help(help) .build(); - assert_matches!(Expression::from_str(&src), Err(err) => expect_err(&*src, &Report::new(err), &expected_err)); + assert_matches!(Expression::from_str(src), Err(err) => expect_err(src, &Report::new(err), &expected_err)); } #[test] @@ -6367,16 +6364,16 @@ mod reserved_keywords_in_policies { .chain(RESERVED_NAMESPACE.iter()) .chain(OTHER_SPECIAL_IDENTS.iter()) .for_each(|id| { - assert_valid_expression(format!("{{ \"{id}\": 1 }}")); - assert_valid_expression(format!("principal has \"{id}\"")); - assert_valid_expression(format!("principal[\"{id}\"] == \"foo\"")); + assert_valid_expression(&format!("{{ \"{id}\": 1 }}")); + assert_valid_expression(&format!("principal has \"{id}\"")); + assert_valid_expression(&format!("principal[\"{id}\"] == \"foo\"")); }); // No restrictions on OTHER_SPECIAL_IDENTS for id in &OTHER_SPECIAL_IDENTS { - assert_valid_expression(format!("{{ {id}: 1 }}")); - assert_valid_expression(format!("principal has {id}")); - assert_valid_expression(format!("principal.{id} == \"foo\"")); + assert_valid_expression(&format!("{{ {id}: 1 }}")); + assert_valid_expression(&format!("principal has {id}")); + assert_valid_expression(&format!("principal.{id} == \"foo\"")); } // RESERVED_IDENTS cannot be used as keys without quotes @@ -6385,66 +6382,62 @@ mod reserved_keywords_in_policies { match id { "true" | "false" => { assert_invalid_expression_with_help( - format!("{{ {id}: 1 }}"), - format!("invalid attribute name: {id}"), - id.into(), - "attribute names can either be identifiers or string literals".into(), + &format!("{{ {id}: 1 }}"), + &format!("invalid attribute name: {id}"), + id, + "attribute names can either be identifiers or string literals", ); assert_invalid_expression( - format!("principal has {id}"), - RESERVED_IDENT_MSG(id), - id.to_string(), + &format!("principal has {id}"), + &RESERVED_IDENT_MSG(id), + id, ); } "if" => { assert_invalid_expression( - format!("{{ {id}: 1 }}"), - RESERVED_IDENT_MSG(id), - format!("{id}: 1"), + &format!("{{ {id}: 1 }}"), + &RESERVED_IDENT_MSG(id), + &format!("{id}: 1"), ); assert_invalid_expression( - format!("principal has {id}"), - RESERVED_IDENT_MSG(id), - id.to_string(), + &format!("principal has {id}"), + &RESERVED_IDENT_MSG(id), + id, ); } _ => { assert_invalid_expression( - format!("{{ {id}: 1 }}"), - RESERVED_IDENT_MSG(id), - id.into(), + &format!("{{ {id}: 1 }}"), + &RESERVED_IDENT_MSG(id), + id, ); assert_invalid_expression( - format!("principal has {id}"), - RESERVED_IDENT_MSG(id), - id.into(), + &format!("principal has {id}"), + &RESERVED_IDENT_MSG(id), + id, ); } } // this case leads to a consistent error for all keywords assert_invalid_expression( - format!("principal.{id} == \"foo\""), - RESERVED_IDENT_MSG(id), - id.into(), + &format!("principal.{id} == \"foo\""), + &RESERVED_IDENT_MSG(id), + id, ); } // RESERVED_NAMESPACE cannot be used as keys without quotes for id in RESERVED_NAMESPACE { + assert_invalid_expression(&format!("{{ {id}: 1 }}"), &RESERVED_NAMESPACE_MSG(id), id); assert_invalid_expression( - format!("{{ {id}: 1 }}"), - RESERVED_NAMESPACE_MSG(id), - id.into(), + &format!("principal has {id}"), + &RESERVED_NAMESPACE_MSG(id), + id, ); assert_invalid_expression( - format!("principal has {id}"), - RESERVED_NAMESPACE_MSG(id), - id.into(), - ); - assert_invalid_expression( - format!("principal.{id} == \"foo\""), - RESERVED_NAMESPACE_MSG(id), - id.into(), + &format!("principal.{id} == \"foo\""), + &RESERVED_NAMESPACE_MSG(id), + id, ); } } @@ -6453,35 +6446,31 @@ mod reserved_keywords_in_policies { fn test_reserved_namespace_elements() { // No restrictions on OTHER_SPECIAL_IDENTS for id in &OTHER_SPECIAL_IDENTS { - assert_valid_expression(format!("foo::{id}::\"bar\"")); - assert_valid_expression(format!("principal is {id}::foo")); + assert_valid_expression(&format!("foo::{id}::\"bar\"")); + assert_valid_expression(&format!("principal is {id}::foo")); } // RESERVED_IDENTS cannot be used in namespaces for id in RESERVED_IDENTS { + assert_invalid_expression(&format!("foo::{id}::\"bar\""), &RESERVED_IDENT_MSG(id), id); assert_invalid_expression( - format!("foo::{id}::\"bar\""), - RESERVED_IDENT_MSG(id), - id.into(), - ); - assert_invalid_expression( - format!("principal is {id}::foo"), - RESERVED_IDENT_MSG(id), - id.into(), + &format!("principal is {id}::foo"), + &RESERVED_IDENT_MSG(id), + id, ); } // RESERVED_NAMESPACE cannot be used in namespaces for id in RESERVED_NAMESPACE { assert_invalid_expression( - format!("foo::{id}::\"bar\""), - RESERVED_NAMESPACE_MSG(&format!("foo::{id}")), - format!("foo::{id}"), + &format!("foo::{id}::\"bar\""), + &RESERVED_NAMESPACE_MSG(&format!("foo::{id}")), + &format!("foo::{id}"), ); assert_invalid_expression( - format!("principal is {id}::foo"), - RESERVED_NAMESPACE_MSG(&format!("{id}::foo")), - format!("{id}::foo"), + &format!("principal is {id}::foo"), + &RESERVED_NAMESPACE_MSG(&format!("{id}::foo")), + &format!("{id}::foo"), ); } } @@ -6493,40 +6482,32 @@ mod reserved_keywords_in_policies { for id in RESERVED_IDENTS { assert_invalid_expression( - format!("extension::function::{id}(\"foo\")"), - RESERVED_IDENT_MSG(id), - id.into(), - ); - assert_invalid_expression( - format!("context.{id}(1)"), - RESERVED_IDENT_MSG(id), - id.into(), + &format!("extension::function::{id}(\"foo\")"), + &RESERVED_IDENT_MSG(id), + id, ); + assert_invalid_expression(&format!("context.{id}(1)"), &RESERVED_IDENT_MSG(id), id); } for id in RESERVED_NAMESPACE { assert_invalid_expression( - format!("extension::function::{id}(\"foo\")"), - RESERVED_NAMESPACE_MSG(&format!("extension::function::{id}")), - format!("extension::function::{id}"), - ); - assert_invalid_expression( - format!("context.{id}(1)"), - RESERVED_NAMESPACE_MSG(id), - id.into(), + &format!("extension::function::{id}(\"foo\")"), + &RESERVED_NAMESPACE_MSG(&format!("extension::function::{id}")), + &format!("extension::function::{id}"), ); + assert_invalid_expression(&format!("context.{id}(1)"), &RESERVED_NAMESPACE_MSG(id), id); } for id in OTHER_SPECIAL_IDENTS { assert_invalid_expression( - format!("extension::function::{id}(\"foo\")"), - format!("`extension::function::{id}` is not a valid function"), - format!("extension::function::{id}(\"foo\")"), + &format!("extension::function::{id}(\"foo\")"), + &format!("`extension::function::{id}` is not a valid function"), + &format!("extension::function::{id}(\"foo\")"), ); assert_invalid_expression( - format!("context.{id}(1)"), - format!("`{id}` is not a valid method"), - format!("context.{id}(1)"), + &format!("context.{id}(1)"), + &format!("`{id}` is not a valid method"), + &format!("context.{id}(1)"), ); } } From 6dfa6542ac78f3e8715d6ebbfd0d12289c744878 Mon Sep 17 00:00:00 2001 From: shaobo-he-aws <130499339+shaobo-he-aws@users.noreply.github.com> Date: Mon, 30 Dec 2024 17:56:10 -0800 Subject: [PATCH 11/16] =?UTF-8?q?Allow=20schema=20annotation=20keys=20to?= =?UTF-8?q?=20be=20any=20identifiers=20including=20reserved=E2=80=A6=20(#1?= =?UTF-8?q?397)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Shaobo He --- .../src/cedar_schema/grammar.lalrpop | 30 ++++------ .../src/cedar_schema/test.rs | 57 +++++++++++++++++++ 2 files changed, 68 insertions(+), 19 deletions(-) diff --git a/cedar-policy-validator/src/cedar_schema/grammar.lalrpop b/cedar-policy-validator/src/cedar_schema/grammar.lalrpop index 3db5e48aa..910528352 100644 --- a/cedar-policy-validator/src/cedar_schema/grammar.lalrpop +++ b/cedar-policy-validator/src/cedar_schema/grammar.lalrpop @@ -90,14 +90,7 @@ match { } -#[inline] -AnyIdent: Node = { - - // `IDENTIFIER` should produce a valid `AnyId` - => Node::with_source_loc(AnyId::from_str(id).unwrap(), Loc::new(l..r, Arc::clone(src))), -} - -// Annotations := {'@' IDENTIFIER '(' String ')'} +// Annotations := {'@' AnyIdent '(' String ')'} Annotation: Node<(Node, Option>)> = { "@" ")")?> => Node::with_source_loc((key, value), Loc::new(l..r, Arc::clone(src))) } @@ -225,8 +218,15 @@ Comma: Vec = { }, } -// IDENT := ['_''a'-'z''A'-'Z']['_''a'-'z''A'-'Z''0'-'9']* +// IDENT := ['_''a'-'z''A'-'Z']['_''a'-'z''A'-'Z''0'-'9']* - RESERVED Ident: Node = { + =>? Id::from_str(id.node.as_ref()).map(|i| Node::with_source_loc(i, id.loc.clone())).map_err(|err : cedar_policy_core::parser::err::ParseErrors| ParseError::User { + error: UserError::ReservedIdentifierUsed(Node::with_source_loc(id.node.to_smolstr(), id.loc.clone())) + }), +} + +// AnyIdent := ['_''a'-'z''A'-'Z']['_''a'-'z''A'-'Z''0'-'9']* +AnyIdent: Node = { NAMESPACE => Node::with_source_loc("namespace".parse().unwrap(), Loc::new(l..r, Arc::clone(src))), ENTITY @@ -256,17 +256,9 @@ Ident: Node = { TYPE => Node::with_source_loc("type".parse().unwrap(), Loc::new(l..r, Arc::clone(src))), IN - =>? Err(ParseError::User { - error: UserError::ReservedIdentifierUsed(Node::with_source_loc("in".into(), Loc::new(l..r, Arc::clone(src)))) - }), + => Node::with_source_loc("in".parse().unwrap(), Loc::new(l..r, Arc::clone(src))), - =>? Id::from_str(i) - .map(|id : Id| Node::with_source_loc(id, Loc::new(l..r, Arc::clone(src)))) - .map_err(|err : cedar_policy_core::parser::err::ParseErrors| - ParseError::User { - error: UserError::ReservedIdentifierUsed(Node::with_source_loc(i.to_smolstr(), Loc::new(l..r, Arc::clone(src)))) - } - ) + => Node::with_source_loc(i.parse().unwrap(), Loc::new(l..r, Arc::clone(src))), } STR: Node = { diff --git a/cedar-policy-validator/src/cedar_schema/test.rs b/cedar-policy-validator/src/cedar_schema/test.rs index 4cf7628c5..19a14e4c0 100644 --- a/cedar-policy-validator/src/cedar_schema/test.rs +++ b/cedar-policy-validator/src/cedar_schema/test.rs @@ -1178,6 +1178,8 @@ mod translator_tests { ValidatorSchema, }; + use super::SPECIAL_IDS; + // We allow translating schemas that violate RFC 52 to `json_schema::Fragment`. // The violations are reported during further translation to `ValidatorSchema` #[test] @@ -1999,6 +2001,28 @@ mod translator_tests { .expect("should translate to JSON schema"); assert_eq!(serde_json::to_value(schema).unwrap(), json_value); } + + #[test] + fn any_id() { + for id in SPECIAL_IDS { + test_translation( + &format!("@{id} entity User {{}};"), + serde_json::json!({ + "": { + "entityTypes": { + "User": { + "annotations": { + id: "", + } + } + }, + "actions": {}, + } + }), + ) + } + } + #[test] fn annotations() { // namespace annotations @@ -2634,6 +2658,28 @@ mod entity_tags { } } +#[cfg(test)] +pub(crate) const SPECIAL_IDS: [&str; 18] = [ + "principal", + "action", + "resource", + "context", + "true", + "false", + "permit", + "forbid", + "when", + "unless", + "in", + "has", + "like", + "is", + "if", + "then", + "else", + "__cedar", +]; + // RFC 48 test cases #[cfg(test)] mod annotations { @@ -2641,6 +2687,17 @@ mod annotations { use crate::cedar_schema::parser::parse_schema; + use super::SPECIAL_IDS; + + // test if annotation keys can be any id + #[test] + fn any_id() { + for id in SPECIAL_IDS { + let schema_str = format!("@{id} entity User {{}};"); + assert_matches!(parse_schema(&schema_str), Ok(_)); + } + } + #[test] fn no_keys() { assert_matches!( From e3e6d85b025c6c9533f343e7bffddeaa3a587b2b Mon Sep 17 00:00:00 2001 From: John Kastner <130772734+john-h-kastner-aws@users.noreply.github.com> Date: Tue, 31 Dec 2024 09:27:11 -0500 Subject: [PATCH 12/16] Enable the pedantic lint `needless_pass_by_value` (#1398) Signed-off-by: John Kastner --- Cargo.toml | 1 + cedar-policy-cli/src/lib.rs | 8 ++-- cedar-policy-core/src/est/expr.rs | 4 +- cedar-policy-core/src/parser/cst_to_ast.rs | 8 ++-- cedar-policy-core/src/parser/err.rs | 11 +++--- cedar-policy-formatter/src/pprint/doc.rs | 2 +- cedar-policy-formatter/src/pprint/utils.rs | 8 ++-- .../src/cedar_schema/fmt.rs | 37 +++++++++---------- .../src/entity_manifest/type_annotations.rs | 4 +- cedar-policy-validator/src/typecheck.rs | 21 +++++------ cedar-policy-validator/src/types.rs | 14 ++++--- .../src/types/request_env.rs | 2 +- 12 files changed, 61 insertions(+), 59 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 20d2569ef..ad556ee7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ rust-2018-idioms = "deny" # means we'll see it in local runs. [workspace.lints.clippy] nursery = { level = "warn", priority = -1 } +needless_pass_by_value = "warn" # These lints may be worth enforcing, but cause a lot of noise at the moment. use_self = "allow" diff --git a/cedar-policy-cli/src/lib.rs b/cedar-policy-cli/src/lib.rs index 9a001c6b9..089026f20 100644 --- a/cedar-policy-cli/src/lib.rs +++ b/cedar-policy-cli/src/lib.rs @@ -947,7 +947,7 @@ fn translate_policy_inner(args: &TranslatePolicyArgs) -> Result { let translate = match args.direction { PolicyTranslationDirection::CedarToJson => translate_policy_to_json, }; - read_from_file_or_stdin(args.input_file.clone(), "policy").and_then(translate) + read_from_file_or_stdin(args.input_file.as_ref(), "policy").and_then(translate) } pub fn translate_policy(args: &TranslatePolicyArgs) -> CedarExitCode { @@ -984,7 +984,7 @@ fn translate_schema_inner(args: &TranslateSchemaArgs) -> Result { SchemaTranslationDirection::JsonToCedar => translate_schema_to_cedar, SchemaTranslationDirection::CedarToJson => translate_schema_to_json, }; - read_from_file_or_stdin(args.input_file.clone(), "schema").and_then(translate) + read_from_file_or_stdin(args.input_file.as_ref(), "schema").and_then(translate) } pub fn translate_schema(args: &TranslateSchemaArgs) -> CedarExitCode { @@ -1377,7 +1377,7 @@ fn load_entities(entities_filename: impl AsRef, schema: Option<&Schema>) - /// This will rename template-linked policies to the id of their template, which may /// cause id conflicts, so only call this function before instancing /// templates into the policy set. -fn rename_from_id_annotation(ps: PolicySet) -> Result { +fn rename_from_id_annotation(ps: &PolicySet) -> Result { let mut new_ps = PolicySet::new(); let t_iter = ps.templates().map(|t| match t.annotation("id") { None => Ok(t.clone()), @@ -1443,7 +1443,7 @@ fn read_cedar_policy_set( Report::new(err).with_source_code(NamedSource::new(name, ps_str)) }) .wrap_err_with(|| format!("failed to parse {context}"))?; - rename_from_id_annotation(ps) + rename_from_id_annotation(&ps) } /// Read a policy set, static policy or policy template, in Cedar JSON (EST) syntax, from the file given diff --git a/cedar-policy-core/src/est/expr.rs b/cedar-policy-core/src/est/expr.rs index 49edc729c..8e401b44f 100644 --- a/cedar-policy-core/src/est/expr.rs +++ b/cedar-policy-core/src/est/expr.rs @@ -1511,7 +1511,7 @@ impl TryFrom<&Node>> for Expr { extract_single_argument(args, "containsAny()", &access.loc)?, )), "isEmpty" => { - require_zero_arguments(args, "isEmpty()", &access.loc)?; + require_zero_arguments(&args, "isEmpty()", &access.loc)?; Either::Right(Expr::is_empty(left)) } "getTag" => Either::Right(Expr::get_tag( @@ -1584,7 +1584,7 @@ pub fn extract_single_argument( /// Return a wrong arity error if the iterator has any elements. pub fn require_zero_arguments( - args: impl ExactSizeIterator, + args: &impl ExactSizeIterator, fn_name: &'static str, loc: &Loc, ) -> Result<(), ParseErrors> { diff --git a/cedar-policy-core/src/parser/cst_to_ast.rs b/cedar-policy-core/src/parser/cst_to_ast.rs index 882ce4480..88ceecc2a 100644 --- a/cedar-policy-core/src/parser/cst_to_ast.rs +++ b/cedar-policy-core/src/parser/cst_to_ast.rs @@ -490,7 +490,7 @@ impl ast::UnreservedId { "containsAny" => extract_single_argument(args.into_iter(), "containsAny", loc) .map(|arg| construct_method_contains_any(e, arg, loc.clone())), "isEmpty" => { - require_zero_arguments(args.into_iter(), "isEmpty", loc)?; + require_zero_arguments(&args.into_iter(), "isEmpty", loc)?; Ok(construct_method_is_empty(e, loc.clone())) } "getTag" => extract_single_argument(args.into_iter(), "getTag", loc) @@ -984,7 +984,7 @@ impl Node> { }); let (target, field) = flatten_tuple_2(maybe_target, maybe_field)?; Ok(ExprOrSpecial::Expr { - expr: construct_exprs_extended_has(target, field, self.loc.clone()), + expr: construct_exprs_extended_has(target, &field, &self.loc), loc: self.loc.clone(), }) } @@ -1941,7 +1941,7 @@ fn construct_expr_has_attr(t: ast::Expr, s: SmolStr, loc: Loc) -> ast::Expr { fn construct_expr_get_attr(t: ast::Expr, s: SmolStr, loc: Loc) -> ast::Expr { ast::ExprBuilder::new().with_source_loc(loc).get_attr(t, s) } -fn construct_exprs_extended_has(t: ast::Expr, attrs: NonEmpty, loc: Loc) -> ast::Expr { +fn construct_exprs_extended_has(t: ast::Expr, attrs: &NonEmpty, loc: &Loc) -> ast::Expr { let (first, rest) = attrs.split_first(); let has_expr = construct_expr_has_attr(t.clone(), first.to_owned(), loc.clone()); let get_expr = construct_expr_get_attr(t, first.to_owned(), loc.clone()); @@ -1973,7 +1973,7 @@ fn construct_exprs_extended_has(t: ast::Expr, attrs: NonEmpty, loc: Loc has_expr, construct_expr_has_attr(get_expr.clone(), attr.to_owned(), loc.clone()), std::iter::empty(), - &loc, + loc, ), construct_expr_get_attr(get_expr, attr.to_owned(), loc.clone()), ) diff --git a/cedar-policy-core/src/parser/err.rs b/cedar-policy-core/src/parser/err.rs index 49b750523..ab96cf869 100644 --- a/cedar-policy-core/src/parser/err.rs +++ b/cedar-policy-core/src/parser/err.rs @@ -834,11 +834,12 @@ impl ParseErrors { /// Flatten a `Vec` into a single `ParseErrors`, returning /// `None` if the input vector is empty. - pub(crate) fn flatten(v: Vec) -> Option { - let (first, rest) = v.split_first()?; - let mut first = first.clone(); - rest.iter() - .for_each(|errs| first.extend(errs.iter().cloned())); + pub(crate) fn flatten(errs: impl IntoIterator) -> Option { + let mut errs = errs.into_iter(); + let mut first = errs.next()?; + for inner in errs { + first.extend(inner); + } Some(first) } diff --git a/cedar-policy-formatter/src/pprint/doc.rs b/cedar-policy-formatter/src/pprint/doc.rs index 22e920801..bb31f02a5 100644 --- a/cedar-policy-formatter/src/pprint/doc.rs +++ b/cedar-policy-formatter/src/pprint/doc.rs @@ -415,7 +415,7 @@ impl Doc for Node> { } else { RcDoc::text("-") }, - comment.get(i as usize)?.clone(), + comment.get(i as usize)?, RcDoc::nil(), )) }) diff --git a/cedar-policy-formatter/src/pprint/utils.rs b/cedar-policy-formatter/src/pprint/utils.rs index 7ad9d4481..ad8e3d507 100644 --- a/cedar-policy-formatter/src/pprint/utils.rs +++ b/cedar-policy-formatter/src/pprint/utils.rs @@ -14,6 +14,8 @@ * limitations under the License. */ +use std::borrow::Borrow; + use itertools::Itertools; use pretty::RcDoc; @@ -132,11 +134,11 @@ pub fn get_comment_in_range<'src>( /// trailing comment, then it will introduce a newline at the end. pub fn add_comment<'src>( d: RcDoc<'src>, - comment: Comment<'src>, + comment: impl Borrow>, next_doc: RcDoc<'src>, ) -> RcDoc<'src> { - let leading_comment = comment.leading_comment(); - let trailing_comment = comment.trailing_comment(); + let leading_comment = comment.borrow().leading_comment(); + let trailing_comment = comment.borrow().trailing_comment(); let leading_comment_doc = get_leading_comment_doc_from_str(leading_comment); let trailing_comment_doc = get_trailing_comment_doc_from_str(trailing_comment, next_doc); leading_comment_doc.append(d).append(trailing_comment_doc) diff --git a/cedar-policy-validator/src/cedar_schema/fmt.rs b/cedar-policy-validator/src/cedar_schema/fmt.rs index 5f20f11e1..f559f66a2 100644 --- a/cedar-policy-validator/src/cedar_schema/fmt.rs +++ b/cedar-policy-validator/src/cedar_schema/fmt.rs @@ -95,21 +95,22 @@ impl Display for json_schema::RecordType { } } -/// Create a non-empty with borrowed contents from a slice -fn non_empty_slice(v: &[T]) -> Option> { - NonEmpty::collect(v.iter()) -} - -fn fmt_vec(f: &mut std::fmt::Formatter<'_>, ets: NonEmpty) -> std::fmt::Result { - let contents = ets.iter().map(T::to_string).join(", "); - write!(f, "[{contents}]") +fn fmt_non_empty_slice( + f: &mut std::fmt::Formatter<'_>, + (head, tail): (&T, &[T]), +) -> std::fmt::Result { + write!(f, "[{head}")?; + for e in tail { + write!(f, ", {e}")?; + } + write!(f, "]") } impl Display for json_schema::EntityType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if let Some(non_empty) = non_empty_slice(&self.member_of_types) { + if let Some(non_empty) = self.member_of_types.split_first() { write!(f, " in ")?; - fmt_vec(f, non_empty)?; + fmt_non_empty_slice(f, non_empty)?; } let ty = &self.shape; @@ -128,18 +129,14 @@ impl Display for json_schema::EntityType { impl Display for json_schema::ActionType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if let Some(parents) = self - .member_of - .as_ref() - .and_then(|refs| non_empty_slice(refs.as_slice())) - { + if let Some(parents) = self.member_of.as_ref().and_then(|refs| refs.split_first()) { write!(f, " in ")?; - fmt_vec(f, parents)?; + fmt_non_empty_slice(f, parents)?; } if let Some(spec) = &self.applies_to { match ( - non_empty_slice(spec.principal_types.as_slice()), - non_empty_slice(spec.resource_types.as_slice()), + spec.principal_types.split_first(), + spec.resource_types.split_first(), ) { // One of the lists is empty // This can only be represented by the empty action @@ -151,9 +148,9 @@ impl Display for json_schema::ActionType { (Some(ps), Some(rs)) => { write!(f, " appliesTo {{")?; write!(f, "\n principal: ")?; - fmt_vec(f, ps)?; + fmt_non_empty_slice(f, ps)?; write!(f, ",\n resource: ")?; - fmt_vec(f, rs)?; + fmt_non_empty_slice(f, rs)?; write!(f, ",\n context: {}", &spec.context.0)?; write!(f, "\n}}")?; } diff --git a/cedar-policy-validator/src/entity_manifest/type_annotations.rs b/cedar-policy-validator/src/entity_manifest/type_annotations.rs index 0c70e771e..0bf4b071e 100644 --- a/cedar-policy-validator/src/entity_manifest/type_annotations.rs +++ b/cedar-policy-validator/src/entity_manifest/type_annotations.rs @@ -69,7 +69,7 @@ impl RootAccessTrie { match key { EntityRoot::Literal(lit) => slice.to_typed( request_type, - &Type::euid_literal(lit.clone(), schema).ok_or_else(|| { + &Type::euid_literal(lit, schema).ok_or_else(|| { MismatchedMissingEntityError { entity: lit.clone(), } @@ -77,7 +77,7 @@ impl RootAccessTrie { schema, )?, EntityRoot::Var(Var::Action) => { - let ty = Type::euid_literal(request_type.action.clone(), schema) + let ty = Type::euid_literal(&request_type.action, schema) .ok_or_else(|| MismatchedMissingEntityError { entity: request_type.action.clone(), })?; diff --git a/cedar-policy-validator/src/typecheck.rs b/cedar-policy-validator/src/typecheck.rs index 36e87e63e..a6d4e9c65 100644 --- a/cedar-policy-validator/src/typecheck.rs +++ b/cedar-policy-validator/src/typecheck.rs @@ -184,12 +184,11 @@ impl<'a> Typechecker<'a> { // explicit that `expect_type` will be called for every element of // request_env without short circuiting. let policy_condition = &t.condition(); - for requeste in self - .unlinked_request_envs() - .flat_map(|env| self.link_request_env(env, t)) - { - let check = typecheck_fn(&requeste, policy_condition); - result_checks.push((requeste, check)) + for unlinked_e in self.unlinked_request_envs() { + for linked_e in self.link_request_env(&unlinked_e, t) { + let check = typecheck_fn(&linked_e, policy_condition); + result_checks.push((linked_e, check)) + } } result_checks } @@ -208,7 +207,7 @@ impl<'a> Typechecker<'a> { let mut policy_checks = Vec::new(); for t in policy_templates.iter() { let condition_expr = t.condition(); - for linked_env in self.link_request_env(request.clone(), t) { + for linked_env in self.link_request_env(&request, t) { let mut type_errors = Vec::new(); let empty_prior_capability = CapabilitySet::new(); let ty = self.expect_type( @@ -276,11 +275,11 @@ impl<'a> Typechecker<'a> { /// Given a request environment and a template, return new environments /// formed by linking template slots with possible entity types. - fn link_request_env<'b>( + fn link_request_env<'b, 'c>( &'b self, - env: RequestEnv<'b>, + env: &'c RequestEnv<'b>, t: &'b Template, - ) -> Box> + 'b> { + ) -> Box> + 'c> { match env { RequestEnv::UndeclaredAction => Box::new(std::iter::once(RequestEnv::UndeclaredAction)), RequestEnv::DeclaredAction { @@ -469,7 +468,7 @@ impl<'a> Typechecker<'a> { // detected by a different part of the validator, so a ValidationError is // not generated here. We still return `TypecheckFail` so that // typechecking is not considered successful. - match Type::euid_literal((**euid).clone(), self.schema) { + match Type::euid_literal(euid.as_ref(), self.schema) { // The entity type is undeclared, but that's OK for a // partial schema. The attributes record will be empty if we // try to access it later, so all attributes will have the diff --git a/cedar-policy-validator/src/types.rs b/cedar-policy-validator/src/types.rs index da4a162f2..d8e22c4a9 100644 --- a/cedar-policy-validator/src/types.rs +++ b/cedar-policy-validator/src/types.rs @@ -119,10 +119,10 @@ impl Type { /// Construct a type for a literal EUID. This type will be a named entity /// type for the type of the [`EntityUID`]. - pub(crate) fn euid_literal(entity: EntityUID, schema: &ValidatorSchema) -> Option { + pub(crate) fn euid_literal(entity: &EntityUID, schema: &ValidatorSchema) -> Option { if entity.entity_type().is_action() { schema - .get_action_id(&entity) + .get_action_id(entity) .map(Type::entity_reference_from_action_id) } else { schema @@ -2534,7 +2534,7 @@ mod test { #[test] fn test_action_entity_lub() { let action_view_ty = - Type::euid_literal(r#"Action::"view""#.parse().unwrap(), &action_schema()).unwrap(); + Type::euid_literal(&r#"Action::"view""#.parse().unwrap(), &action_schema()).unwrap(); assert_least_upper_bound( action_schema(), @@ -2551,7 +2551,7 @@ mod test { action_schema(), ValidationMode::Strict, action_view_ty.clone(), - Type::euid_literal(r#"Action::"edit""#.parse().unwrap(), &action_schema()).unwrap(), + Type::euid_literal(&r#"Action::"edit""#.parse().unwrap(), &action_schema()).unwrap(), Ok(action_view_ty.clone()), ); @@ -2561,14 +2561,16 @@ mod test { action_schema(), ValidationMode::Permissive, action_view_ty.clone(), - Type::euid_literal(r#"ns::Action::"move""#.parse().unwrap(), &action_schema()).unwrap(), + Type::euid_literal(&r#"ns::Action::"move""#.parse().unwrap(), &action_schema()) + .unwrap(), Ok(Type::any_entity_reference()), ); assert_least_upper_bound( action_schema(), ValidationMode::Strict, action_view_ty.clone(), - Type::euid_literal(r#"ns::Action::"move""#.parse().unwrap(), &action_schema()).unwrap(), + Type::euid_literal(&r#"ns::Action::"move""#.parse().unwrap(), &action_schema()) + .unwrap(), Err(LubHelp::EntityType), ); diff --git a/cedar-policy-validator/src/types/request_env.rs b/cedar-policy-validator/src/types/request_env.rs index 22f18a533..dffe00f41 100644 --- a/cedar-policy-validator/src/types/request_env.rs +++ b/cedar-policy-validator/src/types/request_env.rs @@ -98,7 +98,7 @@ impl<'a> RequestEnv<'a> { /// [`Type`] of the `action` for this request environment pub fn action_type(&self, schema: &ValidatorSchema) -> Option { match self.action_entity_uid() { - Some(action) => Type::euid_literal(action.clone(), schema), + Some(action) => Type::euid_literal(action, schema), None => Some(Type::any_entity_reference()), } } From 3fd764eb2baff1c8246f718c043d8acb16e92322 Mon Sep 17 00:00:00 2001 From: John Kastner <130772734+john-h-kastner-aws@users.noreply.github.com> Date: Tue, 31 Dec 2024 12:45:15 -0500 Subject: [PATCH 13/16] Add `cfg(test)` to entire schema test module (#1399) Signed-off-by: John Kastner --- .../src/cedar_schema/test.rs | 27 +++++++------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/cedar-policy-validator/src/cedar_schema/test.rs b/cedar-policy-validator/src/cedar_schema/test.rs index 19a14e4c0..a5c48b2ef 100644 --- a/cedar-policy-validator/src/cedar_schema/test.rs +++ b/cedar-policy-validator/src/cedar_schema/test.rs @@ -13,11 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -#![allow(clippy::cognitive_complexity)] - +#![cfg(test)] // PANIC SAFETY: unit tests -#[allow(clippy::panic)] -#[cfg(test)] +#![allow( + clippy::cognitive_complexity, + clippy::panic, + clippy::unwrap_used, + clippy::indexing_slicing, + clippy::unreachable +)] + mod demo_tests { use std::{ collections::BTreeMap, @@ -928,7 +933,6 @@ namespace Baz {action "Foo" appliesTo { } } -#[cfg(test)] mod parser_tests { use crate::cedar_schema::parser::parse_schema; use cool_asserts::assert_matches; @@ -1155,11 +1159,6 @@ mod parser_tests { } } -// PANIC SAFETY: tests -#[allow(clippy::unreachable)] -// PANIC SAFETY: tests -#[allow(clippy::panic)] -#[cfg(test)] mod translator_tests { use cedar_policy_core::ast as cedar_ast; use cedar_policy_core::extensions::Extensions; @@ -1453,10 +1452,6 @@ mod translator_tests { }); } - // PANIC SAFETY: testing - #[allow(clippy::unwrap_used)] - // PANIC SAFETY: testing - #[allow(clippy::indexing_slicing)] #[test] fn type_name_resolution_cross_namespace() { let (schema, _) = json_schema::Fragment::from_cedarschema_str( @@ -2275,7 +2270,6 @@ mod translator_tests { } } -#[cfg(test)] mod common_type_references { use cool_asserts::assert_matches; @@ -2588,7 +2582,6 @@ mod common_type_references { } /// Tests involving entity tags (RFC 82) -#[cfg(test)] mod entity_tags { use crate::json_schema; use crate::schema::test::utils::collect_warnings; @@ -2658,7 +2651,6 @@ mod entity_tags { } } -#[cfg(test)] pub(crate) const SPECIAL_IDS: [&str; 18] = [ "principal", "action", @@ -2681,7 +2673,6 @@ pub(crate) const SPECIAL_IDS: [&str; 18] = [ ]; // RFC 48 test cases -#[cfg(test)] mod annotations { use cool_asserts::assert_matches; From a9523b2c972b74ecd5927588e3f794024cdfec41 Mon Sep 17 00:00:00 2001 From: John Kastner <130772734+john-h-kastner-aws@users.noreply.github.com> Date: Tue, 31 Dec 2024 12:45:36 -0500 Subject: [PATCH 14/16] Pass refs to extension functions (#1400) Signed-off-by: John Kastner --- cedar-policy-core/src/ast/extension.rs | 47 ++++--- cedar-policy-core/src/extensions/datetime.rs | 115 +++++++++--------- cedar-policy-core/src/extensions/decimal.rs | 28 ++--- cedar-policy-core/src/extensions/ipaddr.rs | 26 ++-- .../src/extensions/partial_evaluation.rs | 2 +- 5 files changed, 111 insertions(+), 107 deletions(-) diff --git a/cedar-policy-core/src/ast/extension.rs b/cedar-policy-core/src/ast/extension.rs index db1555d05..559af3f28 100644 --- a/cedar-policy-core/src/ast/extension.rs +++ b/cedar-policy-core/src/ast/extension.rs @@ -124,9 +124,22 @@ pub enum CallStyle { // Note: we could use currying to make this a little nicer -/// Trait object that implements the extension function call. -pub type ExtensionFunctionObject = - Box evaluator::Result + Sync + Send + 'static>; +macro_rules! extension_function_object { + ( $( $tys:ty ), * ) => { + Box evaluator::Result + Sync + Send + 'static> + } +} + +/// Trait object that implements the extension function call accepting any number of arguments. +pub type ExtensionFunctionObject = extension_function_object!(&[Value]); +/// Trait object that implements the extension function call accepting exactly 0 arguments +pub type NullaryExtensionFunctionObject = extension_function_object!(); +/// Trait object that implements the extension function call accepting exactly 1 arguments +pub type UnaryExtensionFunctionObject = extension_function_object!(&Value); +/// Trait object that implements the extension function call accepting exactly 2 arguments +pub type BinaryExtensionFunctionObject = extension_function_object!(&Value, &Value); +/// Trait object that implements the extension function call accepting exactly 3 arguments +pub type TernaryExtensionFunctionObject = extension_function_object!(&Value, &Value, &Value); /// Extension function. These can be called by the given `name` in Ceder /// expressions. @@ -172,7 +185,7 @@ impl ExtensionFunction { pub fn nullary( name: Name, style: CallStyle, - func: Box evaluator::Result + Sync + Send + 'static>, + func: NullaryExtensionFunctionObject, return_type: SchemaType, ) -> Self { Self::new( @@ -200,14 +213,14 @@ impl ExtensionFunction { pub fn partial_eval_unknown( name: Name, style: CallStyle, - func: Box evaluator::Result + Sync + Send + 'static>, + func: UnaryExtensionFunctionObject, arg_type: SchemaType, ) -> Self { Self::new( name.clone(), style, Box::new(move |args: &[Value]| match args.first() { - Some(arg) => func(arg.clone()), + Some(arg) => func(arg), None => Err(evaluator::EvaluationError::wrong_num_arguments( name.clone(), 1, @@ -221,10 +234,11 @@ impl ExtensionFunction { } /// Create a new `ExtensionFunction` taking one argument + #[allow(clippy::type_complexity)] pub fn unary( name: Name, style: CallStyle, - func: Box evaluator::Result + Sync + Send + 'static>, + func: UnaryExtensionFunctionObject, return_type: SchemaType, arg_type: SchemaType, ) -> Self { @@ -232,7 +246,7 @@ impl ExtensionFunction { name.clone(), style, Box::new(move |args: &[Value]| match &args { - &[arg] => func(arg.clone()), + &[arg] => func(arg), _ => Err(evaluator::EvaluationError::wrong_num_arguments( name.clone(), 1, @@ -246,12 +260,11 @@ impl ExtensionFunction { } /// Create a new `ExtensionFunction` taking two arguments + #[allow(clippy::type_complexity)] pub fn binary( name: Name, style: CallStyle, - func: Box< - dyn Fn(Value, Value) -> evaluator::Result + Sync + Send + 'static, - >, + func: BinaryExtensionFunctionObject, return_type: SchemaType, arg_types: (SchemaType, SchemaType), ) -> Self { @@ -259,7 +272,7 @@ impl ExtensionFunction { name.clone(), style, Box::new(move |args: &[Value]| match &args { - &[first, second] => func(first.clone(), second.clone()), + &[first, second] => func(first, second), _ => Err(evaluator::EvaluationError::wrong_num_arguments( name.clone(), 2, @@ -273,15 +286,11 @@ impl ExtensionFunction { } /// Create a new `ExtensionFunction` taking three arguments + #[allow(clippy::type_complexity)] pub fn ternary( name: Name, style: CallStyle, - func: Box< - dyn Fn(Value, Value, Value) -> evaluator::Result - + Sync - + Send - + 'static, - >, + func: TernaryExtensionFunctionObject, return_type: SchemaType, arg_types: (SchemaType, SchemaType, SchemaType), ) -> Self { @@ -289,7 +298,7 @@ impl ExtensionFunction { name.clone(), style, Box::new(move |args: &[Value]| match &args { - &[first, second, third] => func(first.clone(), second.clone(), third.clone()), + &[first, second, third] => func(first, second, third), _ => Err(evaluator::EvaluationError::wrong_num_arguments( name.clone(), 3, diff --git a/cedar-policy-core/src/extensions/datetime.rs b/cedar-policy-core/src/extensions/datetime.rs index 5b97b4852..b98c11856 100644 --- a/cedar-policy-core/src/extensions/datetime.rs +++ b/cedar-policy-core/src/extensions/datetime.rs @@ -71,7 +71,7 @@ mod constants { } // The `datetime` type, represented internally as an `i64`. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] struct DateTime { // The number of non-leap milliseconds from the Unix epoch epoch: i64, @@ -91,7 +91,7 @@ fn extension_err( } fn construct_from_str( - arg: Value, + arg: &Value, constructor_name: Name, constructor: impl Fn(&str) -> Result, ) -> evaluator::Result @@ -101,8 +101,11 @@ where let s = arg.get_as_string()?; let ext_value: Ext = constructor(s)?; let arg_source_loc = arg.source_loc().cloned(); - let e = - RepresentableExtensionValue::new(Arc::new(ext_value), constructor_name, vec![arg.into()]); + let e = RepresentableExtensionValue::new( + Arc::new(ext_value), + constructor_name, + vec![arg.clone().into()], + ); Ok(Value { value: ValueKind::ExtensionValue(Arc::new(e)), loc: arg_source_loc, // follow the same convention as the `decimal` extension @@ -112,7 +115,7 @@ where /// Cedar function that constructs a `datetime` Cedar type from a /// Cedar string -fn datetime_from_str(arg: Value) -> evaluator::Result { +fn datetime_from_str(arg: &Value) -> evaluator::Result { construct_from_str(arg, DATETIME_CONSTRUCTOR_NAME.clone(), |s| { parse_datetime(s).map(DateTime::from).map_err(|err| { extension_err( @@ -158,23 +161,23 @@ where } /// Check that `v` is a datetime type and, if it is, return the wrapped value -fn as_datetime(v: &Value) -> Result<&DateTime, evaluator::EvaluationError> { - as_ext(v, &DATETIME_CONSTRUCTOR_NAME) +fn as_datetime(v: &Value) -> Result { + as_ext(v, &DATETIME_CONSTRUCTOR_NAME).copied() } /// Check that `v` is a duration type and, if it is, return the wrapped value -fn as_duration(v: &Value) -> Result<&Duration, evaluator::EvaluationError> { - as_ext(v, &DURATION_CONSTRUCTOR_NAME) +fn as_duration(v: &Value) -> Result { + as_ext(v, &DURATION_CONSTRUCTOR_NAME).copied() } -fn offset(datetime: Value, duration: Value) -> evaluator::Result { - let datetime = as_datetime(&datetime)?; - let duration = as_duration(&duration)?; - let ret = datetime.offset(duration.clone()).ok_or_else(|| { +fn offset(datetime: &Value, duration: &Value) -> evaluator::Result { + let datetime = as_datetime(datetime)?; + let duration = as_duration(duration)?; + let ret = datetime.offset(duration).ok_or_else(|| { extension_err( format!( "overflows when adding an offset: {}+({})", - RestrictedExpr::from(datetime.clone()), + RestrictedExpr::from(datetime), duration ), &OFFSET_METHOD_NAME, @@ -188,15 +191,15 @@ fn offset(datetime: Value, duration: Value) -> evaluator::Result evaluator::Result { - let lhs = as_datetime(&lhs)?; - let rhs = as_datetime(&rhs)?; - let ret = lhs.duration_since(rhs.clone()).ok_or_else(|| { +fn duration_since(lhs: &Value, rhs: &Value) -> evaluator::Result { + let lhs = as_datetime(lhs)?; + let rhs = as_datetime(rhs)?; + let ret = lhs.duration_since(rhs).ok_or_else(|| { extension_err( format!( "overflows when computing the duration between {} and {}", - RestrictedExpr::from(lhs.clone()), - RestrictedExpr::from(rhs.clone()) + RestrictedExpr::from(lhs), + RestrictedExpr::from(rhs) ), &DURATION_SINCE_NAME, None, @@ -209,13 +212,13 @@ fn duration_since(lhs: Value, rhs: Value) -> evaluator::Result evaluator::Result { - let d = as_datetime(&value)?; +fn to_date(value: &Value) -> evaluator::Result { + let d = as_datetime(value)?; let ret = d.to_date().ok_or_else(|| { extension_err( format!( "overflows when computing the date of {}", - RestrictedExpr::from(d.clone()), + RestrictedExpr::from(d), ), &TO_DATE_NAME, None, @@ -228,8 +231,8 @@ fn to_date(value: Value) -> evaluator::Result { .into()) } -fn to_time(value: Value) -> evaluator::Result { - let d = as_datetime(&value)?; +fn to_time(value: &Value) -> evaluator::Result { + let d = as_datetime(value)?; let ret = d.to_time(); Ok(Value { value: ValueKind::ExtensionValue(Arc::new(ret.into())), @@ -251,13 +254,13 @@ impl DateTime { const DAY_IN_MILLISECONDS: i64 = 1000 * 3600 * 24; const UNIX_EPOCH_STR: &'static str = "1970-01-01"; - fn offset(&self, duration: Duration) -> Option { + fn offset(self, duration: Duration) -> Option { self.epoch .checked_add(duration.ms) .map(|epoch| Self { epoch }) } - fn duration_since(&self, other: DateTime) -> Option { + fn duration_since(self, other: DateTime) -> Option { self.epoch .checked_sub(other.epoch) .map(|ms| Duration { ms }) @@ -265,7 +268,7 @@ impl DateTime { // essentially `self.epoch.div_floor(Self::DAY_IN_MILLISECONDS) * Self::DAY_IN_MILLISECONDS` // but `div_floor` is only available on nightly - fn to_date(&self) -> Option { + fn to_date(self) -> Option { if self.epoch.is_negative() { if self.epoch % Self::DAY_IN_MILLISECONDS == 0 { Some(self.epoch) @@ -278,7 +281,7 @@ impl DateTime { .map(|epoch| Self { epoch }) } - fn to_time(&self) -> Duration { + fn to_time(self) -> Duration { Duration { ms: if self.epoch.is_negative() { let rem = self.epoch % Self::DAY_IN_MILLISECONDS; @@ -293,7 +296,7 @@ impl DateTime { } } - fn as_ext_func_call(&self) -> (Name, Vec) { + fn as_ext_func_call(self) -> (Name, Vec) { ( OFFSET_METHOD_NAME.clone(), vec![ @@ -335,7 +338,7 @@ impl From for DateTime { } // The `duration` type -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] struct Duration { // The number of milliseconds ms: i64, @@ -376,7 +379,7 @@ impl From for RepresentableExtensionValue { /// Cedar function that constructs a `duration` Cedar type from a /// Cedar string -fn duration_from_str(arg: Value) -> evaluator::Result { +fn duration_from_str(arg: &Value) -> evaluator::Result { construct_from_str(arg, DURATION_CONSTRUCTOR_NAME.clone(), |s| { parse_duration(s).map_err(|err| { extension_err( @@ -389,27 +392,27 @@ fn duration_from_str(arg: Value) -> evaluator::Result { } impl Duration { - fn to_milliseconds(&self) -> i64 { + fn to_milliseconds(self) -> i64 { self.ms } - fn to_seconds(&self) -> i64 { + fn to_seconds(self) -> i64 { self.to_milliseconds() / 1000 } - fn to_minutes(&self) -> i64 { + fn to_minutes(self) -> i64 { self.to_seconds() / 60 } - fn to_hours(&self) -> i64 { + fn to_hours(self) -> i64 { self.to_minutes() / 60 } - fn to_days(&self) -> i64 { + fn to_days(self) -> i64 { self.to_hours() / 24 } - fn as_ext_func_call(&self) -> (Name, Vec) { + fn as_ext_func_call(self) -> (Name, Vec) { ( DURATION_CONSTRUCTOR_NAME.clone(), vec![Value::from(self.to_string()).into()], @@ -418,10 +421,10 @@ impl Duration { } fn duration_method( - value: Value, - internal_func: impl Fn(&Duration) -> i64, + value: &Value, + internal_func: impl Fn(Duration) -> i64, ) -> evaluator::Result { - let d = as_duration(&value)?; + let d = as_duration(value)?; Ok(Value::from(internal_func(d)).into()) } @@ -1024,12 +1027,12 @@ mod tests { let unix_epoch = DateTime { epoch: 0 }; let today: DateTime = parse_datetime("2024-10-24").unwrap().into(); assert_eq!( - today.duration_since(unix_epoch.clone()), + today.duration_since(unix_epoch), Some(parse_duration("20020d").unwrap()) ); let yesterday: DateTime = parse_datetime("2024-10-23").unwrap().into(); assert_eq!( - yesterday.duration_since(today.clone()), + yesterday.duration_since(today), Some(parse_duration("-1d").unwrap()) ); assert_eq!( @@ -1048,12 +1051,12 @@ mod tests { let unix_epoch = DateTime { epoch: 0 }; let today: DateTime = parse_datetime("2024-10-24").unwrap().into(); assert_eq!( - today.duration_since(unix_epoch.clone()), + today.duration_since(unix_epoch), Some(parse_duration("20020d").unwrap()) ); let yesterday: DateTime = parse_datetime("2024-10-23").unwrap().into(); assert_eq!( - yesterday.duration_since(today.clone()), + yesterday.duration_since(today), Some(parse_duration("-1d").unwrap()) ); let some_day_before_unix_epoch: DateTime = parse_datetime("1900-01-01").unwrap().into(); @@ -1061,22 +1064,17 @@ mod tests { let max_day_offset = parse_duration("23h59m59s999ms").unwrap(); let min_day_offset = parse_duration("-23h59m59s999ms").unwrap(); - for d in [ - today, - yesterday, - unix_epoch.clone(), - some_day_before_unix_epoch, - ] { + for d in [today, yesterday, unix_epoch, some_day_before_unix_epoch] { assert_eq!(d.to_date().expect("should not overflow"), d); assert_eq!( - d.offset(max_day_offset.clone()) + d.offset(max_day_offset) .unwrap() .to_date() .expect("should not overflow"), d ); assert_eq!( - d.offset(min_day_offset.clone()) + d.offset(min_day_offset) .unwrap() .to_date() .expect("should not overflow"), @@ -1096,12 +1094,12 @@ mod tests { let unix_epoch = DateTime { epoch: 0 }; let today: DateTime = parse_datetime("2024-10-24").unwrap().into(); assert_eq!( - today.duration_since(unix_epoch.clone()), + today.duration_since(unix_epoch), Some(parse_duration("20020d").unwrap()) ); let yesterday: DateTime = parse_datetime("2024-10-23").unwrap().into(); assert_eq!( - yesterday.duration_since(today.clone()), + yesterday.duration_since(today), Some(parse_duration("-1d").unwrap()) ); let some_day_before_unix_epoch: DateTime = parse_datetime("1900-01-01").unwrap().into(); @@ -1110,12 +1108,9 @@ mod tests { let min_day_offset = parse_duration("-23h59m59s999ms").unwrap(); for d in [today, yesterday, unix_epoch, some_day_before_unix_epoch] { + assert_eq!(d.offset(max_day_offset).unwrap().to_time(), max_day_offset); assert_eq!( - d.offset(max_day_offset.clone()).unwrap().to_time(), - max_day_offset - ); - assert_eq!( - d.offset(min_day_offset.clone()).unwrap().to_time(), + d.offset(min_day_offset).unwrap().to_time(), parse_duration("1ms").unwrap(), ); } diff --git a/cedar-policy-core/src/extensions/decimal.rs b/cedar-policy-core/src/extensions/decimal.rs index 7e3081deb..bd601a952 100644 --- a/cedar-policy-core/src/extensions/decimal.rs +++ b/cedar-policy-core/src/extensions/decimal.rs @@ -184,7 +184,7 @@ fn extension_err(msg: impl Into, advice: Option) -> evaluator::E /// Cedar function that constructs a `decimal` Cedar type from a /// Cedar string -fn decimal_from_str(arg: Value) -> evaluator::Result { +fn decimal_from_str(arg: &Value) -> evaluator::Result { let str = arg.get_as_string()?; let decimal = Decimal::from_str(str.as_str()).map_err(|e| extension_err(e.to_string(), None))?; @@ -192,7 +192,7 @@ fn decimal_from_str(arg: Value) -> evaluator::Result { let e = RepresentableExtensionValue::new( Arc::new(decimal), constants::DECIMAL_FROM_STR_NAME.clone(), - vec![arg.into()], + vec![arg.clone().into()], ); Ok(Value { value: ValueKind::ExtensionValue(Arc::new(e)), @@ -234,33 +234,33 @@ fn as_decimal(v: &Value) -> Result<&Decimal, evaluator::EvaluationError> { /// Cedar function that tests whether the first `decimal` Cedar type is /// less than the second `decimal` Cedar type, returning a Cedar bool -fn decimal_lt(left: Value, right: Value) -> evaluator::Result { - let left = as_decimal(&left)?; - let right = as_decimal(&right)?; +fn decimal_lt(left: &Value, right: &Value) -> evaluator::Result { + let left = as_decimal(left)?; + let right = as_decimal(right)?; Ok(Value::from(left < right).into()) } /// Cedar function that tests whether the first `decimal` Cedar type is /// less than or equal to the second `decimal` Cedar type, returning a Cedar bool -fn decimal_le(left: Value, right: Value) -> evaluator::Result { - let left = as_decimal(&left)?; - let right = as_decimal(&right)?; +fn decimal_le(left: &Value, right: &Value) -> evaluator::Result { + let left = as_decimal(left)?; + let right = as_decimal(right)?; Ok(Value::from(left <= right).into()) } /// Cedar function that tests whether the first `decimal` Cedar type is /// greater than the second `decimal` Cedar type, returning a Cedar bool -fn decimal_gt(left: Value, right: Value) -> evaluator::Result { - let left = as_decimal(&left)?; - let right = as_decimal(&right)?; +fn decimal_gt(left: &Value, right: &Value) -> evaluator::Result { + let left = as_decimal(left)?; + let right = as_decimal(right)?; Ok(Value::from(left > right).into()) } /// Cedar function that tests whether the first `decimal` Cedar type is /// greater than or equal to the second `decimal` Cedar type, returning a Cedar bool -fn decimal_ge(left: Value, right: Value) -> evaluator::Result { - let left = as_decimal(&left)?; - let right = as_decimal(&right)?; +fn decimal_ge(left: &Value, right: &Value) -> evaluator::Result { + let left = as_decimal(left)?; + let right = as_decimal(right)?; Ok(Value::from(left >= right).into()) } diff --git a/cedar-policy-core/src/extensions/ipaddr.rs b/cedar-policy-core/src/extensions/ipaddr.rs index 590f962bd..ed88f790b 100644 --- a/cedar-policy-core/src/extensions/ipaddr.rs +++ b/cedar-policy-core/src/extensions/ipaddr.rs @@ -305,13 +305,13 @@ fn str_contains_colons_and_dots(s: &str) -> Result<(), String> { /// Cedar function which constructs an `ipaddr` Cedar type from a /// Cedar string -fn ip_from_str(arg: Value) -> evaluator::Result { +fn ip_from_str(arg: &Value) -> evaluator::Result { let str = arg.get_as_string()?; let arg_source_loc = arg.source_loc().cloned(); let ipaddr = RepresentableExtensionValue::new( Arc::new(IPAddr::from_str(str.as_str()).map_err(extension_err)?), names::IP_FROM_STR_NAME.clone(), - vec![arg.into()], + vec![arg.clone().into()], ); Ok(Value { value: ValueKind::ExtensionValue(Arc::new(ipaddr)), @@ -352,38 +352,38 @@ fn as_ipaddr(v: &Value) -> Result<&IPAddr, evaluator::EvaluationError> { /// Cedar function which tests whether an `ipaddr` Cedar type is an IPv4 /// address, returning a Cedar bool -fn is_ipv4(arg: Value) -> evaluator::Result { - let ipaddr = as_ipaddr(&arg)?; +fn is_ipv4(arg: &Value) -> evaluator::Result { + let ipaddr = as_ipaddr(arg)?; Ok(ipaddr.is_ipv4().into()) } /// Cedar function which tests whether an `ipaddr` Cedar type is an IPv6 /// address, returning a Cedar bool -fn is_ipv6(arg: Value) -> evaluator::Result { - let ipaddr = as_ipaddr(&arg)?; +fn is_ipv6(arg: &Value) -> evaluator::Result { + let ipaddr = as_ipaddr(arg)?; Ok(ipaddr.is_ipv6().into()) } /// Cedar function which tests whether an `ipaddr` Cedar type is a /// loopback address, returning a Cedar bool -fn is_loopback(arg: Value) -> evaluator::Result { - let ipaddr = as_ipaddr(&arg)?; +fn is_loopback(arg: &Value) -> evaluator::Result { + let ipaddr = as_ipaddr(arg)?; Ok(ipaddr.is_loopback().into()) } /// Cedar function which tests whether an `ipaddr` Cedar type is a /// multicast address, returning a Cedar bool -fn is_multicast(arg: Value) -> evaluator::Result { - let ipaddr = as_ipaddr(&arg)?; +fn is_multicast(arg: &Value) -> evaluator::Result { + let ipaddr = as_ipaddr(arg)?; Ok(ipaddr.is_multicast().into()) } /// Cedar function which tests whether the first `ipaddr` Cedar type is /// in the IP range represented by the second `ipaddr` Cedar type, returning /// a Cedar bool -fn is_in_range(child: Value, parent: Value) -> evaluator::Result { - let child_ip = as_ipaddr(&child)?; - let parent_ip = as_ipaddr(&parent)?; +fn is_in_range(child: &Value, parent: &Value) -> evaluator::Result { + let child_ip = as_ipaddr(child)?; + let parent_ip = as_ipaddr(parent)?; Ok(child_ip.is_in_range(parent_ip).into()) } diff --git a/cedar-policy-core/src/extensions/partial_evaluation.rs b/cedar-policy-core/src/extensions/partial_evaluation.rs index 13f2e6a88..c271c55b2 100644 --- a/cedar-policy-core/src/extensions/partial_evaluation.rs +++ b/cedar-policy-core/src/extensions/partial_evaluation.rs @@ -23,7 +23,7 @@ use crate::{ }; /// Create a new untyped `Unknown` -fn create_new_unknown(v: Value) -> evaluator::Result { +fn create_new_unknown(v: &Value) -> evaluator::Result { Ok(ExtensionOutputValue::Unknown(Unknown::new_untyped( v.get_as_string()?.clone(), ))) From d49f766c140d80bafb9195743badf52ffe4e1461 Mon Sep 17 00:00:00 2001 From: John Kastner <130772734+john-h-kastner-aws@users.noreply.github.com> Date: Tue, 31 Dec 2024 12:45:47 -0500 Subject: [PATCH 15/16] Use `unwrap_or_clone` instead of `(*foo).clone()` in more places (#1401) Signed-off-by: John Kastner --- cedar-policy-core/src/ast/policy_set.rs | 2 +- cedar-policy-core/src/est/expr.rs | 204 +++++++++++++----------- cedar-policy-core/src/evaluator.rs | 2 +- 3 files changed, 109 insertions(+), 99 deletions(-) diff --git a/cedar-policy-core/src/ast/policy_set.rs b/cedar-policy-core/src/ast/policy_set.rs index 1508c7922..c5267a9a8 100644 --- a/cedar-policy-core/src/ast/policy_set.rs +++ b/cedar-policy-core/src/ast/policy_set.rs @@ -433,7 +433,7 @@ impl PolicySet { match self.templates.remove(policy_id) { Some(t) => { self.template_to_links_map.remove(policy_id); - Ok((*t).clone()) + Ok(Arc::unwrap_or_clone(t)) } None => panic!("Found in template_to_links_map but not in templates"), } diff --git a/cedar-policy-core/src/est/expr.rs b/cedar-policy-core/src/est/expr.rs index 8e401b44f..0440da513 100644 --- a/cedar-policy-core/src/est/expr.rs +++ b/cedar-policy-core/src/est/expr.rs @@ -673,96 +673,96 @@ impl Expr { ExprNoExt::Var(_) => Ok(self), ExprNoExt::Slot(_) => Ok(self), ExprNoExt::Not { arg } => Ok(Expr::ExprNoExt(ExprNoExt::Not { - arg: Arc::new((*arg).clone().sub_entity_literals(mapping)?), + arg: Arc::new(Arc::unwrap_or_clone(arg).sub_entity_literals(mapping)?), })), ExprNoExt::Neg { arg } => Ok(Expr::ExprNoExt(ExprNoExt::Neg { - arg: Arc::new((*arg).clone().sub_entity_literals(mapping)?), + arg: Arc::new(Arc::unwrap_or_clone(arg).sub_entity_literals(mapping)?), })), ExprNoExt::Eq { left, right } => Ok(Expr::ExprNoExt(ExprNoExt::Eq { - left: Arc::new((*left).clone().sub_entity_literals(mapping)?), - right: Arc::new((*right).clone().sub_entity_literals(mapping)?), + left: Arc::new(Arc::unwrap_or_clone(left).sub_entity_literals(mapping)?), + right: Arc::new(Arc::unwrap_or_clone(right).sub_entity_literals(mapping)?), })), ExprNoExt::NotEq { left, right } => Ok(Expr::ExprNoExt(ExprNoExt::NotEq { - left: Arc::new((*left).clone().sub_entity_literals(mapping)?), - right: Arc::new((*right).clone().sub_entity_literals(mapping)?), + left: Arc::new(Arc::unwrap_or_clone(left).sub_entity_literals(mapping)?), + right: Arc::new(Arc::unwrap_or_clone(right).sub_entity_literals(mapping)?), })), ExprNoExt::In { left, right } => Ok(Expr::ExprNoExt(ExprNoExt::In { - left: Arc::new((*left).clone().sub_entity_literals(mapping)?), - right: Arc::new((*right).clone().sub_entity_literals(mapping)?), + left: Arc::new(Arc::unwrap_or_clone(left).sub_entity_literals(mapping)?), + right: Arc::new(Arc::unwrap_or_clone(right).sub_entity_literals(mapping)?), })), ExprNoExt::Less { left, right } => Ok(Expr::ExprNoExt(ExprNoExt::Less { - left: Arc::new((*left).clone().sub_entity_literals(mapping)?), - right: Arc::new((*right).clone().sub_entity_literals(mapping)?), + left: Arc::new(Arc::unwrap_or_clone(left).sub_entity_literals(mapping)?), + right: Arc::new(Arc::unwrap_or_clone(right).sub_entity_literals(mapping)?), })), ExprNoExt::LessEq { left, right } => Ok(Expr::ExprNoExt(ExprNoExt::LessEq { - left: Arc::new((*left).clone().sub_entity_literals(mapping)?), - right: Arc::new((*right).clone().sub_entity_literals(mapping)?), + left: Arc::new(Arc::unwrap_or_clone(left).sub_entity_literals(mapping)?), + right: Arc::new(Arc::unwrap_or_clone(right).sub_entity_literals(mapping)?), })), ExprNoExt::Greater { left, right } => Ok(Expr::ExprNoExt(ExprNoExt::Greater { - left: Arc::new((*left).clone().sub_entity_literals(mapping)?), - right: Arc::new((*right).clone().sub_entity_literals(mapping)?), + left: Arc::new(Arc::unwrap_or_clone(left).sub_entity_literals(mapping)?), + right: Arc::new(Arc::unwrap_or_clone(right).sub_entity_literals(mapping)?), })), ExprNoExt::GreaterEq { left, right } => Ok(Expr::ExprNoExt(ExprNoExt::GreaterEq { - left: Arc::new((*left).clone().sub_entity_literals(mapping)?), - right: Arc::new((*right).clone().sub_entity_literals(mapping)?), + left: Arc::new(Arc::unwrap_or_clone(left).sub_entity_literals(mapping)?), + right: Arc::new(Arc::unwrap_or_clone(right).sub_entity_literals(mapping)?), })), ExprNoExt::And { left, right } => Ok(Expr::ExprNoExt(ExprNoExt::And { - left: Arc::new((*left).clone().sub_entity_literals(mapping)?), - right: Arc::new((*right).clone().sub_entity_literals(mapping)?), + left: Arc::new(Arc::unwrap_or_clone(left).sub_entity_literals(mapping)?), + right: Arc::new(Arc::unwrap_or_clone(right).sub_entity_literals(mapping)?), })), ExprNoExt::Or { left, right } => Ok(Expr::ExprNoExt(ExprNoExt::Or { - left: Arc::new((*left).clone().sub_entity_literals(mapping)?), - right: Arc::new((*right).clone().sub_entity_literals(mapping)?), + left: Arc::new(Arc::unwrap_or_clone(left).sub_entity_literals(mapping)?), + right: Arc::new(Arc::unwrap_or_clone(right).sub_entity_literals(mapping)?), })), ExprNoExt::Add { left, right } => Ok(Expr::ExprNoExt(ExprNoExt::Add { - left: Arc::new((*left).clone().sub_entity_literals(mapping)?), - right: Arc::new((*right).clone().sub_entity_literals(mapping)?), + left: Arc::new(Arc::unwrap_or_clone(left).sub_entity_literals(mapping)?), + right: Arc::new(Arc::unwrap_or_clone(right).sub_entity_literals(mapping)?), })), ExprNoExt::Sub { left, right } => Ok(Expr::ExprNoExt(ExprNoExt::Sub { - left: Arc::new((*left).clone().sub_entity_literals(mapping)?), - right: Arc::new((*right).clone().sub_entity_literals(mapping)?), + left: Arc::new(Arc::unwrap_or_clone(left).sub_entity_literals(mapping)?), + right: Arc::new(Arc::unwrap_or_clone(right).sub_entity_literals(mapping)?), })), ExprNoExt::Mul { left, right } => Ok(Expr::ExprNoExt(ExprNoExt::Mul { - left: Arc::new((*left).clone().sub_entity_literals(mapping)?), - right: Arc::new((*right).clone().sub_entity_literals(mapping)?), + left: Arc::new(Arc::unwrap_or_clone(left).sub_entity_literals(mapping)?), + right: Arc::new(Arc::unwrap_or_clone(right).sub_entity_literals(mapping)?), })), ExprNoExt::Contains { left, right } => Ok(Expr::ExprNoExt(ExprNoExt::Contains { - left: Arc::new((*left).clone().sub_entity_literals(mapping)?), - right: Arc::new((*right).clone().sub_entity_literals(mapping)?), + left: Arc::new(Arc::unwrap_or_clone(left).sub_entity_literals(mapping)?), + right: Arc::new(Arc::unwrap_or_clone(right).sub_entity_literals(mapping)?), })), ExprNoExt::ContainsAll { left, right } => { Ok(Expr::ExprNoExt(ExprNoExt::ContainsAll { - left: Arc::new((*left).clone().sub_entity_literals(mapping)?), - right: Arc::new((*right).clone().sub_entity_literals(mapping)?), + left: Arc::new(Arc::unwrap_or_clone(left).sub_entity_literals(mapping)?), + right: Arc::new(Arc::unwrap_or_clone(right).sub_entity_literals(mapping)?), })) } ExprNoExt::ContainsAny { left, right } => { Ok(Expr::ExprNoExt(ExprNoExt::ContainsAny { - left: Arc::new((*left).clone().sub_entity_literals(mapping)?), - right: Arc::new((*right).clone().sub_entity_literals(mapping)?), + left: Arc::new(Arc::unwrap_or_clone(left).sub_entity_literals(mapping)?), + right: Arc::new(Arc::unwrap_or_clone(right).sub_entity_literals(mapping)?), })) } ExprNoExt::IsEmpty { arg } => Ok(Expr::ExprNoExt(ExprNoExt::IsEmpty { - arg: Arc::new((*arg).clone().sub_entity_literals(mapping)?), + arg: Arc::new(Arc::unwrap_or_clone(arg).sub_entity_literals(mapping)?), })), ExprNoExt::GetTag { left, right } => Ok(Expr::ExprNoExt(ExprNoExt::GetTag { - left: Arc::new((*left).clone().sub_entity_literals(mapping)?), - right: Arc::new((*right).clone().sub_entity_literals(mapping)?), + left: Arc::new(Arc::unwrap_or_clone(left).sub_entity_literals(mapping)?), + right: Arc::new(Arc::unwrap_or_clone(right).sub_entity_literals(mapping)?), })), ExprNoExt::HasTag { left, right } => Ok(Expr::ExprNoExt(ExprNoExt::HasTag { - left: Arc::new((*left).clone().sub_entity_literals(mapping)?), - right: Arc::new((*right).clone().sub_entity_literals(mapping)?), + left: Arc::new(Arc::unwrap_or_clone(left).sub_entity_literals(mapping)?), + right: Arc::new(Arc::unwrap_or_clone(right).sub_entity_literals(mapping)?), })), ExprNoExt::GetAttr { left, attr } => Ok(Expr::ExprNoExt(ExprNoExt::GetAttr { - left: Arc::new((*left).clone().sub_entity_literals(mapping)?), + left: Arc::new(Arc::unwrap_or_clone(left).sub_entity_literals(mapping)?), attr, })), ExprNoExt::HasAttr { left, attr } => Ok(Expr::ExprNoExt(ExprNoExt::HasAttr { - left: Arc::new((*left).clone().sub_entity_literals(mapping)?), + left: Arc::new(Arc::unwrap_or_clone(left).sub_entity_literals(mapping)?), attr, })), ExprNoExt::Like { left, pattern } => Ok(Expr::ExprNoExt(ExprNoExt::Like { - left: Arc::new((*left).clone().sub_entity_literals(mapping)?), + left: Arc::new(Arc::unwrap_or_clone(left).sub_entity_literals(mapping)?), pattern, })), ExprNoExt::Is { @@ -771,12 +771,14 @@ impl Expr { in_expr, } => match in_expr { Some(in_expr) => Ok(Expr::ExprNoExt(ExprNoExt::Is { - left: Arc::new((*left).clone().sub_entity_literals(mapping)?), + left: Arc::new(Arc::unwrap_or_clone(left).sub_entity_literals(mapping)?), entity_type, - in_expr: Some(Arc::new((*in_expr).clone().sub_entity_literals(mapping)?)), + in_expr: Some(Arc::new( + Arc::unwrap_or_clone(in_expr).sub_entity_literals(mapping)?, + )), })), None => Ok(Expr::ExprNoExt(ExprNoExt::Is { - left: Arc::new((*left).clone().sub_entity_literals(mapping)?), + left: Arc::new(Arc::unwrap_or_clone(left).sub_entity_literals(mapping)?), entity_type, in_expr: None, })), @@ -786,9 +788,15 @@ impl Expr { then_expr, else_expr, } => Ok(Expr::ExprNoExt(ExprNoExt::If { - cond_expr: Arc::new((*cond_expr).clone().sub_entity_literals(mapping)?), - then_expr: Arc::new((*then_expr).clone().sub_entity_literals(mapping)?), - else_expr: Arc::new((*else_expr).clone().sub_entity_literals(mapping)?), + cond_expr: Arc::new( + Arc::unwrap_or_clone(cond_expr).sub_entity_literals(mapping)?, + ), + then_expr: Arc::new( + Arc::unwrap_or_clone(then_expr).sub_entity_literals(mapping)?, + ), + else_expr: Arc::new( + Arc::unwrap_or_clone(else_expr).sub_entity_literals(mapping)?, + ), })), ExprNoExt::Set(v) => { let mut new_v = vec![]; @@ -833,90 +841,92 @@ impl Expr { Expr::ExprNoExt(ExprNoExt::Var(var)) => Ok(ast::Expr::var(var)), Expr::ExprNoExt(ExprNoExt::Slot(slot)) => Ok(ast::Expr::slot(slot)), Expr::ExprNoExt(ExprNoExt::Not { arg }) => { - Ok(ast::Expr::not((*arg).clone().try_into_ast(id)?)) + Ok(ast::Expr::not(Arc::unwrap_or_clone(arg).try_into_ast(id)?)) } Expr::ExprNoExt(ExprNoExt::Neg { arg }) => { - Ok(ast::Expr::neg((*arg).clone().try_into_ast(id)?)) + Ok(ast::Expr::neg(Arc::unwrap_or_clone(arg).try_into_ast(id)?)) } Expr::ExprNoExt(ExprNoExt::Eq { left, right }) => Ok(ast::Expr::is_eq( - (*left).clone().try_into_ast(id.clone())?, - (*right).clone().try_into_ast(id)?, + Arc::unwrap_or_clone(left).try_into_ast(id.clone())?, + Arc::unwrap_or_clone(right).try_into_ast(id)?, )), Expr::ExprNoExt(ExprNoExt::NotEq { left, right }) => Ok(ast::Expr::noteq( - (*left).clone().try_into_ast(id.clone())?, - (*right).clone().try_into_ast(id)?, + Arc::unwrap_or_clone(left).try_into_ast(id.clone())?, + Arc::unwrap_or_clone(right).try_into_ast(id)?, )), Expr::ExprNoExt(ExprNoExt::In { left, right }) => Ok(ast::Expr::is_in( - (*left).clone().try_into_ast(id.clone())?, - (*right).clone().try_into_ast(id)?, + Arc::unwrap_or_clone(left).try_into_ast(id.clone())?, + Arc::unwrap_or_clone(right).try_into_ast(id)?, )), Expr::ExprNoExt(ExprNoExt::Less { left, right }) => Ok(ast::Expr::less( - (*left).clone().try_into_ast(id.clone())?, - (*right).clone().try_into_ast(id)?, + Arc::unwrap_or_clone(left).try_into_ast(id.clone())?, + Arc::unwrap_or_clone(right).try_into_ast(id)?, )), Expr::ExprNoExt(ExprNoExt::LessEq { left, right }) => Ok(ast::Expr::lesseq( - (*left).clone().try_into_ast(id.clone())?, - (*right).clone().try_into_ast(id)?, + Arc::unwrap_or_clone(left).try_into_ast(id.clone())?, + Arc::unwrap_or_clone(right).try_into_ast(id)?, )), Expr::ExprNoExt(ExprNoExt::Greater { left, right }) => Ok(ast::Expr::greater( - (*left).clone().try_into_ast(id.clone())?, - (*right).clone().try_into_ast(id)?, + Arc::unwrap_or_clone(left).try_into_ast(id.clone())?, + Arc::unwrap_or_clone(right).try_into_ast(id)?, )), Expr::ExprNoExt(ExprNoExt::GreaterEq { left, right }) => Ok(ast::Expr::greatereq( - (*left).clone().try_into_ast(id.clone())?, - (*right).clone().try_into_ast(id)?, + Arc::unwrap_or_clone(left).try_into_ast(id.clone())?, + Arc::unwrap_or_clone(right).try_into_ast(id)?, )), Expr::ExprNoExt(ExprNoExt::And { left, right }) => Ok(ast::Expr::and( - (*left).clone().try_into_ast(id.clone())?, - (*right).clone().try_into_ast(id)?, + Arc::unwrap_or_clone(left).try_into_ast(id.clone())?, + Arc::unwrap_or_clone(right).try_into_ast(id)?, )), Expr::ExprNoExt(ExprNoExt::Or { left, right }) => Ok(ast::Expr::or( - (*left).clone().try_into_ast(id.clone())?, - (*right).clone().try_into_ast(id)?, + Arc::unwrap_or_clone(left).try_into_ast(id.clone())?, + Arc::unwrap_or_clone(right).try_into_ast(id)?, )), Expr::ExprNoExt(ExprNoExt::Add { left, right }) => Ok(ast::Expr::add( - (*left).clone().try_into_ast(id.clone())?, - (*right).clone().try_into_ast(id)?, + Arc::unwrap_or_clone(left).try_into_ast(id.clone())?, + Arc::unwrap_or_clone(right).try_into_ast(id)?, )), Expr::ExprNoExt(ExprNoExt::Sub { left, right }) => Ok(ast::Expr::sub( - (*left).clone().try_into_ast(id.clone())?, - (*right).clone().try_into_ast(id)?, + Arc::unwrap_or_clone(left).try_into_ast(id.clone())?, + Arc::unwrap_or_clone(right).try_into_ast(id)?, )), Expr::ExprNoExt(ExprNoExt::Mul { left, right }) => Ok(ast::Expr::mul( - (*left).clone().try_into_ast(id.clone())?, - (*right).clone().try_into_ast(id)?, + Arc::unwrap_or_clone(left).try_into_ast(id.clone())?, + Arc::unwrap_or_clone(right).try_into_ast(id)?, )), Expr::ExprNoExt(ExprNoExt::Contains { left, right }) => Ok(ast::Expr::contains( - (*left).clone().try_into_ast(id.clone())?, - (*right).clone().try_into_ast(id)?, + Arc::unwrap_or_clone(left).try_into_ast(id.clone())?, + Arc::unwrap_or_clone(right).try_into_ast(id)?, )), Expr::ExprNoExt(ExprNoExt::ContainsAll { left, right }) => Ok(ast::Expr::contains_all( - (*left).clone().try_into_ast(id.clone())?, - (*right).clone().try_into_ast(id)?, + Arc::unwrap_or_clone(left).try_into_ast(id.clone())?, + Arc::unwrap_or_clone(right).try_into_ast(id)?, )), Expr::ExprNoExt(ExprNoExt::ContainsAny { left, right }) => Ok(ast::Expr::contains_any( - (*left).clone().try_into_ast(id.clone())?, - (*right).clone().try_into_ast(id)?, + Arc::unwrap_or_clone(left).try_into_ast(id.clone())?, + Arc::unwrap_or_clone(right).try_into_ast(id)?, + )), + Expr::ExprNoExt(ExprNoExt::IsEmpty { arg }) => Ok(ast::Expr::is_empty( + Arc::unwrap_or_clone(arg).try_into_ast(id)?, )), - Expr::ExprNoExt(ExprNoExt::IsEmpty { arg }) => { - Ok(ast::Expr::is_empty((*arg).clone().try_into_ast(id)?)) - } Expr::ExprNoExt(ExprNoExt::GetTag { left, right }) => Ok(ast::Expr::get_tag( - (*left).clone().try_into_ast(id.clone())?, - (*right).clone().try_into_ast(id)?, + Arc::unwrap_or_clone(left).try_into_ast(id.clone())?, + Arc::unwrap_or_clone(right).try_into_ast(id)?, )), Expr::ExprNoExt(ExprNoExt::HasTag { left, right }) => Ok(ast::Expr::has_tag( - (*left).clone().try_into_ast(id.clone())?, - (*right).clone().try_into_ast(id)?, + Arc::unwrap_or_clone(left).try_into_ast(id.clone())?, + Arc::unwrap_or_clone(right).try_into_ast(id)?, + )), + Expr::ExprNoExt(ExprNoExt::GetAttr { left, attr }) => Ok(ast::Expr::get_attr( + Arc::unwrap_or_clone(left).try_into_ast(id)?, + attr, + )), + Expr::ExprNoExt(ExprNoExt::HasAttr { left, attr }) => Ok(ast::Expr::has_attr( + Arc::unwrap_or_clone(left).try_into_ast(id)?, + attr, )), - Expr::ExprNoExt(ExprNoExt::GetAttr { left, attr }) => { - Ok(ast::Expr::get_attr((*left).clone().try_into_ast(id)?, attr)) - } - Expr::ExprNoExt(ExprNoExt::HasAttr { left, attr }) => { - Ok(ast::Expr::has_attr((*left).clone().try_into_ast(id)?, attr)) - } Expr::ExprNoExt(ExprNoExt::Like { left, pattern }) => Ok(ast::Expr::like( - (*left).clone().try_into_ast(id)?, + Arc::unwrap_or_clone(left).try_into_ast(id)?, crate::ast::Pattern::from(pattern.as_slice()), )), Expr::ExprNoExt(ExprNoExt::Is { @@ -926,14 +936,14 @@ impl Expr { }) => ast::EntityType::from_normalized_str(entity_type.as_str()) .map_err(FromJsonError::InvalidEntityType) .and_then(|entity_type_name| { - let left: ast::Expr = (*left).clone().try_into_ast(id.clone())?; + let left: ast::Expr = Arc::unwrap_or_clone(left).try_into_ast(id.clone())?; let is_expr = ast::Expr::is_entity_type(left.clone(), entity_type_name); match in_expr { // The AST doesn't have an `... is ... in ..` node, so // we represent it as a conjunction of `is` and `in`. Some(in_expr) => Ok(ast::Expr::and( is_expr, - ast::Expr::is_in(left, (*in_expr).clone().try_into_ast(id)?), + ast::Expr::is_in(left, Arc::unwrap_or_clone(in_expr).try_into_ast(id)?), )), None => Ok(is_expr), } @@ -943,9 +953,9 @@ impl Expr { then_expr, else_expr, }) => Ok(ast::Expr::ite( - (*cond_expr).clone().try_into_ast(id.clone())?, - (*then_expr).clone().try_into_ast(id.clone())?, - (*else_expr).clone().try_into_ast(id)?, + Arc::unwrap_or_clone(cond_expr).try_into_ast(id.clone())?, + Arc::unwrap_or_clone(then_expr).try_into_ast(id.clone())?, + Arc::unwrap_or_clone(else_expr).try_into_ast(id)?, )), Expr::ExprNoExt(ExprNoExt::Set(elements)) => Ok(ast::Expr::set( elements diff --git a/cedar-policy-core/src/evaluator.rs b/cedar-policy-core/src/evaluator.rs index 864eaca9e..5a434d72c 100644 --- a/cedar-policy-core/src/evaluator.rs +++ b/cedar-policy-core/src/evaluator.rs @@ -720,7 +720,7 @@ impl<'e> Evaluator<'e> { // `rhs` is a list of all the UIDs for which we need to // check if `uid1` is a descendant of let rhs = match arg2.value { - ValueKind::Lit(Literal::EntityUID(uid)) => vec![(*uid).clone()], + ValueKind::Lit(Literal::EntityUID(uid)) => vec![Arc::unwrap_or_clone(uid)], // we assume that iterating the `authoritative` BTreeSet is // approximately the same cost as iterating the `fast` HashSet ValueKind::Set(Set { authoritative, .. }) => authoritative From d1f1185ed55bf3126e3cc14271e7b66aa9895804 Mon Sep 17 00:00:00 2001 From: Craig Disselkoen Date: Tue, 31 Dec 2024 12:49:50 -0500 Subject: [PATCH 16/16] Add `Entity::new_with_tags()` and `Entity::tag()` (#1402) Signed-off-by: Craig Disselkoen --- cedar-policy/CHANGELOG.md | 1 + cedar-policy/src/api.rs | 51 ++++++++++++++++++++++++++++----------- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/cedar-policy/CHANGELOG.md b/cedar-policy/CHANGELOG.md index 9a0c76a93..e28401801 100644 --- a/cedar-policy/CHANGELOG.md +++ b/cedar-policy/CHANGELOG.md @@ -17,6 +17,7 @@ Cedar Language Version: TBD - Implemented [RFC 48 (schema annotations)](https://github.com/cedar-policy/rfcs/blob/main/text/0048-schema-annotations.md) (#1316) - New `.isEmpty()` operator on sets (#1358, resolving #1356) +- New `Entity::new_with_tags()` and `Entity::tag()` functions (#1402, resolving #1374) - Added protobuf schemas and (de)serialization code using on `prost` crate behind the experimental `protobufs` flag. - Added protobuf and JSON generation code to `cedar-policy-cli`. - Added a new get helper method to Context that allows easy extraction of generic values from the context by key. This method simplifies the common use case of retrieving values from Context objects. diff --git a/cedar-policy/src/api.rs b/cedar-policy/src/api.rs index 91c859d9d..0f75fd66a 100644 --- a/cedar-policy/src/api.rs +++ b/cedar-policy/src/api.rs @@ -166,15 +166,7 @@ impl Entity { attrs: HashMap, parents: HashSet, ) -> Result { - // note that we take a "parents" parameter here; we will compute TC when - // the `Entities` object is created - Ok(Self(ast::Entity::new( - uid.into(), - attrs.into_iter().map(|(k, v)| (SmolStr::from(k), v.0)), - parents.into_iter().map(EntityUid::into).collect(), - [], - Extensions::all_available(), - )?)) + Self::new_with_tags(uid, attrs, parents, []) } /// Create a new `Entity` with no attributes. @@ -191,6 +183,27 @@ impl Entity { )) } + /// Create a new `Entity` with this Uid, attributes, parents, and tags. + /// + /// Attribute and tag values are specified here as "restricted expressions". + /// See docs on [`RestrictedExpression`]. + pub fn new_with_tags( + uid: EntityUid, + attrs: impl IntoIterator, + parents: impl IntoIterator, + tags: impl IntoIterator, + ) -> Result { + // note that we take a "parents" parameter here, not "ancestors"; we + // will compute TC when the `Entities` object is created + Ok(Self(ast::Entity::new( + uid.into(), + attrs.into_iter().map(|(k, v)| (k.into(), v.0)), + parents.into_iter().map(EntityUid::into).collect(), + tags.into_iter().map(|(k, v)| (k.into(), v.0)), + Extensions::all_available(), + )?)) + } + /// Create a new `Entity` with this Uid, no attributes, and no parents. /// ``` /// # use cedar_policy::{Entity, EntityId, EntityTypeName, EntityUid}; @@ -240,11 +253,21 @@ impl Entity { /// assert!(entity.attr("foo").is_none()); /// ``` pub fn attr(&self, attr: &str) -> Option> { - let v = match ast::Value::try_from(self.0.get(attr)?.clone()) { - Ok(v) => v, - Err(e) => return Some(Err(e)), - }; - Some(Ok(EvalResult::from(v))) + match ast::Value::try_from(self.0.get(attr)?.clone()) { + Ok(v) => Some(Ok(EvalResult::from(v))), + Err(e) => Some(Err(e)), + } + } + + /// Get the value for the given tag, or `None` if not present. + /// + /// This can also return Some(Err) if the tag is not a value (i.e., is + /// unknown due to partial evaluation). + pub fn tag(&self, tag: &str) -> Option> { + match ast::Value::try_from(self.0.get_tag(tag)?.clone()) { + Ok(v) => Some(Ok(EvalResult::from(v))), + Err(e) => Some(Err(e)), + } } /// Consume the entity and return the entity's owned Uid, attributes and parents.