blindplane-access 0.1.0

Signed enterprise access grants, capability policies, revocation and encrypted audit events for Blindplane
Documentation
//! Signed tenant capability-policy behavior.

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)
    );
}