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}