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