use blindplane_access::{
AccessError, AccessIssuer, AccessValidationPolicy, CapabilityKind, CapabilityRule, Decision,
Effect, PolicySpec, PrincipalKeypair, PrincipalKind, TenantPolicy,
};
fn alice() -> PrincipalKeypair {
PrincipalKeypair::from_secret_bytes("acme", "alice", PrincipalKind::User, 1, [7; 32], [9; 32])
.unwrap()
}
fn make_issuer(seed: u8) -> AccessIssuer {
AccessIssuer::from_seed("tenant-admin", [seed; 32]).unwrap()
}
fn spec(rules: Vec<CapabilityRule>) -> PolicySpec {
PolicySpec {
revision: 3,
previous_hash: [4; 32],
authorization_epoch: 7,
issued_at: 90,
not_before: 100,
not_after: 200,
rules,
}
}
#[test]
fn verified_policy_is_exact_and_default_deny() {
let alice = alice();
let issuer = make_issuer(11);
let policy = TenantPolicy::issue(
&issuer,
alice.principal(),
spec(vec![
CapabilityRule::new(CapabilityKind::Skill, "deploy", Effect::Deny).unwrap(),
CapabilityRule::new(CapabilityKind::Mcp, "github", Effect::Allow).unwrap(),
]),
)
.unwrap();
let encoded = policy.encode();
let decoded = TenantPolicy::decode(&encoded, &AccessValidationPolicy::default()).unwrap();
assert_eq!(decoded, policy);
let verified = decoded
.verify(&issuer.trusted(), "acme", "alice", 150, 3, 7)
.unwrap();
assert_eq!(
verified.decision(CapabilityKind::Mcp, "github"),
Decision::Allow
);
assert_eq!(
verified.decision(CapabilityKind::Mcp, "GitHub"),
Decision::Deny
);
assert_eq!(
verified.decision(CapabilityKind::Skill, "deploy"),
Decision::Deny
);
assert_eq!(
verified.decision(CapabilityKind::Tool, "read"),
Decision::Deny
);
}
#[test]
fn policy_rejects_duplicate_capability_names() {
let alice = alice();
let issuer = make_issuer(11);
let duplicate = vec![
CapabilityRule::new(CapabilityKind::Mcp, "github", Effect::Allow).unwrap(),
CapabilityRule::new(CapabilityKind::Mcp, "github", Effect::Deny).unwrap(),
];
assert_eq!(
TenantPolicy::issue(&issuer, alice.principal(), spec(duplicate)),
Err(AccessError::DuplicateRule)
);
}
#[test]
fn policy_signature_and_issuer_pin_fail_closed() {
let alice = alice();
let issuer = make_issuer(11);
let stranger = make_issuer(12);
let policy = TenantPolicy::issue(&issuer, alice.principal(), spec(vec![])).unwrap();
assert_eq!(
policy.verify(&stranger.trusted(), "acme", "alice", 150, 3, 7),
Err(AccessError::UntrustedIssuer)
);
let mut tampered = policy.encode();
*tampered.last_mut().unwrap() ^= 1;
assert_eq!(
TenantPolicy::decode(&tampered, &AccessValidationPolicy::default()),
Err(AccessError::InvalidSignature)
);
}
#[test]
fn policy_checks_subject_time_revision_and_epoch_before_deciding() {
let alice = alice();
let issuer = make_issuer(11);
let policy = TenantPolicy::issue(&issuer, alice.principal(), spec(vec![])).unwrap();
let trusted = issuer.trusted();
assert_eq!(
policy.verify(&trusted, "acme", "bob", 150, 3, 7),
Err(AccessError::SubjectMismatch)
);
assert_eq!(
policy.verify(&trusted, "acme", "alice", 99, 3, 7),
Err(AccessError::NotYetValid)
);
assert_eq!(
policy.verify(&trusted, "acme", "alice", 201, 3, 7),
Err(AccessError::Expired)
);
assert_eq!(
policy.verify(&trusted, "acme", "alice", 150, 4, 7),
Err(AccessError::StaleRevision)
);
assert_eq!(
policy.verify(&trusted, "acme", "alice", 150, 3, 8),
Err(AccessError::Revoked)
);
}