Skip to main content

spacedb_access/
policy.rs

1//! Human-vs-AI access policy — the AI-age rule.
2//!
3//! Capabilities say *what a bearer may do*; policy says *who needs one*. The
4//! default posture: **humans in the owner's roster read freely; everyone else —
5//! every AI agent — needs an explicit, signed grant**, and (optionally) an
6//! agent's grant chain must root at an accountable roster member, so the agent's
7//! authority always traces back to a human/org.
8//!
9//! [`gate`] combines the policy with capability authorization into one decision.
10//!
11//! ## The honest boundary
12//!
13//! A capability (and this gate) grants **permission + accountability + billing**,
14//! **not confidentiality on untrusted compute**. The Phase-1 rule is: access is
15//! allowed only when it is *authorized-by-mID* **and** *executed on a node
16//! entitled to decrypt* (the owner's roster, holding the per-collection DEK).
17//! The second half is enforced by the storage layer's `KeyProvider` — a node
18//! without the key simply cannot read the ciphertext — not re-implemented here.
19//! Untrusted-node confidential compute (enclaves / FHE) is Phase 2.
20
21use std::collections::HashSet;
22
23use crate::authorize::{authorize_chain, AccessRequest, Decision, DenyReason};
24use crate::capability::Ops;
25use crate::chain::CapabilityChain;
26use crate::directory::KeyDirectory;
27use crate::error::AccessResult;
28use crate::identity::Did;
29use crate::revocation::RevocationSet;
30
31/// Who reads freely, and whether agents must chain to an accountable identity.
32#[derive(Clone, Debug, Default)]
33pub struct AccessPolicy {
34    roster: HashSet<Did>,
35    require_accountable_root: bool,
36}
37
38impl AccessPolicy {
39    pub fn new() -> Self {
40        Self::default()
41    }
42
43    /// Add an accountable human/org to the roster (builder style).
44    pub fn with_roster_member(mut self, did: impl Into<Did>) -> Self {
45        self.roster.insert(did.into());
46        self
47    }
48
49    /// Require that an agent's grant chain root at a roster member.
50    pub fn requiring_accountable_agents(mut self) -> Self {
51        self.require_accountable_root = true;
52        self
53    }
54
55    /// Whether `did` is an accountable roster member.
56    pub fn is_roster(&self, did: &Did) -> bool {
57        self.roster.contains(did)
58    }
59}
60
61/// Decide an access under policy + capabilities.
62///
63/// - A roster member performing a pure **read** is allowed with no capability.
64/// - Otherwise a capability chain is **required**; it is authorized normally
65///   (signature · narrowing · revocation · scope · ops · expiry).
66/// - If `require_accountable_root`, the chain must root at a roster member.
67pub fn gate(
68    policy: &AccessPolicy,
69    chain: Option<&CapabilityChain>,
70    request: &AccessRequest,
71    directory: &dyn KeyDirectory,
72    now_unix: u64,
73    revocations: &RevocationSet,
74) -> AccessResult<Decision> {
75    // Roster humans read freely (no grant needed).
76    if request.op == Ops::READ && policy.is_roster(request.bearer) {
77        return Ok(Decision::Allow);
78    }
79
80    // Everyone else needs a valid capability.
81    let chain = match chain {
82        Some(c) => c,
83        None => return Ok(Decision::Deny(DenyReason::NoCapability)),
84    };
85
86    let decision = authorize_chain(chain, request, directory, now_unix, revocations)?;
87    if !decision.is_allowed() {
88        return Ok(decision);
89    }
90
91    // The grant must trace back to an accountable identity.
92    if policy.require_accountable_root {
93        let root_issuer = &chain.links()[0].capability.issuer;
94        if !policy.is_roster(root_issuer) {
95            return Ok(Decision::Deny(DenyReason::NotAccountable));
96        }
97    }
98
99    Ok(Decision::Allow)
100}