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}