hessra_cap_token/attenuate.rs
1extern crate biscuit_auth as biscuit;
2
3use biscuit::macros::block;
4use hessra_token_core::{Biscuit, PublicKey, TokenError, utils};
5
6/// Builder for adding designation blocks to capability tokens.
7///
8/// Designations are standard Biscuit attenuation blocks that narrow the scope
9/// of a capability token by specifying which specific object/resource instance
10/// the token applies to. Unlike prefix restrictions (which required third-party
11/// blocks), designations use regular append-only blocks and do not require a
12/// signing key.
13///
14/// # Example
15/// ```rust,no_run
16/// use hessra_cap_token::DesignationBuilder;
17/// use hessra_token_core::PublicKey;
18///
19/// # fn example(token: String, public_key: PublicKey) -> Result<(), Box<dyn std::error::Error>> {
20/// let attenuated = DesignationBuilder::from_base64(token, public_key)?
21/// .designate("tenant_id".to_string(), "t-123".to_string())
22/// .designate("user_id".to_string(), "u-456".to_string())
23/// .attenuate_base64()?;
24/// # Ok(())
25/// # }
26/// ```
27pub struct DesignationBuilder {
28 token: Vec<u8>,
29 public_key: PublicKey,
30 designations: Vec<(String, String)>,
31}
32
33impl DesignationBuilder {
34 /// Create a new DesignationBuilder from raw token bytes.
35 ///
36 /// # Arguments
37 /// * `token` - The binary token data
38 /// * `public_key` - The public key to verify the token
39 pub fn new(token: Vec<u8>, public_key: PublicKey) -> Self {
40 Self {
41 token,
42 public_key,
43 designations: Vec::new(),
44 }
45 }
46
47 /// Create a new DesignationBuilder from a base64-encoded token string.
48 ///
49 /// # Arguments
50 /// * `token` - The base64-encoded token string
51 /// * `public_key` - The public key to verify the token
52 pub fn from_base64(token: String, public_key: PublicKey) -> Result<Self, TokenError> {
53 let token_bytes = utils::decode_token(&token)?;
54 Ok(Self::new(token_bytes, public_key))
55 }
56
57 /// Add a designation (label, value) pair to narrow the token's scope.
58 ///
59 /// Each designation adds a `check if designation(label, value)` to the token,
60 /// requiring the verifier to provide matching `designation(label, value)` facts.
61 ///
62 /// # Arguments
63 /// * `label` - The designation dimension (e.g., "tenant_id", "user_id", "region")
64 /// * `value` - The specific value for this dimension (e.g., "t-123", "u-456", "us-east-1")
65 pub fn designate(mut self, label: String, value: String) -> Self {
66 self.designations.push((label, value));
67 self
68 }
69
70 /// Attenuate the token with all accumulated designations.
71 ///
72 /// Returns the attenuated token as binary bytes.
73 pub fn attenuate(self) -> Result<Vec<u8>, TokenError> {
74 let biscuit = Biscuit::from(&self.token, self.public_key)?;
75
76 let mut block_builder = block!(r#""#);
77
78 for (label, value) in &self.designations {
79 let label = label.clone();
80 let value = value.clone();
81 block_builder = block_builder
82 .check(biscuit::macros::check!(
83 r#"check if designation({label}, {value});"#
84 ))
85 .map_err(|e| TokenError::AttenuationFailed {
86 reason: format!("Failed to add designation check: {e}"),
87 })?;
88 }
89
90 let attenuated =
91 biscuit
92 .append(block_builder)
93 .map_err(|e| TokenError::AttenuationFailed {
94 reason: format!("Failed to append designation block: {e}"),
95 })?;
96
97 attenuated
98 .to_vec()
99 .map_err(|e| TokenError::AttenuationFailed {
100 reason: format!("Failed to serialize attenuated token: {e}"),
101 })
102 }
103
104 /// Attenuate the token and return as a base64-encoded string.
105 pub fn attenuate_base64(self) -> Result<String, TokenError> {
106 let bytes = self.attenuate()?;
107 Ok(utils::encode_token(&bytes))
108 }
109}