///|
pub(all) struct RuleImpact {
rule_id : String
grants_when_removed : Int
revocations_when_removed : Int
example_grant : AccessRequest?
example_revocation : AccessRequest?
} derive(Debug)
///|
pub(all) struct RuleImpactReport {
policy_name : String
impacts : Array[RuleImpact]
diagnostics : Array[Diagnostic]
complete : Bool
} derive(Debug)
///|
fn without_rule(policy : AccessPolicy, id : String) -> AccessPolicy {
let rules : Array[AccessRule] = []
for rule in policy.rules {
if rule.id != id {
rules.push(rule)
}
}
{ name: policy.name, role_inheritance: policy.role_inheritance, rules, }
}
///|
/// Measures the effect of deleting each rule, one at a time, over the
/// explicit universe. Interacting rule deletions are outside this analysis.
pub fn measure_rule_impact(
policy : AccessPolicy,
universe : RequestUniverse,
) -> RuleImpactReport {
let diagnostics : Array[Diagnostic] = []
for issue in validate_policy(policy) {
diagnostics.push(issue)
}
for issue in validate_universe(universe) {
diagnostics.push(issue)
}
if policy.rules.length() > 128 || universe.requests.length() > 5000 {
diagnostics.push({
code: "I001",
severity: Error,
message: "rule impact supports at most 128 rules and 5000 requests",
})
}
let impacts : Array[RuleImpact] = []
if has_errors(diagnostics) {
return { policy_name: policy.name, impacts, diagnostics, complete: false, }
}
for rule in policy.rules {
let ablated = without_rule(policy, rule.id)
let mut grants = 0
let mut revocations = 0
let mut example_grant : AccessRequest? = None
let mut example_revocation : AccessRequest? = None
for request in universe.requests {
let before = evaluate_request(policy, universe, request)
let after = evaluate_request(ablated, universe, request)
match classify_change(before, after) {
NewGrant => {
grants += 1
if example_grant is None {
example_grant = Some(request)
}
}
RevokedGrant => {
revocations += 1
if example_revocation is None {
example_revocation = Some(request)
}
}
_ => ()
}
}
impacts.push({
rule_id: rule.id,
grants_when_removed: grants,
revocations_when_removed: revocations,
example_grant,
example_revocation,
})
}
{ policy_name: policy.name, impacts, diagnostics, complete: true, }
}
///|
pub fn inert_rules(report : RuleImpactReport) -> Array[String] {
let ids : Array[String] = []
if !report.complete {
return ids
}
for impact in report.impacts {
if impact.grants_when_removed == 0 && impact.revocations_when_removed == 0 {
ids.push(impact.rule_id)
}
}
ids
}
///|
pub fn render_rule_impact(report : RuleImpactReport) -> String {
let out = StringBuilder()
out.write_string("Rule impact: \{report.policy_name}\n")
out.write_string("Complete: \{report.complete}\n")
for diagnostic in report.diagnostics {
out.write_string("\{diagnostic.code}: \{diagnostic.message}\n")
}
for item in report.impacts {
out.write_string(
"\{item.rule_id}: deleting grants \{item.grants_when_removed}, revokes \{item.revocations_when_removed}\n",
)
match item.example_grant {
Some(request) =>
out.write_string(
" grant witness: \{request.principal_id} \{request.action} \{request.resource_id}\n",
)
None => ()
}
match item.example_revocation {
Some(request) =>
out.write_string(
" revocation witness: \{request.principal_id} \{request.action} \{request.resource_id}\n",
)
None => ()
}
}
out.to_string()
}