use std::panic::AssertUnwindSafe;
use serde_json::{Value, json};
use crate::core::{PolicyDecision, PolicyEngine, PolicyRequest};
use super::conformance::Report;
fn shapes() -> Vec<(&'static str, String, String, String, Value)> {
let long = "x".repeat(4096);
vec![
(
"the effect gate",
"agent:triage".to_owned(),
"effect:perform".to_owned(),
"tool.call".to_owned(),
json!({ "amount_eur": 10, "labels": ["internal"] }),
),
(
"admission",
"agent:triage".to_owned(),
"run:admit".to_owned(),
"order.settle".to_owned(),
json!({ "input": { "amount_eur": 5000 } }),
),
(
"an operator API verb",
"operator:ana".to_owned(),
"api:task.decide".to_owned(),
"task".to_owned(),
json!({}),
),
(
"empty strings",
String::new(),
String::new(),
String::new(),
json!({}),
),
(
"a context that is not an object",
"agent:triage".to_owned(),
"effect:perform".to_owned(),
"tool.call".to_owned(),
Value::Null,
),
(
"a context that is an array",
"agent:triage".to_owned(),
"effect:perform".to_owned(),
"tool.call".to_owned(),
json!([1, 2, 3]),
),
(
"an unknown action",
"agent:triage".to_owned(),
"something:nobody:wrote:a:rule:for".to_owned(),
"tool.call".to_owned(),
json!({}),
),
(
"very long fields",
long.clone(),
long.clone(),
long,
json!({ "note": "x".repeat(4096) }),
),
(
"characters a rule language may treat specially",
"agent:\"; permit(principal, action, resource);".to_owned(),
"effect:perform".to_owned(),
"tool.call\u{0}\u{1f}\u{202e}".to_owned(),
json!({ "emoji": "🙂", "nested": { "deep": { "deeper": [null] } } }),
),
]
}
fn ask(engine: &dyn PolicyEngine, request: &PolicyRequest<'_>) -> Option<PolicyDecision> {
std::panic::catch_unwind(AssertUnwindSafe(|| engine.authorize(request))).ok()
}
pub fn check(engine: &dyn PolicyEngine, report: &mut Report) {
evaluation_is_total(engine, report);
evaluation_is_pure(engine, report);
evaluation_carries_no_state_between_requests(engine, report);
a_refusal_says_which_rule(engine, report);
the_bundle_identity_is_stable(engine, report);
the_digest_is_the_bundles(engine, report);
preflight_is_total(engine, report);
}
fn evaluation_is_total(engine: &dyn PolicyEngine, report: &mut Report) {
const RULE: &str = "evaluation is total";
for (what, principal, action, resource, context) in shapes() {
let request = PolicyRequest {
principal: &principal,
action: &action,
resource: &resource,
context: &context,
};
report.checked += 1;
if ask(engine, &request).is_none() {
report.record(
RULE,
format!(
"authorize panicked on {what} — a gate reached by every effect must \
answer rather than unwind, which is why the decision type has no \
error case"
),
);
}
}
}
fn evaluation_is_pure(engine: &dyn PolicyEngine, report: &mut Report) {
const RULE: &str = "evaluation is pure";
for (what, principal, action, resource, context) in shapes() {
let request = PolicyRequest {
principal: &principal,
action: &action,
resource: &resource,
context: &context,
};
let (Some(first), Some(second)) = (ask(engine, &request), ask(engine, &request)) else {
continue; };
report.checked += 1;
if first != second {
report.record(
RULE,
format!(
"{what} answered {first:?} then {second:?} — the journal records the \
bundle identity and the request so a verdict can be re-derived \
offline, and an engine that answers twice cannot be"
),
);
}
}
}
fn evaluation_carries_no_state_between_requests(engine: &dyn PolicyEngine, report: &mut Report) {
const RULE: &str = "no state between requests";
let all = shapes();
let forwards: Vec<Option<PolicyDecision>> = all
.iter()
.map(|(_, p, a, r, c)| {
ask(
engine,
&PolicyRequest {
principal: p,
action: a,
resource: r,
context: c,
},
)
})
.collect();
let backwards: Vec<Option<PolicyDecision>> = all
.iter()
.rev()
.map(|(_, p, a, r, c)| {
ask(
engine,
&PolicyRequest {
principal: p,
action: a,
resource: r,
context: c,
},
)
})
.collect();
report.checked += 1;
for (i, (forward, backward)) in forwards
.iter()
.zip(backwards.iter().rev())
.enumerate()
.filter(|(_, (f, b))| f != b)
{
let _ = (forward, backward);
report.record(
RULE,
format!(
"{} answered differently depending on what was evaluated before it — \
an engine holding state across requests decides one run's effect by \
another run's history",
all[i].0
),
);
}
}
fn a_refusal_says_which_rule(engine: &dyn PolicyEngine, report: &mut Report) {
const RULE: &str = "a refusal says which rule";
for (what, principal, action, resource, context) in shapes() {
let request = PolicyRequest {
principal: &principal,
action: &action,
resource: &resource,
context: &context,
};
let Some(decision) = ask(engine, &request) else {
continue;
};
let reason = match &decision {
PolicyDecision::Permit => continue,
PolicyDecision::Deny { reason } | PolicyDecision::Malformed { reason } => reason,
};
report.checked += 1;
if reason.trim().is_empty() {
report.record(
RULE,
format!(
"{what} was refused with an empty reason — the wrapper supplies the \
action and the resource, so this string's only job is saying which \
rule fired, and an empty one sends somebody to read the whole set"
),
);
}
}
}
fn the_bundle_identity_is_stable(engine: &dyn PolicyEngine, report: &mut Report) {
const RULE: &str = "the bundle identity is stable";
report.checked += 1;
let first = engine.bundle();
let second = engine.bundle();
if first.digest() != second.digest() {
report.record(
RULE,
"two calls to bundle() gave different digests — the identity is journaled \
once at admission and answers *what governed this run* forever after, so \
one that moves makes every such answer unfalsifiable",
);
}
}
fn the_digest_is_the_bundles(engine: &dyn PolicyEngine, report: &mut Report) {
const RULE: &str = "digest is the bundle's";
report.checked += 1;
if engine.digest() != engine.bundle().digest() {
report.record(
RULE,
"digest() and bundle().digest() disagree — one of them is journaled and the \
other is what an auditor recomputes, and nothing reconciles them",
);
}
}
fn preflight_is_total(engine: &dyn PolicyEngine, report: &mut Report) {
const RULE: &str = "preflight is total";
let all = shapes();
let requests: Vec<PolicyRequest<'_>> = all
.iter()
.map(|(_, p, a, r, c)| PolicyRequest {
principal: p,
action: a,
resource: r,
context: c,
})
.collect();
report.checked += 1;
if std::panic::catch_unwind(AssertUnwindSafe(|| engine.preflight(&requests))).is_err() {
report.record(
RULE,
"preflight panicked — it runs inside `build`, so this is a plane that cannot \
be assembled rather than a run that cannot be admitted",
);
}
}