1use std::error::Error as StdError;
18use std::fmt;
19
20use rocketmq_error::fields;
21use rocketmq_error::Error as CanonicalError;
22use rocketmq_error::ErrorContext;
23use rocketmq_error::ErrorDescriptor;
24use rocketmq_error::AUTH_CONFIGURATION_INVALID;
25use rocketmq_error::AUTH_OPERATION_FAILED;
26use rocketmq_error::CORE_ARGUMENT_INVALID;
27use rocketmq_error::CORE_SERIALIZATION_FAILED;
28use rocketmq_error::STORAGE_BACKEND_UNAVAILABLE;
29use rocketmq_error::STORAGE_READ_FAILED;
30use rocketmq_error::STORAGE_WRITE_FAILED;
31use rocketmq_security_api::SecurityProviderError;
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum AuthFailureKind {
38 InvalidInput,
39 InvalidConfiguration,
40 NotFound,
41 Conflict,
42 Expired,
43 Unauthenticated,
44 PermissionDenied,
45 Timeout,
46 Unavailable,
47 InvalidData,
48 Unsupported,
49 Internal,
50}
51
52impl AuthFailureKind {
53 const fn public_message(self) -> &'static str {
54 match self {
55 Self::InvalidInput => "Authentication service input is invalid",
56 Self::InvalidConfiguration => "Authentication service configuration is invalid",
57 Self::NotFound => "Authentication service resource was not found",
58 Self::Conflict => "Authentication service state conflicts with the request",
59 Self::Expired => "Authentication service request has expired",
60 Self::Unauthenticated => "Authentication could not be verified",
61 Self::PermissionDenied => "Authorization was denied",
62 Self::Timeout => "Authentication service operation timed out",
63 Self::Unavailable => "Authentication service is unavailable",
64 Self::InvalidData => "Authentication service data is invalid",
65 Self::Unsupported => "Authentication service operation is unsupported",
66 Self::Internal => "Authentication service operation failed",
67 }
68 }
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73pub enum AuthOperation {
74 Initialize,
75 InitializeProvider,
76 BuildContext,
77 Authenticate,
78 Authorize,
79 ReadMetadata,
80 WriteMetadata,
81 LockMetadata,
82 EncodeMetadata,
83 DecodeMetadata,
84 ManageMetadata,
85 Bootstrap,
86 RotateCredential,
87 MaintainService,
88 LoadSecret,
89 Audit,
90}
91
92impl AuthOperation {
93 #[must_use]
95 pub const fn as_str(self) -> &'static str {
96 match self {
97 Self::Initialize => "initialize",
98 Self::InitializeProvider => "initialize-provider",
99 Self::BuildContext => "build-context",
100 Self::Authenticate => "authenticate",
101 Self::Authorize => "authorize",
102 Self::ReadMetadata => "read-metadata",
103 Self::WriteMetadata => "write-metadata",
104 Self::LockMetadata => "lock-metadata",
105 Self::EncodeMetadata => "encode-metadata",
106 Self::DecodeMetadata => "decode-metadata",
107 Self::ManageMetadata => "manage-metadata",
108 Self::Bootstrap => "bootstrap",
109 Self::RotateCredential => "rotate-credential",
110 Self::MaintainService => "maintain-service",
111 Self::LoadSecret => "load-secret",
112 Self::Audit => "audit",
113 }
114 }
115}
116
117pub struct AuthServiceError {
122 operation: AuthOperation,
123 kind: AuthFailureKind,
124 source: Option<Box<dyn StdError + Send + Sync + 'static>>,
125}
126
127impl AuthServiceError {
128 #[must_use]
129 pub const fn new(operation: AuthOperation, kind: AuthFailureKind) -> Self {
130 Self {
131 operation,
132 kind,
133 source: None,
134 }
135 }
136
137 pub fn with_source<E>(operation: AuthOperation, kind: AuthFailureKind, source: E) -> Self
138 where
139 E: StdError + Send + Sync + 'static,
140 {
141 Self {
142 operation,
143 kind,
144 source: Some(Box::new(source)),
145 }
146 }
147
148 #[must_use]
153 pub fn provider(operation: AuthOperation, source: SecurityProviderError) -> Self {
154 let kind = match source.condition() {
155 rocketmq_error::CanonicalCondition::InvalidArgument
156 | rocketmq_error::CanonicalCondition::FailedPrecondition => AuthFailureKind::InvalidConfiguration,
157 rocketmq_error::CanonicalCondition::NotFound => AuthFailureKind::NotFound,
158 rocketmq_error::CanonicalCondition::AlreadyExists | rocketmq_error::CanonicalCondition::Aborted => {
159 AuthFailureKind::Conflict
160 }
161 rocketmq_error::CanonicalCondition::Unauthenticated => AuthFailureKind::Unauthenticated,
162 rocketmq_error::CanonicalCondition::PermissionDenied => AuthFailureKind::PermissionDenied,
163 rocketmq_error::CanonicalCondition::ResourceExhausted
164 | rocketmq_error::CanonicalCondition::Unavailable
165 | rocketmq_error::CanonicalCondition::Cancelled => AuthFailureKind::Unavailable,
166 rocketmq_error::CanonicalCondition::DeadlineExceeded => AuthFailureKind::Timeout,
167 rocketmq_error::CanonicalCondition::DataLoss => AuthFailureKind::InvalidData,
168 rocketmq_error::CanonicalCondition::Unimplemented => AuthFailureKind::Unsupported,
169 rocketmq_error::CanonicalCondition::Internal => AuthFailureKind::Internal,
170 };
171 Self::with_source(operation, kind, source)
172 }
173
174 #[must_use]
175 pub const fn operation(&self) -> AuthOperation {
176 self.operation
177 }
178
179 #[must_use]
180 pub const fn kind(&self) -> AuthFailureKind {
181 self.kind
182 }
183
184 #[must_use]
185 pub const fn source_present(&self) -> bool {
186 self.source.is_some()
187 }
188
189 pub(crate) fn invalid_context<T>(_diagnostic: T) -> Self {
190 Self::new(AuthOperation::BuildContext, AuthFailureKind::InvalidInput)
191 }
192
193 pub(crate) fn invalid_context_source<E>(source: E) -> Self
194 where
195 E: StdError + Send + Sync + 'static,
196 {
197 Self::with_source(AuthOperation::BuildContext, AuthFailureKind::InvalidInput, source)
198 }
199
200 pub(crate) fn configuration_error<T>(_diagnostic: T) -> Self {
201 Self::new(AuthOperation::Initialize, AuthFailureKind::InvalidConfiguration)
202 }
203
204 pub(crate) fn subject_not_found<T>(_diagnostic: T) -> Self {
205 Self::new(AuthOperation::ManageMetadata, AuthFailureKind::NotFound)
206 }
207
208 pub(crate) fn not_initialized<T>(_diagnostic: T) -> Self {
209 Self::new(AuthOperation::Initialize, AuthFailureKind::Unavailable)
210 }
211
212 pub(crate) fn storage_lock_failed<T>(_diagnostic: T) -> Self {
213 Self::new(AuthOperation::LockMetadata, AuthFailureKind::Unavailable)
214 }
215
216 pub(crate) fn metadata_io(source: rocketmq_runtime::RuntimeError) -> Self {
217 let kind = match source.condition() {
218 rocketmq_error::CanonicalCondition::DeadlineExceeded => AuthFailureKind::Timeout,
219 rocketmq_error::CanonicalCondition::Unavailable => AuthFailureKind::Unavailable,
220 _ => AuthFailureKind::Internal,
221 };
222 Self::with_source(AuthOperation::WriteMetadata, kind, source)
223 }
224
225 pub(crate) fn provider_failed<E>(operation: AuthOperation, source: E) -> Self
226 where
227 E: StdError + Send + Sync + 'static,
228 {
229 Self::with_source(operation, AuthFailureKind::Unavailable, source)
230 }
231
232 pub(crate) fn storage_read_failed<E>(source: E) -> Self
233 where
234 E: StdError + Send + Sync + 'static,
235 {
236 Self::with_source(AuthOperation::ReadMetadata, AuthFailureKind::Unavailable, source)
237 }
238
239 pub(crate) fn storage_write_failed<E>(source: E) -> Self
240 where
241 E: StdError + Send + Sync + 'static,
242 {
243 Self::with_source(AuthOperation::WriteMetadata, AuthFailureKind::Unavailable, source)
244 }
245
246 pub(crate) const fn storage_conflict() -> Self {
247 Self::new(AuthOperation::WriteMetadata, AuthFailureKind::Conflict)
248 }
249
250 pub(crate) const fn serialization_failed(operation: AuthOperation) -> Self {
251 Self::new(operation, AuthFailureKind::InvalidData)
252 }
253}
254
255impl fmt::Display for AuthServiceError {
256 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
257 formatter.write_str(self.kind.public_message())
258 }
259}
260
261impl fmt::Debug for AuthServiceError {
262 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
263 formatter
264 .debug_struct("AuthServiceError")
265 .field("operation", &self.operation)
266 .field("kind", &self.kind)
267 .field("source_present", &self.source_present())
268 .finish()
269 }
270}
271
272impl StdError for AuthServiceError {
273 fn source(&self) -> Option<&(dyn StdError + 'static)> {
274 self.source.as_deref().map(|source| source as &(dyn StdError + 'static))
275 }
276}
277
278impl From<AuthServiceError> for CanonicalError {
279 fn from(error: AuthServiceError) -> Self {
280 let operation = error.operation();
281 let kind = error.kind();
282 match (operation, kind) {
283 (_, AuthFailureKind::PermissionDenied) => project_canonical(
284 error,
285 &rocketmq_error::AUTH_PERMISSION_DENIED,
286 ErrorContext::new().with_text(fields::OPERATION_DIAGNOSTIC, operation.as_str()),
287 ),
288 (AuthOperation::Authenticate, AuthFailureKind::NotFound) => project_canonical(
289 error,
290 &rocketmq_error::AUTH_CREDENTIALS_INVALID,
291 ErrorContext::new().with_secret_presence(fields::CREDENTIALS_PRESENT),
292 ),
293 (AuthOperation::ManageMetadata, AuthFailureKind::NotFound) => project_canonical(
294 error,
295 &rocketmq_error::AUTH_USER_NOT_FOUND,
296 ErrorContext::new().with_text(fields::OPERATION_DIAGNOSTIC, operation.as_str()),
297 ),
298 (_, AuthFailureKind::InvalidInput | AuthFailureKind::NotFound) => project_canonical(
299 error,
300 &CORE_ARGUMENT_INVALID,
301 ErrorContext::new().with_secret_presence(fields::MESSAGE_PRESENT),
302 ),
303 (_, AuthFailureKind::InvalidConfiguration | AuthFailureKind::Unsupported) => project_canonical(
304 error,
305 &AUTH_CONFIGURATION_INVALID,
306 ErrorContext::new()
307 .with_text(fields::KEY, "auth.authorization")
308 .with_secret_presence(fields::REASON_PRESENT),
309 ),
310 (AuthOperation::Initialize, _) => project_canonical(
311 error,
312 &AUTH_CONFIGURATION_INVALID,
313 ErrorContext::new()
314 .with_text(fields::KEY, "auth.authorization")
315 .with_secret_presence(fields::REASON_PRESENT),
316 ),
317 (AuthOperation::ReadMetadata, _) => project_storage(error, &STORAGE_READ_FAILED, "read"),
318 (AuthOperation::WriteMetadata, _) => project_storage(error, &STORAGE_WRITE_FAILED, "write"),
319 (AuthOperation::LockMetadata, _) => project_storage(error, &STORAGE_BACKEND_UNAVAILABLE, "lock"),
320 (AuthOperation::EncodeMetadata, _) => project_serialization(error, "encode"),
321 (AuthOperation::DecodeMetadata, _) => project_serialization(error, "decode"),
322 (_, AuthFailureKind::Unauthenticated | AuthFailureKind::Expired) => project_canonical(
323 error,
324 &rocketmq_error::AUTH_CREDENTIALS_INVALID,
325 ErrorContext::new().with_secret_presence(fields::CREDENTIALS_PRESENT),
326 ),
327 _ => project_canonical(
328 error,
329 &AUTH_OPERATION_FAILED,
330 ErrorContext::new().with_text(fields::OPERATION_DIAGNOSTIC, operation.as_str()),
331 ),
332 }
333 }
334}
335
336fn project_storage(
337 error: AuthServiceError,
338 descriptor: &'static ErrorDescriptor,
339 operation: &'static str,
340) -> CanonicalError {
341 project_canonical(
342 error,
343 descriptor,
344 ErrorContext::new()
345 .with_text(fields::STORE_OPERATION, operation)
346 .with_text(fields::STORE_COMPONENT, "auth-metadata"),
347 )
348}
349
350fn project_serialization(error: AuthServiceError, operation: &'static str) -> CanonicalError {
351 project_canonical(
352 error,
353 &CORE_SERIALIZATION_FAILED,
354 ErrorContext::new()
355 .with_text(fields::OPERATION_DIAGNOSTIC, operation)
356 .with_text(fields::FORMAT, "auth-metadata"),
357 )
358}
359
360fn project_canonical(
361 error: AuthServiceError,
362 descriptor: &'static ErrorDescriptor,
363 context: ErrorContext,
364) -> CanonicalError {
365 let has_source = error.source_present();
366 let context = if has_source
367 && descriptor
368 .fields()
369 .iter()
370 .any(|schema| *schema == fields::SOURCE_PRESENT.schema())
371 {
372 context.with_secret_presence(fields::SOURCE_PRESENT)
373 } else {
374 context
375 };
376 let canonical = if has_source {
377 CanonicalError::caused_by(descriptor, error)
378 } else {
379 CanonicalError::new(descriptor)
380 };
381 canonical.with_context(context)
382}
383
384pub type AuthServiceResult<T> = Result<T, AuthServiceError>;
386
387#[cfg(test)]
388mod tests {
389 use std::io;
390 use std::mem::size_of;
391
392 use super::*;
393
394 #[test]
395 fn public_formatting_never_exposes_source_text() {
396 let error = AuthServiceError::with_source(
397 AuthOperation::ReadMetadata,
398 AuthFailureKind::Unavailable,
399 io::Error::other("secret\r\n/injected/private/path"),
400 );
401
402 assert_eq!(error.to_string(), "Authentication service is unavailable");
403 let debug = format!("{error:?}");
404 assert!(!debug.contains("secret"));
405 assert!(!debug.contains("private/path"));
406 assert!(debug.contains("source_present: true"));
407 }
408
409 #[test]
410 fn typed_source_is_retained_for_trusted_diagnostics() {
411 let error = AuthServiceError::with_source(
412 AuthOperation::ReadMetadata,
413 AuthFailureKind::Unavailable,
414 io::Error::other("disk unavailable"),
415 );
416
417 assert!(error
418 .source()
419 .and_then(|source| source.downcast_ref::<io::Error>())
420 .is_some());
421 }
422
423 #[test]
424 fn service_error_remains_small_enough_for_result_boundaries() {
425 assert!(size_of::<AuthServiceError>() <= 4 * size_of::<usize>());
426 }
427
428 #[test]
429 fn rocketmq_projection_uses_fixed_messages_and_retains_operational_sources() {
430 let invalid: CanonicalError = AuthServiceError::with_source(
431 AuthOperation::BuildContext,
432 AuthFailureKind::InvalidInput,
433 io::Error::other("secret\r\npath"),
434 )
435 .into();
436 assert_eq!(invalid.descriptor(), &CORE_ARGUMENT_INVALID);
437 assert!(!invalid.to_string().contains("secret"));
438
439 let operational: CanonicalError = AuthServiceError::with_source(
440 AuthOperation::ReadMetadata,
441 AuthFailureKind::Unavailable,
442 io::Error::other("diagnostic cause"),
443 )
444 .into();
445 assert_eq!(operational.descriptor(), &STORAGE_READ_FAILED);
446 let auth = operational.source().expect("auth facade source must be retained");
447 assert!(auth.downcast_ref::<AuthServiceError>().is_some());
448 let io = auth.source().expect("I/O source must be retained");
449 assert!(io.downcast_ref::<io::Error>().is_some());
450 }
451
452 #[test]
453 fn rocketmq_projection_preserves_frozen_error_categories() {
454 let cases = [
455 (
456 AuthServiceError::new(AuthOperation::ManageMetadata, AuthFailureKind::NotFound),
457 &rocketmq_error::AUTH_USER_NOT_FOUND,
458 ),
459 (
460 AuthServiceError::new(AuthOperation::Initialize, AuthFailureKind::InvalidConfiguration),
461 &AUTH_CONFIGURATION_INVALID,
462 ),
463 (
464 AuthServiceError::new(AuthOperation::ReadMetadata, AuthFailureKind::Unavailable),
465 &STORAGE_READ_FAILED,
466 ),
467 (
468 AuthServiceError::new(AuthOperation::WriteMetadata, AuthFailureKind::Conflict),
469 &STORAGE_WRITE_FAILED,
470 ),
471 (
472 AuthServiceError::new(AuthOperation::LockMetadata, AuthFailureKind::Unavailable),
473 &STORAGE_BACKEND_UNAVAILABLE,
474 ),
475 (
476 AuthServiceError::new(AuthOperation::EncodeMetadata, AuthFailureKind::InvalidData),
477 &CORE_SERIALIZATION_FAILED,
478 ),
479 (
480 AuthServiceError::new(AuthOperation::Authorize, AuthFailureKind::Internal),
481 &AUTH_OPERATION_FAILED,
482 ),
483 (
484 AuthServiceError::with_source(
485 AuthOperation::Authorize,
486 AuthFailureKind::Unavailable,
487 io::Error::other("provider unavailable"),
488 ),
489 &AUTH_OPERATION_FAILED,
490 ),
491 ];
492
493 for (error, descriptor) in cases {
494 assert_eq!(CanonicalError::from(error).descriptor(), descriptor);
495 }
496 }
497
498 #[test]
499 fn provider_mapping_uses_canonical_conditions_and_preserves_source() {
500 let cases = [
501 (
502 rocketmq_security_api::SecurityProviderFailure::NotFound,
503 AuthFailureKind::NotFound,
504 ),
505 (
506 rocketmq_security_api::SecurityProviderFailure::Conflict,
507 AuthFailureKind::Conflict,
508 ),
509 (
510 rocketmq_security_api::SecurityProviderFailure::Unsupported,
511 AuthFailureKind::Unsupported,
512 ),
513 (
514 rocketmq_security_api::SecurityProviderFailure::InvalidData,
515 AuthFailureKind::InvalidData,
516 ),
517 (
518 rocketmq_security_api::SecurityProviderFailure::ContractViolation,
519 AuthFailureKind::InvalidConfiguration,
520 ),
521 (
522 rocketmq_security_api::SecurityProviderFailure::Unavailable,
523 AuthFailureKind::Unavailable,
524 ),
525 (
526 rocketmq_security_api::SecurityProviderFailure::OperationFailed,
527 AuthFailureKind::Internal,
528 ),
529 ];
530
531 for (provider_kind, auth_kind) in cases {
532 let provider =
533 SecurityProviderError::new(provider_kind, rocketmq_security_api::SecurityOperation::ReadSecret);
534 let error = AuthServiceError::provider(AuthOperation::LoadSecret, provider);
535 assert_eq!(error.kind(), auth_kind);
536 assert!(error
537 .source()
538 .and_then(|source| source.downcast_ref::<SecurityProviderError>())
539 .is_some());
540 }
541 }
542}