harden(auth): distinguish session-store failure (500) from absent session (401); exhaustive marker + verify_dummy tests

This commit is contained in:
2026-06-02 14:48:40 +02:00
parent 992526ef77
commit 4e7288731a
+21 -7
View File
@@ -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<Self, Self::Rejection> {
// 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);
}
}