Skip to content

Commit 5fca202

Browse files
Aaron Elineshaobo-he-aws
authored andcommitted
Fixes an issue where certain cedar policies couldn't be converted to JSON (#601)
1 parent 8984a76 commit 5fca202

File tree

4 files changed

+133
-32
lines changed

4 files changed

+133
-32
lines changed

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

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -989,28 +989,38 @@ impl TryFrom<cst::Mult> for Expr {
989989
impl TryFrom<cst::Unary> for Expr {
990990
type Error = ParseErrors;
991991
fn try_from(u: cst::Unary) -> Result<Expr, ParseErrors> {
992-
let inner = match u.item.node {
993-
Some(m) => m.try_into(),
992+
// We need to delay the conversion to where it's needed
993+
let member_node_to_expr = |node: Option<cst::Member>| match node {
994+
Some(member) => member.try_into(),
994995
None => Err(ParseError::ToAST("node should not be empty".to_string()).into()),
995-
}?;
996+
};
997+
996998
match u.op {
997-
Some(cst::NegOp::Bang(0)) => Ok(inner),
998-
Some(cst::NegOp::Bang(1)) => Ok(Expr::not(inner)),
999-
Some(cst::NegOp::Bang(2)) => {
1000-
// not safe to collapse !! to nothing
1001-
Ok(Expr::not(Expr::not(inner)))
1002-
}
1003-
Some(cst::NegOp::Bang(n)) => {
1004-
if n % 2 == 0 {
1005-
// safe to collapse to !! but not to nothing
1006-
Ok(Expr::not(Expr::not(inner)))
1007-
} else {
1008-
// safe to collapse to !
1009-
Ok(Expr::not(inner))
999+
Some(cst::NegOp::Bang(num_bangs)) => {
1000+
let inner = member_node_to_expr(u.item.node)?;
1001+
match num_bangs {
1002+
0 => Ok(inner),
1003+
1 => Ok(Expr::not(inner)),
1004+
2 => Ok(Expr::not(Expr::not(inner))),
1005+
3 => Ok(Expr::not(Expr::not(Expr::not(inner)))),
1006+
4 => Ok(Expr::not(Expr::not(Expr::not(Expr::not(inner))))),
1007+
_ => Err(ParseError::ToAST("Too many !'s".to_string()).into()),
10101008
}
10111009
}
1012-
Some(cst::NegOp::Dash(0)) => Ok(inner),
1010+
Some(cst::NegOp::Dash(0)) => member_node_to_expr(u.item.node),
10131011
Some(cst::NegOp::Dash(mut num_dashes)) => {
1012+
let inner = match u.item.to_lit() {
1013+
Some(cst::Literal::Num(num))
1014+
if num
1015+
.checked_sub(1)
1016+
.map(|y| y == i64::MAX as u64)
1017+
.unwrap_or(false) =>
1018+
{
1019+
num_dashes -= 1;
1020+
Expr::ExprNoExt(ExprNoExt::Value(JSONValue::Long(i64::MIN)))
1021+
}
1022+
_ => member_node_to_expr(u.item.node)?,
1023+
};
10141024
let inner = match inner {
10151025
Expr::ExprNoExt(ExprNoExt::Value(JSONValue::Long(n))) if n != std::i64::MIN => {
10161026
// collapse the negated literal into a single negative literal.
@@ -1027,20 +1037,14 @@ impl TryFrom<cst::Unary> for Expr {
10271037
// not safe to collapse `--` to nothing
10281038
Ok(Expr::neg(Expr::neg(inner)))
10291039
}
1030-
n => {
1031-
if n % 2 == 0 {
1032-
// safe to collapse to `--` but not to nothing
1033-
Ok(Expr::neg(Expr::neg(inner)))
1034-
} else {
1035-
// safe to collapse to -
1036-
Ok(Expr::neg(inner))
1037-
}
1038-
}
1040+
3 => Ok(Expr::neg(Expr::neg(Expr::neg(inner)))),
1041+
4 => Ok(Expr::neg(Expr::neg(Expr::neg(Expr::neg(inner))))),
1042+
_ => Err(ParseError::ToAST("Too many -'s".to_string()).into()),
10391043
}
10401044
}
10411045
Some(cst::NegOp::OverBang) => Err(ParseError::ToAST("Too many !'s".to_string()).into()),
10421046
Some(cst::NegOp::OverDash) => Err(ParseError::ToAST("Too many -'s".to_string()).into()),
1043-
None => Ok(inner),
1047+
None => member_node_to_expr(u.item.node),
10441048
}
10451049
}
10461050
}

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1363,11 +1363,11 @@ enum AstAccessor {
13631363
}
13641364

13651365
impl ASTNode<Option<cst::Member>> {
1366-
// Try to convert `cst::Member` into a `cst::Literal`, i.e.
1367-
// match `Member(Primary(Literal(_), []))`.
1368-
// It does not match the `Expr` arm of `Primary`, which means expressions
1369-
// like `(1)` are not considered as literals on the CST level.
1370-
fn to_lit(&self) -> Option<&cst::Literal> {
1366+
/// Try to convert `cst::Member` into a `cst::Literal`, i.e.
1367+
/// match `Member(Primary(Literal(_), []))`.
1368+
/// It does not match the `Expr` arm of `Primary`, which means expressions
1369+
/// like `(1)` are not considered as literals on the CST level.
1370+
pub(crate) fn to_lit(&self) -> Option<&cst::Literal> {
13711371
let m = self.as_ref().node.as_ref()?;
13721372
if !m.access.is_empty() {
13731373
return None;

cedar-policy/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ Cedar Language Version: 2.1.3
1919
policy. With this fix it is possible to create a link with a policy id
2020
after previously failing to create that link with the same id from a static
2121
policy.
22+
- Fixes an issue where certain cedar policies couldn't be converted to JSON (#601)
2223

2324
## 2.4.2
2425

cedar-policy/src/api.rs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4766,3 +4766,99 @@ mod schema_based_parsing_tests {
47664766
);
47674767
}
47684768
}
4769+
#[cfg(test)]
4770+
// PANIC SAFETY: unit tests
4771+
#[allow(clippy::unwrap_used)]
4772+
mod test {
4773+
use super::*;
4774+
4775+
#[test]
4776+
fn test_all_ints() {
4777+
test_single_int(0);
4778+
test_single_int(i64::MAX);
4779+
test_single_int(i64::MIN);
4780+
test_single_int(7);
4781+
test_single_int(-7);
4782+
}
4783+
4784+
fn test_single_int(x: i64) {
4785+
for i in 0..4 {
4786+
test_single_int_with_dashes(x, i);
4787+
}
4788+
}
4789+
4790+
fn test_single_int_with_dashes(x: i64, num_dashes: usize) {
4791+
let dashes = vec!['-'; num_dashes].into_iter().collect::<String>();
4792+
let src = format!(r#"permit(principal, action, resource) when {{ {dashes}{x} }};"#);
4793+
let p: Policy = src.parse().unwrap();
4794+
let json = p.to_json().unwrap();
4795+
let round_trip = Policy::from_json(None, json).unwrap();
4796+
let pretty_print = format!("{round_trip}");
4797+
assert!(pretty_print.contains(&x.to_string()));
4798+
if x != 0 {
4799+
let expected_dashes = if x < 0 { num_dashes + 1 } else { num_dashes };
4800+
assert_eq!(
4801+
pretty_print.chars().filter(|c| *c == '-').count(),
4802+
expected_dashes
4803+
);
4804+
}
4805+
}
4806+
4807+
// Serializing a valid 64-bit int that can't be represented in double precision float
4808+
#[test]
4809+
fn json_bignum_1() {
4810+
let src = r#"
4811+
permit(
4812+
principal,
4813+
action == Action::"action",
4814+
resource
4815+
) when {
4816+
-9223372036854775808
4817+
};"#;
4818+
let p: Policy = src.parse().unwrap();
4819+
p.to_json().unwrap();
4820+
}
4821+
4822+
#[test]
4823+
fn json_bignum_1a() {
4824+
let src = r#"
4825+
permit(principal, action, resource) when {
4826+
(true && (-90071992547409921)) && principal
4827+
};"#;
4828+
let p: Policy = src.parse().unwrap();
4829+
let v = p.to_json().unwrap();
4830+
let s = serde_json::to_string(&v).unwrap();
4831+
assert!(s.contains("90071992547409921"));
4832+
}
4833+
4834+
// Deserializing a valid 64-bit int that can't be represented in double precision float
4835+
#[test]
4836+
fn json_bignum_2() {
4837+
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}}}}]}"#;
4838+
let v: serde_json::Value = serde_json::from_str(src).unwrap();
4839+
let p = Policy::from_json(None, v).unwrap();
4840+
let pretty = format!("{p}");
4841+
// Ensure the number didn't get rounded
4842+
assert!(pretty.contains("90071992547409921"));
4843+
}
4844+
4845+
// Deserializing a valid 64-bit int that can't be represented in double precision float
4846+
#[test]
4847+
fn json_bignum_2a() {
4848+
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}}}}]}"#;
4849+
let v: serde_json::Value = serde_json::from_str(src).unwrap();
4850+
let p = Policy::from_json(None, v).unwrap();
4851+
let pretty = format!("{p}");
4852+
// Ensure the number didn't get rounded
4853+
assert!(pretty.contains("-9223372036854775808"));
4854+
}
4855+
4856+
// Deserializing a number that doesn't fit in 64 bit integer
4857+
// This _should_ fail, as there's no way to do this w/out loss of precision
4858+
#[test]
4859+
fn json_bignum_3() {
4860+
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}}}}]}"#;
4861+
let v: serde_json::Value = serde_json::from_str(src).unwrap();
4862+
assert!(Policy::from_json(None, v).is_err());
4863+
}
4864+
}

0 commit comments

Comments
 (0)