Skip to main content

hessra_token_core/
error.rs

1use thiserror::Error;
2
3/// Detailed error type for hessra-token operations with specific failure information
4#[derive(Error, Debug, Clone)]
5pub enum TokenError {
6    // ===== Signature and Format Errors =====
7    /// Token signature verification failed
8    #[error("Invalid token signature: {details}")]
9    InvalidSignature { details: String },
10
11    /// The root public key was not recognized
12    #[error("Unknown public key - token was not signed by a recognized authority")]
13    UnknownPublicKey,
14
15    /// Invalid key format provided
16    #[error("Invalid key format: {reason}")]
17    InvalidKeyFormat { reason: String },
18
19    /// Token deserialization failed
20    #[error("Failed to deserialize token: {reason}")]
21    DeserializationError { reason: String },
22
23    /// Token serialization failed
24    #[error("Failed to serialize token: {reason}")]
25    SerializationError { reason: String },
26
27    /// Base64 decoding failed
28    #[error("Failed to decode base64 token: {reason}")]
29    Base64DecodingError { reason: String },
30
31    /// Token format version is not supported
32    #[error("Unsupported token version: expected {maximum}-{minimum}, got {actual}")]
33    UnsupportedVersion {
34        maximum: u32,
35        minimum: u32,
36        actual: u32,
37    },
38
39    // ===== Verification Errors (Common) =====
40    /// Token has expired
41    #[error("Token expired at {expired_at}, current time is {current_time}")]
42    Expired {
43        /// When the token expired (Unix timestamp)
44        expired_at: i64,
45        /// Current time when verification was attempted (Unix timestamp)
46        current_time: i64,
47        /// Block ID where the expiration check failed
48        block_id: u32,
49        /// Check ID within the block
50        check_id: u32,
51    },
52
53    /// Namespace restriction check failed
54    #[error("Namespace mismatch: expected '{expected}', {}", match provided {
55        Some(p) => format!("got '{p}'"),
56        None => "no namespace provided".to_string(),
57    })]
58    NamespaceMismatch {
59        /// Expected namespace from token
60        expected: String,
61        /// Namespace provided during verification (if any)
62        provided: Option<String>,
63        /// Block ID where the check failed
64        block_id: u32,
65        /// Check ID within the block
66        check_id: u32,
67    },
68
69    /// A generic check failed (couldn't parse semantic meaning)
70    #[error("Verification check failed in block {block_id}, check {check_id}: {rule}")]
71    CheckFailed {
72        /// Block ID where the check failed
73        block_id: u32,
74        /// Check ID within the block
75        check_id: u32,
76        /// The Datalog rule that failed
77        rule: String,
78    },
79
80    // ===== Identity Token Errors =====
81    /// Identity/actor mismatch
82    #[error("Identity mismatch: expected '{expected}', got '{actual}'")]
83    IdentityMismatch {
84        /// Expected identity
85        expected: String,
86        /// Actual identity provided
87        actual: String,
88    },
89
90    /// Identity hierarchy violation (delegation not allowed)
91    #[error(
92        "Identity hierarchy violation: actor '{actual}' is not authorized for identity '{expected}' (delegatable: {delegatable})"
93    )]
94    HierarchyViolation {
95        /// Base identity that issued the token
96        expected: String,
97        /// Actor attempting to use the token
98        actual: String,
99        /// Whether delegation was allowed
100        delegatable: bool,
101        /// Block ID where the check failed
102        block_id: u32,
103        /// Check ID within the block
104        check_id: u32,
105    },
106
107    /// Token attenuation failed
108    #[error("Token attenuation failed: {reason}")]
109    AttenuationFailed { reason: String },
110
111    /// Bearer token not allowed in this context
112    #[error("Bearer token not allowed: {reason}")]
113    BearerNotAllowed { reason: String },
114
115    // ===== Capability Token Errors =====
116    /// Capability rights denied
117    #[error("Authorization denied: {subject} does not have permission to perform '{operation}' on '{resource}'", subject = subject.as_deref().unwrap_or("<capability bearer>"))]
118    RightsDenied {
119        /// Subject requesting the action (None for pure capability tokens)
120        subject: Option<String>,
121        /// Resource being accessed
122        resource: String,
123        /// Operation being performed
124        operation: String,
125    },
126
127    /// No authorization policy matched and checks failed
128    #[error("No matching authorization policy found. Failed checks: {}", format_check_failures(.failed_checks))]
129    NoMatchingPolicy {
130        /// List of checks that failed
131        failed_checks: Vec<CheckFailure>,
132    },
133
134    /// Authorization policy matched but checks failed
135    #[error("Authorization policy {policy_type} matched (index {policy_index}), but the following checks failed: {}", format_check_failures(.failed_checks))]
136    PolicyMatchedButChecksFailed {
137        /// Type of policy that matched (Allow/Deny)
138        policy_type: String,
139        /// Index of the policy
140        policy_index: usize,
141        /// List of checks that failed
142        failed_checks: Vec<CheckFailure>,
143    },
144
145    // ===== Execution Errors =====
146    /// Datalog execution limit reached
147    #[error("Token verification exceeded execution limits: {reason}")]
148    ExecutionLimitReached { reason: String },
149
150    /// Datalog expression evaluation failed
151    #[error("Token verification expression error: {reason}")]
152    ExpressionError { reason: String },
153
154    /// Invalid block rule
155    #[error("Invalid rule in block {block_id}: {rule}")]
156    InvalidBlockRule { block_id: u32, rule: String },
157
158    // ===== Generic Errors =====
159    /// Internal error
160    #[error("Internal error: {0}")]
161    Internal(String),
162
163    /// Generic error with message
164    #[error("{0}")]
165    Generic(String),
166}
167
168/// Details about a failed check from biscuit verification
169#[derive(Debug, Clone)]
170pub struct CheckFailure {
171    /// Block ID (None if from authorizer)
172    pub block_id: Option<u32>,
173    /// Check ID
174    pub check_id: u32,
175    /// The Datalog rule that failed
176    pub rule: String,
177}
178
179impl std::fmt::Display for CheckFailure {
180    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
181        if let Some(block_id) = self.block_id {
182            write!(
183                f,
184                "Check #{} in block #{}: {}",
185                self.check_id, block_id, self.rule
186            )
187        } else {
188            write!(f, "Check #{} in authorizer: {}", self.check_id, self.rule)
189        }
190    }
191}
192
193fn format_check_failures(checks: &[CheckFailure]) -> String {
194    checks
195        .iter()
196        .map(|c| c.to_string())
197        .collect::<Vec<_>>()
198        .join("; ")
199}
200
201impl TokenError {
202    // ===== Helper Methods for Common Error Checks =====
203
204    /// Check if this error is due to token expiration
205    pub fn is_expired(&self) -> bool {
206        matches!(self, TokenError::Expired { .. })
207    }
208
209    /// Check if this error is due to namespace mismatch
210    pub fn is_namespace_mismatch(&self) -> bool {
211        matches!(self, TokenError::NamespaceMismatch { .. })
212    }
213
214    /// Check if this error is due to identity mismatch
215    pub fn is_identity_mismatch(&self) -> bool {
216        matches!(
217            self,
218            TokenError::IdentityMismatch { .. } | TokenError::HierarchyViolation { .. }
219        )
220    }
221
222    /// Check if this error is due to authorization rights denial
223    pub fn is_rights_denied(&self) -> bool {
224        matches!(self, TokenError::RightsDenied { .. })
225    }
226
227    /// Check if this error is a signature/format error
228    pub fn is_signature_error(&self) -> bool {
229        matches!(
230            self,
231            TokenError::InvalidSignature { .. }
232                | TokenError::UnknownPublicKey
233                | TokenError::DeserializationError { .. }
234                | TokenError::Base64DecodingError { .. }
235        )
236    }
237
238    /// Get the expiration time if this is an expiration error
239    pub fn get_expiration_time(&self) -> Option<i64> {
240        match self {
241            TokenError::Expired { expired_at, .. } => Some(*expired_at),
242            _ => None,
243        }
244    }
245
246    /// Get the expected namespace if this is a namespace mismatch error
247    pub fn get_expected_namespace(&self) -> Option<&str> {
248        match self {
249            TokenError::NamespaceMismatch { expected, .. } => Some(expected.as_str()),
250            _ => None,
251        }
252    }
253
254    /// Get the missing rights if this is a rights denied error
255    pub fn get_denied_access(&self) -> Option<(Option<&str>, &str, &str)> {
256        match self {
257            TokenError::RightsDenied {
258                subject,
259                resource,
260                operation,
261            } => Some((subject.as_deref(), resource.as_str(), operation.as_str())),
262            _ => None,
263        }
264    }
265
266    // ===== Constructor Helper Methods =====
267
268    /// Create a generic error
269    pub fn generic<S: Into<String>>(msg: S) -> Self {
270        TokenError::Generic(msg.into())
271    }
272
273    /// Create an internal error
274    pub fn internal<S: Into<String>>(msg: S) -> Self {
275        TokenError::Internal(msg.into())
276    }
277
278    /// Create an invalid key format error
279    pub fn invalid_key_format<S: Into<String>>(reason: S) -> Self {
280        TokenError::InvalidKeyFormat {
281            reason: reason.into(),
282        }
283    }
284}
285
286// ===== Conversions from biscuit-auth errors =====
287
288impl From<biscuit_auth::error::Token> for TokenError {
289    fn from(err: biscuit_auth::error::Token) -> Self {
290        use biscuit_auth::error::{Logic, MatchedPolicy, Token};
291
292        match err {
293            Token::Format(format_err) => {
294                use biscuit_auth::error::Format;
295                match format_err {
296                    Format::Signature(sig_err) => TokenError::InvalidSignature {
297                        details: sig_err.to_string(),
298                    },
299                    Format::UnknownPublicKey => TokenError::UnknownPublicKey,
300                    Format::DeserializationError(msg) | Format::BlockDeserializationError(msg) => {
301                        TokenError::DeserializationError { reason: msg }
302                    }
303                    Format::SerializationError(msg) | Format::BlockSerializationError(msg) => {
304                        TokenError::SerializationError { reason: msg }
305                    }
306                    Format::Version {
307                        maximum,
308                        minimum,
309                        actual,
310                    } => TokenError::UnsupportedVersion {
311                        maximum,
312                        minimum,
313                        actual,
314                    },
315                    Format::InvalidKey(msg) => TokenError::InvalidKeyFormat { reason: msg },
316                    other => TokenError::Generic(other.to_string()),
317                }
318            }
319            Token::Base64(base64_err) => TokenError::Base64DecodingError {
320                reason: base64_err.to_string(),
321            },
322            Token::FailedLogic(logic_err) => match logic_err {
323                Logic::Unauthorized { policy, checks } => {
324                    let failed_checks = convert_failed_checks(checks);
325                    let (policy_type, policy_index) = match policy {
326                        MatchedPolicy::Allow(idx) => ("Allow", idx),
327                        MatchedPolicy::Deny(idx) => ("Deny", idx),
328                    };
329                    TokenError::PolicyMatchedButChecksFailed {
330                        policy_type: policy_type.to_string(),
331                        policy_index,
332                        failed_checks,
333                    }
334                }
335                Logic::NoMatchingPolicy { checks } => {
336                    let failed_checks = convert_failed_checks(checks);
337                    TokenError::NoMatchingPolicy { failed_checks }
338                }
339                Logic::InvalidBlockRule(block_id, rule) => {
340                    TokenError::InvalidBlockRule { block_id, rule }
341                }
342                Logic::AuthorizerNotEmpty => {
343                    TokenError::Internal("Authorizer already contains a token".to_string())
344                }
345            },
346            Token::RunLimit(limit) => TokenError::ExecutionLimitReached {
347                reason: limit.to_string(),
348            },
349            Token::Execution(expr) => TokenError::ExpressionError {
350                reason: expr.to_string(),
351            },
352            Token::Language(lang_err) => TokenError::Generic(lang_err.to_string()),
353            other => TokenError::Generic(other.to_string()),
354        }
355    }
356}
357
358/// Convert biscuit FailedCheck to our CheckFailure type
359fn convert_failed_checks(checks: Vec<biscuit_auth::error::FailedCheck>) -> Vec<CheckFailure> {
360    checks
361        .into_iter()
362        .map(|check| match check {
363            biscuit_auth::error::FailedCheck::Block(block_check) => CheckFailure {
364                block_id: Some(block_check.block_id),
365                check_id: block_check.check_id,
366                rule: block_check.rule,
367            },
368            biscuit_auth::error::FailedCheck::Authorizer(auth_check) => CheckFailure {
369                block_id: None,
370                check_id: auth_check.check_id,
371                rule: auth_check.rule,
372            },
373        })
374        .collect()
375}
376
377impl From<hex::FromHexError> for TokenError {
378    fn from(err: hex::FromHexError) -> Self {
379        TokenError::InvalidKeyFormat {
380            reason: err.to_string(),
381        }
382    }
383}
384
385impl From<&str> for TokenError {
386    fn from(err: &str) -> Self {
387        TokenError::Generic(err.to_string())
388    }
389}
390
391impl From<String> for TokenError {
392    fn from(err: String) -> Self {
393        TokenError::Generic(err)
394    }
395}