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}