Skip to content

Commit d7ee5a9

Browse files
authored
display analysis counterexamples for Compare in the Lean CLI (#860)
Signed-off-by: Craig Disselkoen <cdiss@amazon.com>
1 parent d1ab450 commit d7ee5a9

File tree

35 files changed

+1824
-725
lines changed

35 files changed

+1824
-725
lines changed

cedar-lean-cli/Cargo.toml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,20 @@ publish = false
66

77
[dependencies]
88
cedar-policy = { version = "*", path = "../cedar/cedar-policy" }
9+
cedar-policy-core = { version = "*", path = "../cedar/cedar-policy-core" }
910
cedar-lean-ffi = { version = "*", path = "../cedar-lean-ffi" }
1011
clap = { version = "4.5.36", features = ["derive"] }
12+
itertools = "0.14"
13+
miette = "7.6.0"
14+
nonempty = "0.12"
15+
prettytable-rs = "0.10"
1116
serde = "1"
1217
serde_json = "1.0"
1318
thiserror = "2.0"
14-
itertools = "0.14.0"
15-
miette = "7.6.0"
16-
prettytable-rs = "0.10"
1719

1820
[dev-dependencies]
1921
assert_cmd = "2.0.17"
20-
insta = "1.46"
22+
insta = { version = "1.46", features = ["filters"] }
2123

2224
# https://insta.rs/docs/quickstart/ recommends compiling `insta` and `similar` in release mode
2325
[profile.dev.package]

cedar-lean-cli/src/analysis.rs

Lines changed: 150 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@
1616
use crate::util::{AnalyzePolicyFindingsSer, OpenRequestEnv};
1717
use crate::{err::ExecError, util::RequestEnvSer};
1818
use cedar_lean_ffi::CedarLeanFfi;
19-
use cedar_policy::{Effect, Policy, PolicyId, PolicySet, RequestEnv, Schema};
19+
use cedar_policy::{Effect, Policy, PolicyId, PolicySet, RequestEnv, RestrictedExpression, Schema};
2020
use itertools::Itertools;
21+
use nonempty::NonEmpty;
2122
use prettytable::{Attr, Cell, Row, Table};
2223
use serde::Serialize;
2324
use std::{
@@ -491,10 +492,14 @@ fn vacuity_result(
491492
/// Represents if policy1 is shadowed by policy2 or vice versa
492493
#[derive(Clone, Copy, Debug, PartialEq)]
493494
enum ShadowingResult {
494-
Equivalent, // policies are non vacuous and allow the same requests
495-
Policy1Shadows2, // policies are non vacuous and policy1 allows strictly more requests than policy2
496-
Policy2Shadows1, // policies are non vacuous and policy2 allows strictly more requests than policy1
497-
NoResult, // at least one policy is vacuous, or the policies allow incomparable sets of requests
495+
/// policies are non vacuous and allow the same requests
496+
Equivalent,
497+
/// policies are non vacuous and policy1 allows strictly more requests than policy2
498+
Policy1Shadows2,
499+
/// policies are non vacuous and policy2 allows strictly more requests than policy1
500+
Policy2Shadows1,
501+
/// at least one policy is vacuous, or the policies allow incomparable sets of requests
502+
NoResult,
498503
}
499504

500505
/// Compute Redudant and Shadowed relationship between `policy1` and `policy2` (per environment)
@@ -698,26 +703,142 @@ fn update_findings<T>(
698703
}
699704
}
700705

706+
fn display_value(f: &mut std::fmt::Formatter<'_>, v: &cedar_lean_ffi::Value) -> std::fmt::Result {
707+
let rexpr = RestrictedExpression::try_from(v.clone()).unwrap();
708+
let core_rexpr: &cedar_policy_core::ast::RestrictedExpr = rexpr.as_ref();
709+
write!(f, "{core_rexpr}")
710+
}
711+
712+
/// `prefix`: printed at the beginning of each line, including the first
713+
fn display_entity(
714+
f: &mut std::fmt::Formatter<'_>,
715+
uid: &cedar_lean_ffi::EntityUid,
716+
edata: &cedar_lean_ffi::EntityData,
717+
prefix: &str,
718+
) -> std::fmt::Result {
719+
write!(f, "{prefix}{uid}")?;
720+
if let Some(ancs) = NonEmpty::collect(edata.ancestors.iter()) {
721+
write!(f, " in [{ancs}]", ancs = ancs.iter().join(", "))?;
722+
}
723+
if let Some(attrs) = NonEmpty::collect(edata.attrs.iter()) {
724+
write!(f, " {{\n")?;
725+
for (k, v) in attrs.iter() {
726+
if cedar_policy_core::ast::is_normalized_ident(k) {
727+
write!(f, "{prefix} {k}: ")?;
728+
} else {
729+
write!(f, "{prefix} \"{}\": ", k.escape_debug())?;
730+
}
731+
display_value(f, v)?;
732+
write!(f, ",\n")?;
733+
}
734+
write!(f, "{prefix}}}")?;
735+
}
736+
if let Some(tags) = NonEmpty::collect(edata.tags.iter()) {
737+
write!(f, " tags {{\n")?;
738+
for (k, v) in tags.iter() {
739+
if cedar_policy_core::ast::is_normalized_ident(k) {
740+
write!(f, "{prefix} {k}: ")?;
741+
} else {
742+
write!(f, "{prefix} \"{}\": ", k.escape_debug())?;
743+
}
744+
display_value(f, v)?;
745+
write!(f, ",\n")?;
746+
}
747+
write!(f, "{prefix}}}")?;
748+
}
749+
Ok(())
750+
}
751+
752+
#[derive(Debug, Clone, Serialize)]
753+
#[serde(transparent)]
754+
struct ExampleEnv(cedar_lean_ffi::Env);
755+
756+
impl std::fmt::Display for ExampleEnv {
757+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
758+
write!(
759+
f,
760+
"principal: {p}, action: {a}, resource: {r}",
761+
p = self.0.request.principal,
762+
a = self.0.request.action,
763+
r = self.0.request.resource,
764+
)?;
765+
match self.0.request.context.len() {
766+
0 => (),
767+
1 => {
768+
let (k, v) = self.0.request.context.first().unwrap();
769+
if cedar_policy_core::ast::is_normalized_ident(k) {
770+
write!(f, "\ncontext: {{ {k}: ")?;
771+
} else {
772+
write!(f, "\ncontext: {{ \"{}\": ", k.escape_debug())?;
773+
}
774+
display_value(f, v)?;
775+
write!(f, " }}")?;
776+
}
777+
_ => {
778+
writeln!(f, "\ncontext: {{")?;
779+
for (k, v) in self.0.request.context.iter() {
780+
if cedar_policy_core::ast::is_normalized_ident(k) {
781+
write!(f, " {k}: ")?;
782+
} else {
783+
write!(f, " \"{}\": ", k.escape_debug())?;
784+
}
785+
display_value(f, v)?;
786+
write!(f, ",\n")?;
787+
}
788+
write!(f, "}}")?;
789+
}
790+
}
791+
match self.0.entities.len() {
792+
0 => (),
793+
_ => {
794+
writeln!(f, "\nentities: [")?;
795+
for (uid, edata) in self.0.entities.iter() {
796+
display_entity(f, uid, edata, " ")?;
797+
writeln!(f, ",")?;
798+
}
799+
write!(f, "]")?;
800+
}
801+
}
802+
writeln!(f)
803+
}
804+
}
805+
701806
#[derive(Debug, Clone, Serialize)]
702807
enum PolicySetComparisonStatus {
703-
MorePermissive,
704-
LessPermissive,
808+
MorePermissive {
809+
/// Env where the first policyset allows and the second denies
810+
example: ExampleEnv,
811+
},
812+
LessPermissive {
813+
/// Env where the first policyset denies and the second allows
814+
example: ExampleEnv,
815+
},
705816
Equivalent,
706-
Incomparable,
817+
Incomparable {
818+
/// Env where the first policyset allows and the second denies
819+
more_permissive_example: ExampleEnv,
820+
/// Env where the first policyset denies and the second allows
821+
less_permissive_example: ExampleEnv,
822+
},
707823
}
708824

709825
impl PolicySetComparisonStatus {
710826
fn print(self) -> String {
711827
match self {
712-
PolicySetComparisonStatus::MorePermissive => {
713-
String::from("pset1 is more permissive than pset2")
828+
PolicySetComparisonStatus::MorePermissive { example } => {
829+
format!("pset1 is more permissive than pset2\nExample: {example}")
714830
}
715-
PolicySetComparisonStatus::LessPermissive => {
716-
String::from("pset1 is less permissive than pset2")
831+
PolicySetComparisonStatus::LessPermissive { example } => {
832+
format!("pset1 is less permissive than pset2\nExample: {example}")
717833
}
718834
PolicySetComparisonStatus::Equivalent => String::from("pset1 is equivalent to pset2"),
719-
PolicySetComparisonStatus::Incomparable => {
720-
String::from("pset1 is incomparable with pset2")
835+
PolicySetComparisonStatus::Incomparable {
836+
more_permissive_example,
837+
less_permissive_example,
838+
} => {
839+
format!(
840+
"pset1 is incomparable with pset2\nExample where pset1 is more permissive: {more_permissive_example}\nExample where pset1 is less permissive: {less_permissive_example}"
841+
)
721842
}
722843
}
723844
}
@@ -757,20 +878,27 @@ pub fn compare_policysets(
757878
json_output: bool,
758879
) -> Result<(), ExecError> {
759880
let req_envs = OpenRequestEnv::any().to_request_envs(&schema)?;
760-
let lean_context = CedarLeanFfi::new();
761-
let schema = lean_context.load_lean_schema_object(&schema)?;
881+
let lean_ffi = CedarLeanFfi::new();
882+
let schema = lean_ffi.load_lean_schema_object(&schema)?;
762883
let comparison_results: Vec<PolicySetComparisonResult> = req_envs
763884
.iter()
764885
.map(|req_env| -> Result<PolicySetComparisonResult, ExecError> {
765886
let fwd_implies =
766-
lean_context.run_check_implies(&pset1, &pset2, schema.clone(), req_env)?;
887+
lean_ffi.run_check_implies_with_cex(&pset1, &pset2, schema.clone(), req_env)?;
767888
let bwd_implies =
768-
lean_context.run_check_implies(&pset2, &pset1, schema.clone(), req_env)?;
889+
lean_ffi.run_check_implies_with_cex(&pset2, &pset1, schema.clone(), req_env)?;
769890
let status = match (fwd_implies, bwd_implies) {
770-
(true, true) => PolicySetComparisonStatus::Equivalent,
771-
(true, false) => PolicySetComparisonStatus::LessPermissive,
772-
(false, true) => PolicySetComparisonStatus::MorePermissive,
773-
(false, false) => PolicySetComparisonStatus::Incomparable,
891+
(None, None) => PolicySetComparisonStatus::Equivalent,
892+
(None, Some(cex)) => PolicySetComparisonStatus::LessPermissive {
893+
example: ExampleEnv(cex),
894+
},
895+
(Some(cex), None) => PolicySetComparisonStatus::MorePermissive {
896+
example: ExampleEnv(cex),
897+
},
898+
(Some(more_cex), Some(less_cex)) => PolicySetComparisonStatus::Incomparable {
899+
more_permissive_example: ExampleEnv(more_cex),
900+
less_permissive_example: ExampleEnv(less_cex),
901+
},
774902
};
775903
Ok(PolicySetComparisonResult {
776904
req_env: RequestEnvSer::new(req_env),

0 commit comments

Comments
 (0)