Skip to content

Commit caf7f83

Browse files
author
Aaron Eline
authored
Fixes an issue where certain cedar policies couldn't be converted to JSON (#601)
1 parent 1a96112 commit caf7f83

File tree

4 files changed

+134
-30
lines changed

4 files changed

+134
-30
lines changed

cedar-policy-core/src/est/expr.rs

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1011,25 +1011,35 @@ impl TryFrom<&Node<Option<cst::Unary>>> for Expr {
10111011
type Error = ParseErrors;
10121012
fn try_from(u: &Node<Option<cst::Unary>>) -> Result<Expr, ParseErrors> {
10131013
let u_node = u.ok_or_missing()?;
1014-
let inner = (&u_node.item).try_into()?;
1014+
10151015
match u_node.op {
1016-
Some(cst::NegOp::Bang(0)) => Ok(inner),
1017-
Some(cst::NegOp::Bang(1)) => Ok(Expr::not(inner)),
1018-
Some(cst::NegOp::Bang(2)) => {
1019-
// not safe to collapse !! to nothing
1020-
Ok(Expr::not(Expr::not(inner)))
1021-
}
1022-
Some(cst::NegOp::Bang(n)) => {
1023-
if n % 2 == 0 {
1024-
// safe to collapse to !! but not to nothing
1025-
Ok(Expr::not(Expr::not(inner)))
1026-
} else {
1027-
// safe to collapse to !
1028-
Ok(Expr::not(inner))
1016+
Some(cst::NegOp::Bang(num_bangs)) => {
1017+
let inner = (&u_node.item).try_into()?;
1018+
match num_bangs {
1019+
0 => Ok(inner),
1020+
1 => Ok(Expr::not(inner)),
1021+
2 => Ok(Expr::not(Expr::not(inner))),
1022+
3 => Ok(Expr::not(Expr::not(Expr::not(inner)))),
1023+
4 => Ok(Expr::not(Expr::not(Expr::not(Expr::not(inner))))),
1024+
_ => Err(u
1025+
.to_ast_err(ToASTErrorKind::UnaryOpLimit(ast::UnaryOp::Not))
1026+
.into()),
10291027
}
10301028
}
1031-
Some(cst::NegOp::Dash(0)) => Ok(inner),
1029+
Some(cst::NegOp::Dash(0)) => Ok((&u_node.item).try_into()?),
10321030
Some(cst::NegOp::Dash(mut num_dashes)) => {
1031+
let inner = match &u_node.item.to_lit() {
1032+
Some(cst::Literal::Num(num))
1033+
if num
1034+
.checked_sub(1)
1035+
.map(|y| y == InputInteger::MAX as u64)
1036+
.unwrap_or(false) =>
1037+
{
1038+
num_dashes -= 1;
1039+
Expr::ExprNoExt(ExprNoExt::Value(CedarValueJson::Long(InputInteger::MIN)))
1040+
}
1041+
_ => (&u_node.item).try_into()?,
1042+
};
10331043
let inner = match inner {
10341044
Expr::ExprNoExt(ExprNoExt::Value(CedarValueJson::Long(n)))
10351045
if n != InputInteger::MIN =>
@@ -1048,15 +1058,11 @@ impl TryFrom<&Node<Option<cst::Unary>>> for Expr {
10481058
// not safe to collapse `--` to nothing
10491059
Ok(Expr::neg(Expr::neg(inner)))
10501060
}
1051-
n => {
1052-
if n % 2 == 0 {
1053-
// safe to collapse to `--` but not to nothing
1054-
Ok(Expr::neg(Expr::neg(inner)))
1055-
} else {
1056-
// safe to collapse to -
1057-
Ok(Expr::neg(inner))
1058-
}
1059-
}
1061+
3 => Ok(Expr::neg(Expr::neg(Expr::neg(inner)))),
1062+
4 => Ok(Expr::neg(Expr::neg(Expr::neg(Expr::neg(inner))))),
1063+
_ => Err(u
1064+
.to_ast_err(ToASTErrorKind::UnaryOpLimit(ast::UnaryOp::Neg))
1065+
.into()),
10601066
}
10611067
}
10621068
Some(cst::NegOp::OverBang) => Err(u
@@ -1065,7 +1071,7 @@ impl TryFrom<&Node<Option<cst::Unary>>> for Expr {
10651071
Some(cst::NegOp::OverDash) => Err(u
10661072
.to_ast_err(ToASTErrorKind::UnaryOpLimit(ast::UnaryOp::Neg))
10671073
.into()),
1068-
None => Ok(inner),
1074+
None => Ok((&u_node.item).try_into()?),
10691075
}
10701076
}
10711077
}

cedar-policy-core/src/parser/cst_to_ast.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1574,11 +1574,11 @@ enum AstAccessor {
15741574
}
15751575

15761576
impl Node<Option<cst::Member>> {
1577-
// Try to convert `cst::Member` into a `cst::Literal`, i.e.
1578-
// match `Member(Primary(Literal(_), []))`.
1579-
// It does not match the `Expr` arm of `Primary`, which means expressions
1580-
// like `(1)` are not considered as literals on the CST level.
1581-
fn to_lit(&self) -> Option<&cst::Literal> {
1577+
/// Try to convert `cst::Member` into a `cst::Literal`, i.e.
1578+
/// match `Member(Primary(Literal(_), []))`.
1579+
/// It does not match the `Expr` arm of `Primary`, which means expressions
1580+
/// like `(1)` are not considered as literals on the CST level.
1581+
pub fn to_lit(&self) -> Option<&cst::Literal> {
15821582
let m = self.as_ref().node.as_ref()?;
15831583
if !m.access.is_empty() {
15841584
return None;

cedar-policy/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ method checks the request against the schema provided and the
6060
- The entity type tested for by an `is` expression may be an identifier shared
6161
with a builtin variable. E.g., `... is principal` and `... is action` are now
6262
accepted by the Cedar parser. (#558)
63+
- Policies containing the literal `i64::MIN` can now be properly converted to JSON ESTs (#601, resolving #596)
6364

6465
## [3.0.1] - 2023-12-21
6566
Cedar Language Version: 3.0.0

cedar-policy/src/api.rs

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3924,3 +3924,100 @@ mod partial_eval_test {
39243924
assert_eq!(a.residuals(), &p);
39253925
}
39263926
}
3927+
3928+
#[cfg(test)]
3929+
// PANIC SAFETY: unit tests
3930+
#[allow(clippy::unwrap_used)]
3931+
mod test {
3932+
use super::*;
3933+
3934+
#[test]
3935+
fn test_all_ints() {
3936+
test_single_int(0);
3937+
test_single_int(i64::MAX);
3938+
test_single_int(i64::MIN);
3939+
test_single_int(7);
3940+
test_single_int(-7);
3941+
}
3942+
3943+
fn test_single_int(x: i64) {
3944+
for i in 0..4 {
3945+
test_single_int_with_dashes(x, i);
3946+
}
3947+
}
3948+
3949+
fn test_single_int_with_dashes(x: i64, num_dashes: usize) {
3950+
let dashes = vec!['-'; num_dashes].into_iter().collect::<String>();
3951+
let src = format!(r#"permit(principal, action, resource) when {{ {dashes}{x} }};"#);
3952+
let p: Policy = src.parse().unwrap();
3953+
let json = p.to_json().unwrap();
3954+
let round_trip = Policy::from_json(None, json).unwrap();
3955+
let pretty_print = format!("{round_trip}");
3956+
assert!(pretty_print.contains(&x.to_string()));
3957+
if x != 0 {
3958+
let expected_dashes = if x < 0 { num_dashes + 1 } else { num_dashes };
3959+
assert_eq!(
3960+
pretty_print.chars().filter(|c| *c == '-').count(),
3961+
expected_dashes
3962+
);
3963+
}
3964+
}
3965+
3966+
// Serializing a valid 64-bit int that can't be represented in double precision float
3967+
#[test]
3968+
fn json_bignum_1() {
3969+
let src = r#"
3970+
permit(
3971+
principal,
3972+
action == Action::"action",
3973+
resource
3974+
) when {
3975+
-9223372036854775808
3976+
};"#;
3977+
let p: Policy = src.parse().unwrap();
3978+
p.to_json().unwrap();
3979+
}
3980+
3981+
#[test]
3982+
fn json_bignum_1a() {
3983+
let src = r#"
3984+
permit(principal, action, resource) when {
3985+
(true && (-90071992547409921)) && principal
3986+
};"#;
3987+
let p: Policy = src.parse().unwrap();
3988+
let v = p.to_json().unwrap();
3989+
let s = serde_json::to_string(&v).unwrap();
3990+
assert!(s.contains("90071992547409921"));
3991+
}
3992+
3993+
// Deserializing a valid 64-bit int that can't be represented in double precision float
3994+
#[test]
3995+
fn json_bignum_2() {
3996+
let src = r#"{"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"All"},"conditions":[{"kind":"when","body":{"==":{"left":{".":{"left":{"Var":"principal"},"attr":"x"}},"right":{"Value":90071992547409921}}}}]}"#;
3997+
let v: serde_json::Value = serde_json::from_str(src).unwrap();
3998+
let p = Policy::from_json(None, v).unwrap();
3999+
let pretty = format!("{p}");
4000+
// Ensure the number didn't get rounded
4001+
assert!(pretty.contains("90071992547409921"));
4002+
}
4003+
4004+
// Deserializing a valid 64-bit int that can't be represented in double precision float
4005+
#[test]
4006+
fn json_bignum_2a() {
4007+
let src = r#"{"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"All"},"conditions":[{"kind":"when","body":{"==":{"left":{".":{"left":{"Var":"principal"},"attr":"x"}},"right":{"Value":-9223372036854775808}}}}]}"#;
4008+
let v: serde_json::Value = serde_json::from_str(src).unwrap();
4009+
let p = Policy::from_json(None, v).unwrap();
4010+
let pretty = format!("{p}");
4011+
// Ensure the number didn't get rounded
4012+
assert!(pretty.contains("-9223372036854775808"));
4013+
}
4014+
4015+
// Deserializing a number that doesn't fit in 64 bit integer
4016+
// This _should_ fail, as there's no way to do this w/out loss of precision
4017+
#[test]
4018+
fn json_bignum_3() {
4019+
let src = r#"{"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"All"},"conditions":[{"kind":"when","body":{"==":{"left":{".":{"left":{"Var":"principal"},"attr":"x"}},"right":{"Value":9223372036854775808}}}}]}"#;
4020+
let v: serde_json::Value = serde_json::from_str(src).unwrap();
4021+
assert!(Policy::from_json(None, v).is_err());
4022+
}
4023+
}

0 commit comments

Comments
 (0)