harden(auth): distinguish session-store failure (500) from absent session (401); exhaustive marker + verify_dummy tests
This commit is contained in:
+21
-7
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user