From 4e7288731a32b32a795318dc5c48ce778d7bc569 Mon Sep 17 00:00:00 2001 From: Anders Olsson Date: Tue, 2 Jun 2026 14:48:40 +0200 Subject: [PATCH] harden(auth): distinguish session-store failure (500) from absent session (401); exhaustive marker + verify_dummy tests --- crates/auth/src/lib.rs | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/crates/auth/src/lib.rs b/crates/auth/src/lib.rs index ce31dc0..0a0466a 100644 --- a/crates/auth/src/lib.rs +++ b/crates/auth/src/lib.rs @@ -73,6 +73,10 @@ pub enum AuthError { Unauthenticated, #[error("insufficient permissions")] Forbidden, + /// The session store itself failed (e.g. the database is unreachable) — distinct + /// from "no session", so an outage surfaces as 500 rather than a misleading 401. + #[error("session store unavailable")] + Internal, } impl IntoResponse for AuthError { @@ -80,6 +84,7 @@ impl IntoResponse for AuthError { match self { AuthError::Unauthenticated => StatusCode::UNAUTHORIZED, AuthError::Forbidden => StatusCode::FORBIDDEN, + AuthError::Internal => StatusCode::INTERNAL_SERVER_ERROR, } .into_response() } @@ -101,29 +106,30 @@ where type Rejection = AuthError; async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { + // A failed extraction here means the SessionManagerLayer is missing from the + // stack — a wiring bug, not an auth failure: surface it as 500. let session = Session::from_request_parts(parts, state) .await - .map_err(|_| AuthError::Unauthenticated)?; + .map_err(|_| AuthError::Internal)?; + // For each key: a store error (DB down) is `Internal` (500); an absent key is + // `Unauthenticated` (401) — these must not be conflated. let id: uuid::Uuid = session .get(SESSION_USER_ID) .await - .ok() - .flatten() + .map_err(|_| AuthError::Internal)? .ok_or(AuthError::Unauthenticated)?; let email: String = session .get(SESSION_EMAIL) .await - .ok() - .flatten() + .map_err(|_| AuthError::Internal)? .ok_or(AuthError::Unauthenticated)?; let role_str: String = session .get(SESSION_ROLE) .await - .ok() - .flatten() + .map_err(|_| AuthError::Internal)? .ok_or(AuthError::Unauthenticated)?; let role = Role::from_db(&role_str).ok_or(AuthError::Unauthenticated)?; @@ -221,9 +227,17 @@ mod tests { assert!(!verify_password("anything", "not-a-phc-string")); } + #[test] + fn verify_dummy_does_not_panic() { + verify_dummy("any input"); + verify_dummy("called again"); // exercises the already-initialized OnceLock path + } + #[test] fn capability_markers_map_to_domain_capabilities() { assert_eq!(ManageUsers::CAP, domain::Capability::ManageUsers); + assert_eq!(EditCatalogue::CAP, domain::Capability::EditCatalogue); assert_eq!(PublishObjects::CAP, domain::Capability::PublishObjects); + assert_eq!(ViewInternal::CAP, domain::Capability::ViewInternal); } }