Skip to main content

hessra_cap_engine/
engine.rs

1//! The capability engine: orchestrates policy evaluation, token minting, and verification.
2
3use hessra_cap_token::{CapabilityVerifier, DesignationBuilder, HessraCapability};
4use hessra_identity_token::{HessraIdentity, IdentityVerifier};
5use hessra_token_core::{KeyPair, PublicKey, TokenTimeConfig};
6
7use crate::context::{self, ContextToken, HessraContext};
8use crate::error::EngineError;
9use crate::types::{
10    CapabilityGrant, Designation, ExposureLabel, IdentityConfig, MintOptions, MintResult, ObjectId,
11    Operation, PolicyBackend, PolicyDecision, SessionConfig,
12};
13
14/// The Hessra Capability Engine.
15///
16/// Evaluates policy, orchestrates token minting/verification, and manages
17/// information flow control via context tokens.
18///
19/// The engine is generic over a `PolicyBackend` implementation, allowing
20/// different policy models (CList, RBAC, ABAC, etc.) to be plugged in.
21pub struct CapabilityEngine<P: PolicyBackend> {
22    policy: P,
23    keypair: KeyPair,
24}
25
26impl<P: PolicyBackend> CapabilityEngine<P> {
27    /// Create a new engine with a policy backend and signing keypair.
28    pub fn new(policy: P, keypair: KeyPair) -> Self {
29        Self { policy, keypair }
30    }
31
32    /// Create a new engine that generates its own keypair.
33    ///
34    /// Useful for local/development use where the engine manages its own keys.
35    pub fn with_generated_keys(policy: P) -> Self {
36        Self {
37            policy,
38            keypair: KeyPair::new(),
39        }
40    }
41
42    /// Get the engine's public key (for token verification).
43    pub fn public_key(&self) -> PublicKey {
44        self.keypair.public()
45    }
46
47    /// Get a reference to the policy backend.
48    pub fn policy(&self) -> &P {
49        &self.policy
50    }
51
52    // =========================================================================
53    // Policy evaluation
54    // =========================================================================
55
56    /// Evaluate whether a capability request would be granted, without minting.
57    ///
58    /// Checks both the capability space (does the subject hold this capability?)
59    /// and exposure restrictions (would context exposure block this?).
60    pub fn evaluate(
61        &self,
62        subject: &ObjectId,
63        target: &ObjectId,
64        operation: &Operation,
65        context: Option<&ContextToken>,
66    ) -> PolicyDecision {
67        let exposure_labels: Vec<ExposureLabel> = context
68            .map(|c| c.exposure_labels().to_vec())
69            .unwrap_or_default();
70
71        self.policy
72            .evaluate(subject, target, operation, &exposure_labels)
73    }
74
75    // =========================================================================
76    // Capability tokens
77    // =========================================================================
78
79    /// Mint a capability token for a subject to access a target with an operation.
80    ///
81    /// The engine:
82    /// 1. Evaluates the policy (capability space + exposure restrictions)
83    /// 2. If granted, mints a capability token via `hessra-cap-token`
84    /// 3. If the target has data classifications, auto-applies exposure to the context
85    ///
86    /// Returns a `MintResult` containing the token and optionally an updated context.
87    pub fn mint_capability(
88        &self,
89        subject: &ObjectId,
90        target: &ObjectId,
91        operation: &Operation,
92        context: Option<&ContextToken>,
93    ) -> Result<MintResult, EngineError> {
94        // Step 1: Evaluate policy
95        let decision = self.evaluate(subject, target, operation, context);
96        match &decision {
97            PolicyDecision::Granted => {}
98            PolicyDecision::Denied { reason } => {
99                return Err(EngineError::CapabilityDenied {
100                    subject: subject.clone(),
101                    target: target.clone(),
102                    operation: operation.clone(),
103                    reason: reason.clone(),
104                });
105            }
106            PolicyDecision::DeniedByExposure {
107                label,
108                blocked_target,
109            } => {
110                return Err(EngineError::ExposureRestriction {
111                    label: label.clone(),
112                    target: blocked_target.clone(),
113                });
114            }
115        }
116
117        // Step 2: Mint the capability token
118        let time_config = TokenTimeConfig::default();
119        let token = HessraCapability::new(
120            subject.as_str().to_string(),
121            target.as_str().to_string(),
122            operation.as_str().to_string(),
123            time_config,
124        )
125        .issue(&self.keypair)
126        .map_err(|e| EngineError::TokenOperation(format!("failed to mint capability: {e}")))?;
127
128        // Step 3: Auto-apply exposure if the target has data classifications
129        let updated_context = if let Some(ctx) = context {
130            let classifications = self.policy.classification(target);
131            if classifications.is_empty() {
132                Some(ctx.clone())
133            } else {
134                Some(context::add_exposure_block(
135                    ctx,
136                    &classifications,
137                    target,
138                    &self.keypair,
139                )?)
140            }
141        } else {
142            None
143        };
144
145        Ok(MintResult {
146            token,
147            context: updated_context,
148        })
149    }
150
151    /// Verify a capability token for a target and operation.
152    ///
153    /// This is capability-first verification: no subject is required.
154    /// The token IS the proof of authorization.
155    pub fn verify_capability(
156        &self,
157        token: &str,
158        target: &ObjectId,
159        operation: &Operation,
160    ) -> Result<(), EngineError> {
161        CapabilityVerifier::new(
162            token.to_string(),
163            self.keypair.public(),
164            target.as_str().to_string(),
165            operation.as_str().to_string(),
166        )
167        .verify()
168        .map_err(EngineError::Token)
169    }
170
171    /// Mint a capability token with additional restrictions.
172    ///
173    /// Like `mint_capability`, but supports namespace restriction and custom time config.
174    /// This is useful when the caller needs to propagate namespace restrictions or
175    /// control token lifetime.
176    pub fn mint_capability_with_options(
177        &self,
178        subject: &ObjectId,
179        target: &ObjectId,
180        operation: &Operation,
181        context: Option<&ContextToken>,
182        options: MintOptions,
183    ) -> Result<MintResult, EngineError> {
184        // Step 1: Evaluate policy
185        let decision = self.evaluate(subject, target, operation, context);
186        match &decision {
187            PolicyDecision::Granted => {}
188            PolicyDecision::Denied { reason } => {
189                return Err(EngineError::CapabilityDenied {
190                    subject: subject.clone(),
191                    target: target.clone(),
192                    operation: operation.clone(),
193                    reason: reason.clone(),
194                });
195            }
196            PolicyDecision::DeniedByExposure {
197                label,
198                blocked_target,
199            } => {
200                return Err(EngineError::ExposureRestriction {
201                    label: label.clone(),
202                    target: blocked_target.clone(),
203                });
204            }
205        }
206
207        // Step 2: Mint the token with options
208        let time_config = options.time_config.unwrap_or_default();
209        let mut builder = HessraCapability::new(
210            subject.as_str().to_string(),
211            target.as_str().to_string(),
212            operation.as_str().to_string(),
213            time_config,
214        );
215
216        if let Some(namespace) = options.namespace {
217            builder = builder.namespace_restricted(namespace);
218        }
219
220        let token = builder
221            .issue(&self.keypair)
222            .map_err(|e| EngineError::TokenOperation(format!("failed to mint capability: {e}")))?;
223
224        // Step 3: Auto-apply exposure if the target has data classifications
225        let updated_context = if let Some(ctx) = context {
226            let classifications = self.policy.classification(target);
227            if classifications.is_empty() {
228                Some(ctx.clone())
229            } else {
230                Some(context::add_exposure_block(
231                    ctx,
232                    &classifications,
233                    target,
234                    &self.keypair,
235                )?)
236            }
237        } else {
238            None
239        };
240
241        Ok(MintResult {
242            token,
243            context: updated_context,
244        })
245    }
246
247    // =========================================================================
248    // Direct token issuance (no policy evaluation)
249    // =========================================================================
250
251    /// Issue a capability token directly, without policy evaluation.
252    ///
253    /// Use this when the caller has already performed authorization checks
254    /// through its own mechanisms (e.g., enterprise RBAC, custom domain logic).
255    /// For the fully-managed path that includes policy evaluation, use
256    /// `mint_capability` or `mint_capability_with_options` instead.
257    pub fn issue_capability(
258        &self,
259        subject: &ObjectId,
260        target: &ObjectId,
261        operation: &Operation,
262        options: MintOptions,
263    ) -> Result<String, EngineError> {
264        let time_config = options.time_config.unwrap_or_default();
265        let mut builder = HessraCapability::new(
266            subject.as_str().to_string(),
267            target.as_str().to_string(),
268            operation.as_str().to_string(),
269            time_config,
270        );
271
272        if let Some(namespace) = options.namespace {
273            builder = builder.namespace_restricted(namespace);
274        }
275
276        builder
277            .issue(&self.keypair)
278            .map_err(|e| EngineError::TokenOperation(format!("failed to issue capability: {e}")))
279    }
280
281    // =========================================================================
282    // Designation attenuation
283    // =========================================================================
284
285    /// Attenuate a capability token with designations.
286    ///
287    /// Adds designation checks to narrow the token's scope to specific
288    /// object instances. The verifier must provide matching designation facts.
289    pub fn attenuate_with_designations(
290        &self,
291        token: &str,
292        designations: &[Designation],
293    ) -> Result<String, EngineError> {
294        let mut builder = DesignationBuilder::from_base64(token.to_string(), self.keypair.public())
295            .map_err(EngineError::Token)?;
296
297        for d in designations {
298            builder = builder.designate(d.label.clone(), d.value.clone());
299        }
300
301        builder.attenuate_base64().map_err(EngineError::Token)
302    }
303
304    /// Convenience: mint a capability and immediately attenuate with designations.
305    pub fn mint_designated_capability(
306        &self,
307        subject: &ObjectId,
308        target: &ObjectId,
309        operation: &Operation,
310        designations: &[Designation],
311        context: Option<&ContextToken>,
312    ) -> Result<MintResult, EngineError> {
313        let mut result = self.mint_capability(subject, target, operation, context)?;
314
315        if !designations.is_empty() {
316            result.token = self.attenuate_with_designations(&result.token, designations)?;
317        }
318
319        Ok(result)
320    }
321
322    /// Verify a capability token that includes designation checks.
323    pub fn verify_designated_capability(
324        &self,
325        token: &str,
326        target: &ObjectId,
327        operation: &Operation,
328        designations: &[Designation],
329    ) -> Result<(), EngineError> {
330        let mut verifier = CapabilityVerifier::new(
331            token.to_string(),
332            self.keypair.public(),
333            target.as_str().to_string(),
334            operation.as_str().to_string(),
335        );
336
337        for d in designations {
338            verifier = verifier.with_designation(d.label.clone(), d.value.clone());
339        }
340
341        verifier.verify().map_err(EngineError::Token)
342    }
343
344    // =========================================================================
345    // Identity tokens
346    // =========================================================================
347
348    /// Mint an identity token for a subject.
349    pub fn mint_identity(
350        &self,
351        subject: &ObjectId,
352        config: IdentityConfig,
353    ) -> Result<String, EngineError> {
354        let time_config = TokenTimeConfig {
355            start_time: None,
356            duration: config.ttl,
357        };
358
359        let mut builder = HessraIdentity::new(subject.as_str().to_string(), time_config)
360            .delegatable(config.delegatable);
361
362        if let Some(namespace) = config.namespace {
363            builder = builder.namespace_restricted(namespace);
364        }
365
366        builder
367            .issue(&self.keypair)
368            .map_err(|e| EngineError::Identity(format!("failed to mint identity: {e}")))
369    }
370
371    /// Verify an identity token and return the authenticated object ID.
372    ///
373    /// This verifies the token as a bearer token (no specific identity required).
374    pub fn authenticate(&self, token: &str) -> Result<ObjectId, EngineError> {
375        // Verify the token is valid
376        IdentityVerifier::new(token.to_string(), self.keypair.public())
377            .verify()
378            .map_err(|e| EngineError::Identity(format!("authentication failed: {e}")))?;
379
380        // Inspect the token to extract the subject
381        let inspect =
382            hessra_identity_token::inspect_identity_token(token.to_string(), self.keypair.public())
383                .map_err(|e| {
384                    EngineError::Identity(format!("failed to inspect identity token: {e}"))
385                })?;
386
387        Ok(ObjectId::new(inspect.identity))
388    }
389
390    /// Verify an identity token for a specific identity.
391    pub fn verify_identity(
392        &self,
393        token: &str,
394        expected_identity: &ObjectId,
395    ) -> Result<(), EngineError> {
396        IdentityVerifier::new(token.to_string(), self.keypair.public())
397            .with_identity(expected_identity.as_str().to_string())
398            .verify()
399            .map_err(|e| EngineError::Identity(format!("identity verification failed: {e}")))
400    }
401
402    // =========================================================================
403    // Context tokens
404    // =========================================================================
405
406    /// Mint a fresh context token for a subject (new session, no exposure).
407    pub fn mint_context(
408        &self,
409        subject: &ObjectId,
410        session_config: SessionConfig,
411    ) -> Result<ContextToken, EngineError> {
412        HessraContext::new(subject.clone(), session_config).issue(&self.keypair)
413    }
414
415    /// Add exposure to a context token from a specific data source.
416    ///
417    /// Looks up the data source's classification in the policy and adds
418    /// the corresponding exposure labels to the context token.
419    pub fn add_exposure(
420        &self,
421        context: &ContextToken,
422        data_source: &ObjectId,
423    ) -> Result<ContextToken, EngineError> {
424        let labels = self.policy.classification(data_source);
425        if labels.is_empty() {
426            return Ok(context.clone());
427        }
428        context::add_exposure_block(context, &labels, data_source, &self.keypair)
429    }
430
431    /// Add a specific exposure label directly to a context token.
432    pub fn add_exposure_label(
433        &self,
434        context: &ContextToken,
435        label: ExposureLabel,
436        source: &ObjectId,
437    ) -> Result<ContextToken, EngineError> {
438        context::add_exposure_block(context, &[label], source, &self.keypair)
439    }
440
441    /// Fork a context token for a sub-agent, inheriting the parent's exposure.
442    pub fn fork_context(
443        &self,
444        parent: &ContextToken,
445        child_subject: &ObjectId,
446        session_config: SessionConfig,
447    ) -> Result<ContextToken, EngineError> {
448        context::fork_context(parent, child_subject, session_config, &self.keypair)
449    }
450
451    /// Extract exposure labels from a context token by re-parsing the Biscuit.
452    pub fn extract_exposure(
453        &self,
454        context: &ContextToken,
455    ) -> Result<Vec<ExposureLabel>, EngineError> {
456        context::extract_exposure_labels(context.token(), self.keypair.public())
457    }
458
459    // =========================================================================
460    // Introspection
461    // =========================================================================
462
463    /// List all capability grants for a subject.
464    pub fn list_grants(&self, subject: &ObjectId) -> Vec<CapabilityGrant> {
465        self.policy.list_grants(subject)
466    }
467
468    /// Check if a subject can delegate capabilities.
469    pub fn can_delegate(&self, subject: &ObjectId) -> bool {
470        self.policy.can_delegate(subject)
471    }
472}