Skip to main content

spacedb_access/
capability.rs

1//! The capability — a signed, scoped, expiring grant.
2//!
3//! An owner mints a [`Capability`] to a bearer (a human or an AI agent),
4//! describing exactly what it may do (`scope` × `ops`), for how long (`expiry`),
5//! within what budget, and how far it may be re-delegated. The owner signs the
6//! canonical bytes, producing a [`SignedCapability`] anyone can verify against the
7//! owner's published key. Nothing is accessible without one (for AI), and
8//! everything granted is attributable, expiring, and (S2) revocable.
9
10use serde::{Deserialize, Serialize};
11
12use crate::error::{AccessError, AccessResult};
13use crate::identity::{Did, Identity};
14
15/// What a capability applies to.
16#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
17pub enum Scope {
18    /// An entire collection (and every document in it).
19    Collection(String),
20    /// A single document within a collection.
21    Document { collection: String, doc_id: String },
22    /// A named function (compute).
23    Function(String),
24}
25
26impl Scope {
27    /// Whether this (granted) scope covers a `requested` access scope. A
28    /// collection grant covers any document in it; document and function grants
29    /// match exactly.
30    pub fn covers(&self, requested: &Scope) -> bool {
31        match (self, requested) {
32            (Scope::Collection(c), Scope::Collection(rc)) => c == rc,
33            (Scope::Collection(c), Scope::Document { collection, .. }) => c == collection,
34            (
35                Scope::Document { collection, doc_id },
36                Scope::Document {
37                    collection: rc,
38                    doc_id: rd,
39                },
40            ) => collection == rc && doc_id == rd,
41            (Scope::Function(f), Scope::Function(rf)) => f == rf,
42            _ => false,
43        }
44    }
45}
46
47/// The operations a capability grants — a bitset of read / write / compute.
48#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
49pub struct Ops(u8);
50
51impl Ops {
52    pub const NONE: Ops = Ops(0);
53    pub const READ: Ops = Ops(1);
54    pub const WRITE: Ops = Ops(2);
55    pub const COMPUTE: Ops = Ops(4);
56
57    /// Whether `self` grants every op in `needed` (and `needed` is non-empty).
58    pub fn contains(self, needed: Ops) -> bool {
59        needed.0 != 0 && (self.0 & needed.0) == needed.0
60    }
61
62    pub fn is_empty(self) -> bool {
63        self.0 == 0
64    }
65
66    /// Whether `self` is a subset of `other` — used to check a sub-grant doesn't
67    /// escalate ops beyond its parent (S2).
68    pub fn is_subset_of(self, other: Ops) -> bool {
69        (self.0 & other.0) == self.0
70    }
71}
72
73impl std::ops::BitOr for Ops {
74    type Output = Ops;
75    fn bitor(self, rhs: Ops) -> Ops {
76        Ops(self.0 | rhs.0)
77    }
78}
79
80/// A grant of access from an issuer to a bearer.
81#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
82pub struct Capability {
83    /// Unique grant id (the revocation key).
84    pub id: [u8; 16],
85    /// Who granted this (the owner / a delegating bearer).
86    pub issuer: Did,
87    /// Who may use it (a human or an AI agent).
88    pub bearer: Did,
89    /// What it applies to.
90    pub scope: Scope,
91    /// Which operations it allows.
92    pub ops: Ops,
93    /// Optional expiry (unix seconds); `None` = until revoked.
94    pub expiry: Option<u64>,
95    /// Optional spend cap in micro-`$MATA` (the metering hook for M8).
96    pub budget_micro_mata: Option<u64>,
97    /// How many more times this may be re-delegated (0 = not delegable).
98    pub delegation_depth: u8,
99}
100
101impl Capability {
102    /// Mint a fresh capability with a random id, no expiry/budget, non-delegable.
103    /// Refine with the builder methods.
104    pub fn grant(
105        issuer: impl Into<Did>,
106        bearer: impl Into<Did>,
107        scope: Scope,
108        ops: Ops,
109    ) -> AccessResult<Self> {
110        let mut id = [0u8; 16];
111        getrandom::fill(&mut id).map_err(|e| AccessError::KeyGen(e.to_string()))?;
112        Ok(Self {
113            id,
114            issuer: issuer.into(),
115            bearer: bearer.into(),
116            scope,
117            ops,
118            expiry: None,
119            budget_micro_mata: None,
120            delegation_depth: 0,
121        })
122    }
123
124    pub fn with_expiry(mut self, unix_seconds: u64) -> Self {
125        self.expiry = Some(unix_seconds);
126        self
127    }
128
129    pub fn with_budget(mut self, micro_mata: u64) -> Self {
130        self.budget_micro_mata = Some(micro_mata);
131        self
132    }
133
134    pub fn with_delegation_depth(mut self, depth: u8) -> Self {
135        self.delegation_depth = depth;
136        self
137    }
138
139    /// The canonical bytes that get signed (deterministic via postcard).
140    pub(crate) fn canonical_bytes(&self) -> AccessResult<Vec<u8>> {
141        postcard::to_allocvec(self).map_err(|e| AccessError::Canonical(e.to_string()))
142    }
143}
144
145/// A capability plus the issuer's signature over its canonical bytes.
146#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
147pub struct SignedCapability {
148    pub capability: Capability,
149    /// DER ECDSA signature by the issuer over `capability.canonical_bytes()`.
150    pub issuer_signature: Vec<u8>,
151}
152
153impl SignedCapability {
154    /// Sign `capability` with `issuer`. The capability's `issuer` field should be
155    /// `issuer.did()`; if it isn't, verification will fail (the directory key for
156    /// the claimed issuer won't match this signature).
157    pub fn sign(capability: Capability, issuer: &Identity) -> AccessResult<Self> {
158        let bytes = capability.canonical_bytes()?;
159        let issuer_signature = issuer.sign(&bytes);
160        Ok(Self {
161            capability,
162            issuer_signature,
163        })
164    }
165
166    /// Serialize for transmission/persistence (postcard).
167    pub fn encode(&self) -> AccessResult<Vec<u8>> {
168        postcard::to_allocvec(self).map_err(|e| AccessError::Canonical(e.to_string()))
169    }
170
171    /// Deserialize a signed capability (postcard).
172    pub fn decode(bytes: &[u8]) -> AccessResult<Self> {
173        postcard::from_bytes(bytes).map_err(|e| AccessError::Canonical(e.to_string()))
174    }
175}