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,
|
Unauthenticated,
|
||||||
#[error("insufficient permissions")]
|
#[error("insufficient permissions")]
|
||||||
Forbidden,
|
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 {
|
impl IntoResponse for AuthError {
|
||||||
@@ -80,6 +84,7 @@ impl IntoResponse for AuthError {
|
|||||||
match self {
|
match self {
|
||||||
AuthError::Unauthenticated => StatusCode::UNAUTHORIZED,
|
AuthError::Unauthenticated => StatusCode::UNAUTHORIZED,
|
||||||
AuthError::Forbidden => StatusCode::FORBIDDEN,
|
AuthError::Forbidden => StatusCode::FORBIDDEN,
|
||||||
|
AuthError::Internal => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
}
|
}
|
||||||
.into_response()
|
.into_response()
|
||||||
}
|
}
|
||||||
@@ -101,29 +106,30 @@ where
|
|||||||
type Rejection = AuthError;
|
type Rejection = AuthError;
|
||||||
|
|
||||||
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
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)
|
let session = Session::from_request_parts(parts, state)
|
||||||
.await
|
.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
|
let id: uuid::Uuid = session
|
||||||
.get(SESSION_USER_ID)
|
.get(SESSION_USER_ID)
|
||||||
.await
|
.await
|
||||||
.ok()
|
.map_err(|_| AuthError::Internal)?
|
||||||
.flatten()
|
|
||||||
.ok_or(AuthError::Unauthenticated)?;
|
.ok_or(AuthError::Unauthenticated)?;
|
||||||
|
|
||||||
let email: String = session
|
let email: String = session
|
||||||
.get(SESSION_EMAIL)
|
.get(SESSION_EMAIL)
|
||||||
.await
|
.await
|
||||||
.ok()
|
.map_err(|_| AuthError::Internal)?
|
||||||
.flatten()
|
|
||||||
.ok_or(AuthError::Unauthenticated)?;
|
.ok_or(AuthError::Unauthenticated)?;
|
||||||
|
|
||||||
let role_str: String = session
|
let role_str: String = session
|
||||||
.get(SESSION_ROLE)
|
.get(SESSION_ROLE)
|
||||||
.await
|
.await
|
||||||
.ok()
|
.map_err(|_| AuthError::Internal)?
|
||||||
.flatten()
|
|
||||||
.ok_or(AuthError::Unauthenticated)?;
|
.ok_or(AuthError::Unauthenticated)?;
|
||||||
|
|
||||||
let role = Role::from_db(&role_str).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"));
|
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]
|
#[test]
|
||||||
fn capability_markers_map_to_domain_capabilities() {
|
fn capability_markers_map_to_domain_capabilities() {
|
||||||
assert_eq!(ManageUsers::CAP, domain::Capability::ManageUsers);
|
assert_eq!(ManageUsers::CAP, domain::Capability::ManageUsers);
|
||||||
|
assert_eq!(EditCatalogue::CAP, domain::Capability::EditCatalogue);
|
||||||
assert_eq!(PublishObjects::CAP, domain::Capability::PublishObjects);
|
assert_eq!(PublishObjects::CAP, domain::Capability::PublishObjects);
|
||||||
|
assert_eq!(ViewInternal::CAP, domain::Capability::ViewInternal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user