Skip to main content

hessra_cap_engine/
types.rs

1//! Core types for the capability engine.
2//!
3//! Provides the unified object model where everything is an object with a
4//! capability space, and the `PolicyBackend` trait for pluggable policy evaluation.
5
6use hessra_token_core::TokenTimeConfig;
7use serde::{Deserialize, Serialize};
8
9/// Object identifier in the unified namespace.
10///
11/// Object IDs are strings with conventional prefixes for human readability:
12/// `service:api-gateway`, `agent:openclaw`, `data:user-ssn`, `tool:web-search`.
13/// The engine does not interpret the prefix -- all objects are treated uniformly.
14#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
15pub struct ObjectId(pub String);
16
17impl ObjectId {
18    pub fn new(id: impl Into<String>) -> Self {
19        Self(id.into())
20    }
21
22    pub fn as_str(&self) -> &str {
23        &self.0
24    }
25}
26
27impl std::fmt::Display for ObjectId {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        write!(f, "{}", self.0)
30    }
31}
32
33impl From<&str> for ObjectId {
34    fn from(s: &str) -> Self {
35        Self(s.to_string())
36    }
37}
38
39impl From<String> for ObjectId {
40    fn from(s: String) -> Self {
41        Self(s)
42    }
43}
44
45/// Operation on a target object.
46#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
47pub struct Operation(pub String);
48
49impl Operation {
50    pub fn new(op: impl Into<String>) -> Self {
51        Self(op.into())
52    }
53
54    pub fn as_str(&self) -> &str {
55        &self.0
56    }
57}
58
59impl std::fmt::Display for Operation {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        write!(f, "{}", self.0)
62    }
63}
64
65impl From<&str> for Operation {
66    fn from(s: &str) -> Self {
67        Self(s.to_string())
68    }
69}
70
71impl From<String> for Operation {
72    fn from(s: String) -> Self {
73        Self(s)
74    }
75}
76
77/// Exposure label for information flow control.
78///
79/// Exposure labels are hierarchical strings representing data sensitivity classifications:
80/// `PII:SSN`, `PHI:diagnosis`, `financial:account-number`.
81/// Wildcard matching (e.g., `PII:*`) is supported in policy rules.
82#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
83pub struct ExposureLabel(pub String);
84
85impl ExposureLabel {
86    pub fn new(label: impl Into<String>) -> Self {
87        Self(label.into())
88    }
89
90    pub fn as_str(&self) -> &str {
91        &self.0
92    }
93}
94
95impl std::fmt::Display for ExposureLabel {
96    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        write!(f, "{}", self.0)
98    }
99}
100
101impl From<&str> for ExposureLabel {
102    fn from(s: &str) -> Self {
103        Self(s.to_string())
104    }
105}
106
107impl From<String> for ExposureLabel {
108    fn from(s: String) -> Self {
109        Self(s)
110    }
111}
112
113/// How a capability declaration binds the principal that can verify the
114/// issued capability.
115///
116/// At verify time, the verifier proves "I am `<anchor>`" by supplying
117/// `Designation { label: "anchor", value: <its-own-principal-name> }`. The
118/// capability is honored only by the named principal. A receiving principal
119/// who is not the anchor can still attenuate and delegate the capability
120/// downward; the capability simply must eventually be presented back to the
121/// anchor for verification.
122#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
123pub enum AnchorBinding {
124    /// Anchor to the subject of this declaration. The engine resolves to a
125    /// concrete principal at mint time.
126    Subject,
127    /// Anchor to a specific named principal (trustee or multi-organization
128    /// pattern).
129    Principal(ObjectId),
130}
131
132/// A capability grant: permission for a subject to perform operations on a target.
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct CapabilityGrant {
135    /// The target object this capability grants access to.
136    pub target: ObjectId,
137    /// The operations allowed on the target.
138    pub operations: Vec<Operation>,
139    /// Anchor binding for issued capabilities under this declaration. `None`
140    /// means the capability is not anchored and can be verified by any
141    /// principal.
142    pub anchor: Option<AnchorBinding>,
143    /// Static designations attached at every mint of this declaration.
144    /// Author-time bindings declared in policy. Each label is validated
145    /// against the target's schema at engine construction.
146    #[serde(default)]
147    pub designations: Vec<Designation>,
148}
149
150/// Result of minting a capability token.
151///
152/// Contains the capability token and optionally an updated context token
153/// with exposure labels applied if the target was a classified data source.
154pub struct MintResult {
155    /// The minted capability token (base64-encoded).
156    pub token: String,
157    /// Updated context token with exposure labels, if a context was provided
158    /// and the target had data classifications.
159    pub context: Option<crate::ContextToken>,
160}
161
162/// Options for customizing capability minting beyond the basic case.
163///
164/// Used with `CapabilityEngine::mint_capability_with_options` and
165/// `CapabilityEngine::issue_capability` to override the policy's default
166/// anchor configuration or set a custom token lifetime.
167#[derive(Debug, Clone, Default)]
168pub struct MintOptions {
169    /// Override the policy's anchor configuration with an explicit principal.
170    /// When set, the engine attaches `designation("anchor", value)` to the
171    /// minted capability, regardless of what the policy declares. Used for
172    /// the `issue_capability` path (which skips policy) and for explicit
173    /// caller intent.
174    pub anchor: Option<ObjectId>,
175    /// Override the default time config. If `None`, uses default (5 minutes).
176    pub time_config: Option<TokenTimeConfig>,
177}
178
179/// A designation label-value pair for narrowing capability scope.
180#[derive(Debug, Clone, Serialize, Deserialize)]
181pub struct Designation {
182    pub label: String,
183    pub value: String,
184}
185
186/// Configuration for minting identity tokens.
187#[derive(Debug, Clone)]
188pub struct IdentityConfig {
189    /// Token time-to-live in seconds.
190    pub ttl: i64,
191    /// Whether the identity token can be delegated to sub-identities.
192    pub delegatable: bool,
193}
194
195impl Default for IdentityConfig {
196    fn default() -> Self {
197        Self {
198            ttl: 3600,
199            delegatable: false,
200        }
201    }
202}
203
204/// Configuration for context token sessions.
205#[derive(Debug, Clone)]
206pub struct SessionConfig {
207    /// Session time-to-live in seconds.
208    pub ttl: i64,
209}
210
211impl Default for SessionConfig {
212    fn default() -> Self {
213        Self { ttl: 3600 }
214    }
215}
216
217/// Result of a policy evaluation.
218#[derive(Debug, Clone)]
219pub enum PolicyDecision {
220    /// The capability request is granted. `anchor` carries the resolved anchor
221    /// principal, if the matched declaration is anchor-bound. The CList policy
222    /// resolves `AnchorBinding::Subject` to the requesting subject before
223    /// returning, so the engine sees a concrete principal id (or `None`).
224    /// `designations` carries author-time static designations declared in the
225    /// matched policy entry; the engine attaches these at mint time alongside
226    /// any caller-supplied designations and validates the union against the
227    /// target's schema.
228    Granted {
229        anchor: Option<ObjectId>,
230        designations: Vec<Designation>,
231    },
232    /// The capability request is denied by policy (object doesn't hold this capability).
233    Denied { reason: String },
234    /// The capability request is denied due to exposure restrictions.
235    DeniedByExposure {
236        label: ExposureLabel,
237        blocked_target: ObjectId,
238    },
239}
240
241impl PolicyDecision {
242    pub fn is_granted(&self) -> bool {
243        matches!(self, PolicyDecision::Granted { .. })
244    }
245}
246
247/// Pluggable policy backend trait.
248///
249/// Implementations evaluate capability requests against their policy model.
250/// The default implementation is the CList backend in `hessra-cap-policy`.
251pub trait PolicyBackend: Send + Sync {
252    /// Evaluate whether a subject can access a target with the given operation,
253    /// considering any exposure labels from the subject's context.
254    fn evaluate(
255        &self,
256        subject: &ObjectId,
257        target: &ObjectId,
258        operation: &Operation,
259        exposure_labels: &[ExposureLabel],
260    ) -> PolicyDecision;
261
262    /// Get the data classification (exposure labels) for a target.
263    ///
264    /// When the engine mints a capability for a classified target, these labels
265    /// are automatically added to the subject's context token.
266    fn classification(&self, target: &ObjectId) -> Vec<ExposureLabel>;
267
268    /// List all capability grants for a subject (for introspection and audit).
269    fn list_grants(&self, subject: &ObjectId) -> Vec<CapabilityGrant>;
270
271    /// Check if a subject can delegate capabilities to other objects.
272    fn can_delegate(&self, subject: &ObjectId) -> bool;
273
274    /// Enumerate every (subject, grant) pair the policy declares. Used by the
275    /// engine to cross-validate static designations against schemas at
276    /// construction time. The default implementation returns an empty vector,
277    /// which disables schema cross-validation; backends that store grants
278    /// statically (e.g., CList) should override this.
279    fn all_grants(&self) -> Vec<(ObjectId, CapabilityGrant)> {
280        Vec::new()
281    }
282
283    /// The immediate parent principal of `subject` in the principal graph,
284    /// if `subject` is a sub-identity. Returns `None` for root principals or
285    /// principals not declared in this backend.
286    ///
287    /// Used by the engine's chain check at mint time. The default returns
288    /// `None`, modeling a flat principal graph; backends that represent
289    /// parent-child relationships (e.g., CList via `ObjectConfig.parent`)
290    /// should override this.
291    fn parent(&self, _subject: &ObjectId) -> Option<ObjectId> {
292        None
293    }
294
295    /// Whether `subject` holds a grant for `(target, operation)`, ignoring
296    /// any current exposure context. Used by the engine's chain check to
297    /// verify ancestor authority without conflating exposure (which is the
298    /// requesting subject's own running state, not an inherited property).
299    ///
300    /// The default delegates to [`Self::lookup_grant`]. Backends may override
301    /// for efficiency.
302    fn has_grant(&self, subject: &ObjectId, target: &ObjectId, operation: &Operation) -> bool {
303        self.lookup_grant(subject, target, operation).is_some()
304    }
305
306    /// Look up the full grant `subject` holds for `(target, operation)`, if
307    /// any. The returned [`CapabilityGrant`] carries the grant's static
308    /// designations and anchor binding, which the engine uses for
309    /// designation-containment enforcement during the chain check.
310    ///
311    /// The default scans [`Self::list_grants`]. Backends with a direct
312    /// `(subject, target, op) -> grant` lookup may override for efficiency.
313    fn lookup_grant(
314        &self,
315        subject: &ObjectId,
316        target: &ObjectId,
317        operation: &Operation,
318    ) -> Option<CapabilityGrant> {
319        self.list_grants(subject)
320            .into_iter()
321            .find(|g| g.target == *target && g.operations.iter().any(|o| o == operation))
322    }
323}