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}