blindplane-access 0.1.0

Signed enterprise access grants, capability policies, revocation and encrypted audit events for Blindplane
Documentation
//! End-to-end user and tenant-administrator audit visibility.

use blindplane_access::{
    AccessError, AuditContext, AuditEvent, AuditEventKind, RoleKeypair, open_audit_event,
    seal_audit_event,
};
use blindplane_core::{Author, fastest_payload_suite};

fn role(id: &str, seed: u8) -> RoleKeypair {
    RoleKeypair::from_secret_bytes("acme", "audit", id, 1, [seed; 32]).unwrap()
}

fn event(subject_id: &str, session_id: &str, body: &[u8]) -> AuditEvent {
    AuditEvent {
        tenant_id: "acme".into(),
        subject_id: subject_id.into(),
        session_id: session_id.into(),
        sequence: 1,
        timestamp: 1_786_406_400,
        kind: AuditEventKind::Request,
        media_type: "application/json".into(),
        body: body.to_vec(),
    }
}

fn context(subject_id: &str, session_id: &str) -> AuditContext {
    AuditContext {
        tenant_id: "acme".into(),
        subject_id: subject_id.into(),
        stream_id: session_id.into(),
        epoch: 1,
        sequence: 1,
    }
}

#[test]
fn each_user_opens_only_their_events_while_admin_opens_both() {
    let author = Author::from_secret_bytes(&[29; 32]);
    let alice_role = role("alice-audit", 31);
    let bob_role = role("bob-audit", 41);
    let admin_role = role("tenant-admin-audit", 51);
    let alice_event = event(
        "alice",
        "session-alice",
        br#"{"prompt":"secret alice request"}"#,
    );
    let bob_event = event("bob", "session-bob", br#"{"prompt":"secret bob request"}"#);

    let alice_record = seal_audit_event(
        &author,
        context("alice", "session-alice"),
        &alice_event,
        &alice_role,
        &admin_role,
        fastest_payload_suite(),
    )
    .unwrap();
    let bob_record = seal_audit_event(
        &author,
        context("bob", "session-bob"),
        &bob_event,
        &bob_role,
        &admin_role,
        fastest_payload_suite(),
    )
    .unwrap();

    assert!(
        !alice_record
            .encode()
            .windows(alice_event.body.len())
            .any(|window| window == alice_event.body)
    );
    assert!(
        !bob_record
            .encode()
            .windows(bob_event.body.len())
            .any(|window| window == bob_event.body)
    );

    assert_eq!(
        open_audit_event(&alice_record, &alice_role, author.public_key()).unwrap(),
        alice_event
    );
    assert_eq!(
        open_audit_event(&bob_record, &bob_role, author.public_key()).unwrap(),
        bob_event
    );
    assert_eq!(
        open_audit_event(&alice_record, &admin_role, author.public_key()).unwrap(),
        alice_event
    );
    assert_eq!(
        open_audit_event(&bob_record, &admin_role, author.public_key()).unwrap(),
        bob_event
    );
    assert_eq!(
        open_audit_event(&alice_record, &bob_role, author.public_key()),
        Err(AccessError::CryptographicFailure)
    );
}

#[test]
fn audit_context_must_match_the_encrypted_event() {
    let author = Author::from_secret_bytes(&[29; 32]);
    let alice_role = role("alice-audit", 31);
    let admin_role = role("tenant-admin-audit", 51);
    let alice_event = event("alice", "session-alice", b"request");
    let mismatched = context("bob", "session-alice");

    assert_eq!(
        seal_audit_event(
            &author,
            mismatched,
            &alice_event,
            &alice_role,
            &admin_role,
            fastest_payload_suite(),
        ),
        Err(AccessError::SubjectMismatch)
    );
}

#[test]
fn audit_event_encoding_is_bounded_and_canonical() {
    let event = event("alice", "session-alice", b"request");
    let encoded = event.encode();
    assert_eq!(AuditEvent::decode(&encoded).unwrap(), event);

    let mut trailing = encoded;
    trailing.push(0);
    assert_eq!(
        AuditEvent::decode(&trailing),
        Err(AccessError::TrailingBytes)
    );
}