Development Documentation (main branch) - For stable release docs, see docs.rs/eidetica
Skip to main content

eidetica/auth/
errors.rs

1//! Authentication error types for the Eidetica library.
2//!
3//! This module defines structured error types for authentication-related operations,
4//! providing better error context and type safety compared to string-based errors.
5
6use thiserror::Error as ThisError;
7
8use crate::Error;
9use crate::entry::ID;
10
11/// Errors that can occur during authentication operations.
12///
13/// # Stability
14///
15/// - New variants may be added in minor versions (enum is `#[non_exhaustive]`)
16/// - Existing variants will not be removed in minor versions
17/// - Field additions/changes require a major version bump
18/// - Helper methods like `is_*()` provide stable APIs
19#[non_exhaustive]
20#[derive(Debug, ThisError)]
21pub enum AuthError {
22    /// A requested authentication key was not found in the configuration.
23    #[error("Key not found: {key_name}")]
24    KeyNotFound {
25        /// The name of the key that was not found
26        key_name: String,
27    },
28
29    /// Invalid key format or parsing error.
30    #[error("Invalid key format: {reason}")]
31    InvalidKeyFormat {
32        /// Description of why the key format is invalid
33        reason: String,
34    },
35
36    /// Key parsing failed due to cryptographic library error.
37    #[error("Key parsing failed: {reason}")]
38    KeyParsingFailed {
39        /// Description of the parsing failure
40        reason: String,
41    },
42
43    /// No authentication configuration was found.
44    #[error("No auth configuration found")]
45    NoAuthConfiguration,
46
47    /// The authentication configuration is invalid.
48    #[error("Invalid auth configuration: {reason}")]
49    InvalidAuthConfiguration {
50        /// Description of why the configuration is invalid
51        reason: String,
52    },
53
54    /// Delegation path is empty when it should contain at least one step.
55    #[error("Empty delegation path")]
56    EmptyDelegationPath,
57
58    /// A delegation step is invalid.
59    #[error("Invalid delegation step: {reason}")]
60    InvalidDelegationStep {
61        /// Description of why the delegation step is invalid
62        reason: String,
63    },
64
65    /// Failed to load a delegated tree.
66    #[error("Failed to load delegated tree {tree_id}")]
67    DelegatedTreeLoadFailed {
68        /// The ID of the tree that failed to load
69        tree_id: ID,
70        /// The underlying error
71        #[source]
72        source: Box<Error>,
73    },
74
75    /// Delegation tips don't match the actual tree state.
76    #[error(
77        "Invalid delegation tips for tree {tree_id}: claimed tips {claimed_tips:?} don't match"
78    )]
79    InvalidDelegationTips {
80        /// The ID of the tree with invalid tips
81        tree_id: ID,
82        /// The tips that were claimed but are invalid
83        claimed_tips: Vec<ID>,
84    },
85
86    /// A delegated tree reference was not found in the configuration.
87    #[error("Delegation not found for tree: {tree_id}")]
88    DelegationNotFound {
89        /// The root tree ID of the delegation that was not found
90        tree_id: ID,
91    },
92
93    /// Delegation path exceeds the maximum allowed number of steps.
94    ///
95    /// The delegation path is wire-supplied on the entry's signature key, and
96    /// each step drives backend work before the authorization gate decides.
97    /// Bounding the path length caps that amplification (a flat path of N steps
98    /// is the chain-depth analog of N levels of recursion).
99    #[error("Delegation path too long: {len} steps (max {max})")]
100    DelegationPathTooLong {
101        /// Number of steps supplied
102        len: usize,
103        /// Maximum allowed
104        max: usize,
105    },
106
107    /// A delegation step claims more tips than allowed.
108    ///
109    /// Claimed tips are wire-supplied and each drives DAG traversal; the per-step
110    /// count is bounded to cap that amplification.
111    #[error("Delegation step for tree {tree_id} claims too many tips: {len} (max {max})")]
112    DelegationTipsTooMany {
113        /// The delegated tree root ID
114        tree_id: ID,
115        /// Number of tips claimed
116        len: usize,
117        /// Maximum allowed
118        max: usize,
119    },
120
121    /// A delegated tree referenced by a delegation is not synced locally enough
122    /// to decide the monotonicity floor.
123    ///
124    /// This is a *transient* condition, not a validation failure: the entries in
125    /// `missing` have not arrived yet. The caller should keep the entry
126    /// unverified and re-check after syncing `missing` from the delegated tree's
127    /// peers, rather than rejecting it as a forgery. Deliberately excluded from
128    /// [`AuthError::is_delegation_error`] — it signals sync state, not a
129    /// delegation defect.
130    #[error(
131        "Delegated tree {tree_id} not synced enough to validate delegation: {} entry(ies) missing",
132        missing.len()
133    )]
134    DelegatedTreeUnsynced {
135        /// The delegated tree root ID
136        tree_id: ID,
137        /// Entries that must be synced before validation can proceed.
138        missing: Vec<ID>,
139    },
140
141    /// Attempted to revoke an entry that is not a key.
142    #[error("Cannot revoke non-key entry: {key_name}")]
143    CannotRevokeNonKey {
144        /// The name of the entry that is not a key
145        key_name: String,
146    },
147
148    /// Entry has malformed signature info (e.g., hint without signature).
149    #[error("Malformed entry: {reason}")]
150    MalformedEntry {
151        /// Description of why the entry is malformed
152        reason: &'static str,
153    },
154
155    /// Signature verification failed.
156    #[error("Invalid signature")]
157    InvalidSignature,
158
159    /// Signature verification failed with specific error.
160    #[error("Signature verification failed: {reason}")]
161    SignatureVerificationFailed {
162        /// Description of the verification failure
163        reason: String,
164    },
165
166    /// Database is required for the operation but not available.
167    #[error("Database required for {operation}")]
168    DatabaseRequired {
169        /// The operation that requires a database
170        operation: String,
171    },
172
173    /// Invalid permission string format.
174    #[error("Invalid permission string: {value}")]
175    InvalidPermissionString {
176        /// The invalid permission string
177        value: String,
178    },
179
180    /// Permission type requires a priority value.
181    #[error("{permission_type} permission requires priority")]
182    PermissionRequiresPriority {
183        /// The permission type that requires priority
184        permission_type: String,
185    },
186
187    /// Invalid priority value.
188    #[error("Invalid priority value: {value}")]
189    InvalidPriorityValue {
190        /// The invalid priority value
191        value: String,
192    },
193
194    /// Invalid key status string.
195    #[error("Invalid key status: {value}")]
196    InvalidKeyStatus {
197        /// The invalid status value
198        value: String,
199    },
200
201    /// Permission denied for an operation.
202    #[error("Permission denied: {reason}")]
203    PermissionDenied {
204        /// Description of why permission was denied
205        reason: String,
206    },
207
208    /// Attempted to add a key that already exists.
209    #[error("Key already exists: {key_name}")]
210    KeyAlreadyExists {
211        /// The name of the key that already exists
212        key_name: String,
213    },
214
215    /// Key name conflicts with existing key that has different public key.
216    #[error(
217        "Key name '{key_name}' conflicts: existing key has pubkey '{existing_pubkey}', new key has pubkey '{new_pubkey}'"
218    )]
219    KeyNameConflict {
220        /// The name of the conflicting key
221        key_name: String,
222        /// The public key of the existing key
223        existing_pubkey: String,
224        /// The public key of the new key
225        new_pubkey: String,
226    },
227
228    /// Signing key does not match the claimed identity in a DatabaseKey.
229    #[error("Signing key mismatch: {reason}")]
230    SigningKeyMismatch {
231        /// Description of the mismatch
232        reason: String,
233    },
234}
235
236impl AuthError {
237    /// Check if this error indicates a key or delegation was not found.
238    pub fn is_not_found(&self) -> bool {
239        matches!(
240            self,
241            AuthError::KeyNotFound { .. } | AuthError::DelegationNotFound { .. }
242        )
243    }
244
245    /// Check if this error indicates invalid signature.
246    pub fn is_invalid_signature(&self) -> bool {
247        matches!(
248            self,
249            AuthError::InvalidSignature | AuthError::SignatureVerificationFailed { .. }
250        )
251    }
252
253    /// Check if this error indicates permission was denied.
254    pub fn is_permission_denied(&self) -> bool {
255        matches!(self, AuthError::PermissionDenied { .. })
256    }
257
258    /// Check if this error indicates a key already exists.
259    pub fn is_key_already_exists(&self) -> bool {
260        matches!(self, AuthError::KeyAlreadyExists { .. })
261    }
262
263    /// Check if this error indicates a key name conflict.
264    pub fn is_key_name_conflict(&self) -> bool {
265        matches!(self, AuthError::KeyNameConflict { .. })
266    }
267
268    /// Check if this error indicates a configuration problem.
269    pub fn is_configuration_error(&self) -> bool {
270        matches!(
271            self,
272            AuthError::NoAuthConfiguration
273                | AuthError::InvalidAuthConfiguration { .. }
274                | AuthError::InvalidKeyFormat { .. }
275                | AuthError::KeyParsingFailed { .. }
276        )
277    }
278
279    /// Check if this error is related to delegation.
280    pub fn is_delegation_error(&self) -> bool {
281        matches!(
282            self,
283            AuthError::EmptyDelegationPath
284                | AuthError::InvalidDelegationStep { .. }
285                | AuthError::DelegatedTreeLoadFailed { .. }
286                | AuthError::InvalidDelegationTips { .. }
287                | AuthError::DelegationNotFound { .. }
288                | AuthError::DelegationPathTooLong { .. }
289                | AuthError::DelegationTipsTooMany { .. }
290        )
291    }
292
293    /// Check if this error indicates a delegated tree is not yet synced enough
294    /// to validate — a transient, retriable condition, not a delegation defect
295    /// (see [`AuthError::DelegatedTreeUnsynced`]).
296    pub fn is_delegated_tree_unsynced(&self) -> bool {
297        matches!(self, AuthError::DelegatedTreeUnsynced { .. })
298    }
299
300    /// Get the key name if this error is about a missing key.
301    pub fn key_name(&self) -> Option<&str> {
302        match self {
303            AuthError::KeyNotFound { key_name: id } => Some(id),
304            _ => None,
305        }
306    }
307}
308
309// Conversion from AuthError to the main Error type
310impl From<AuthError> for Error {
311    fn from(err: AuthError) -> Self {
312        // Use the new structured Auth variant
313        Error::Auth(Box::new(err))
314    }
315}
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320
321    #[test]
322    fn test_error_helpers() {
323        let err = AuthError::KeyNotFound {
324            key_name: "test-key".to_string(),
325        };
326        assert!(err.is_not_found());
327        assert_eq!(err.key_name(), Some("test-key"));
328
329        let err = AuthError::InvalidSignature;
330        assert!(err.is_invalid_signature());
331
332        let err = AuthError::PermissionDenied {
333            reason: "test".to_string(),
334        };
335        assert!(err.is_permission_denied());
336
337        let err = AuthError::NoAuthConfiguration;
338        assert!(err.is_configuration_error());
339
340        let err = AuthError::EmptyDelegationPath;
341        assert!(err.is_delegation_error());
342    }
343
344    #[test]
345    fn test_error_conversion() {
346        let auth_err = AuthError::KeyNotFound {
347            key_name: "test".to_string(),
348        };
349        let err: Error = auth_err.into();
350        match err {
351            Error::Auth(e) => match *e {
352                AuthError::KeyNotFound { key_name: id } => assert_eq!(id, "test"),
353                _ => panic!("Unexpected error variant"),
354            },
355            _ => panic!("Unexpected error variant"),
356        }
357    }
358}