pinakes-server: fix api key timing, notification scoping, and validate progress inputs
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: Ieb342b4b48034de0a2184cdf89d068316a6a6964
This commit is contained in:
parent
52f0b5defc
commit
2b2c1830a1
7 changed files with 334 additions and 179 deletions
|
|
@ -9,23 +9,28 @@ use pinakes_core::config::UserRole;
|
|||
use crate::state::AppState;
|
||||
|
||||
/// Constant-time string comparison to prevent timing attacks on API keys.
|
||||
///
|
||||
/// Always iterates to `max(len_a, len_b)` so that neither a length difference
|
||||
/// nor a byte mismatch causes an early return.
|
||||
fn constant_time_eq(a: &str, b: &str) -> bool {
|
||||
if a.len() != b.len() {
|
||||
return false;
|
||||
let a = a.as_bytes();
|
||||
let b = b.as_bytes();
|
||||
let len = a.len().max(b.len());
|
||||
let mut result = a.len() ^ b.len(); // non-zero if lengths differ
|
||||
for i in 0..len {
|
||||
let ab = a.get(i).copied().unwrap_or(0);
|
||||
let bb = b.get(i).copied().unwrap_or(0);
|
||||
result |= usize::from(ab ^ bb);
|
||||
}
|
||||
a.as_bytes()
|
||||
.iter()
|
||||
.zip(b.as_bytes())
|
||||
.fold(0u8, |acc, (x, y)| acc | (x ^ y))
|
||||
== 0
|
||||
result == 0
|
||||
}
|
||||
|
||||
/// Axum middleware that checks for a valid Bearer token.
|
||||
///
|
||||
/// If `accounts.enabled == true`: look up bearer token in database session
|
||||
/// store. If `accounts.enabled == false`: use existing api_key logic (unchanged
|
||||
/// behavior). Skips authentication for the `/health` and `/auth/login` path
|
||||
/// suffixes.
|
||||
/// store. If `accounts.enabled == false`: use existing `api_key` logic
|
||||
/// (unchanged behavior). Skips authentication for the `/health` and
|
||||
/// `/auth/login` path suffixes.
|
||||
pub async fn require_auth(
|
||||
State(state): State<AppState>,
|
||||
mut request: Request,
|
||||
|
|
@ -58,7 +63,7 @@ pub async fn require_auth(
|
|||
.get("authorization")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|s| s.strip_prefix("Bearer "))
|
||||
.map(|s| s.to_string());
|
||||
.map(std::string::ToString::to_string);
|
||||
|
||||
let Some(token) = token else {
|
||||
tracing::debug!(path = %path, "rejected: missing Authorization header");
|
||||
|
|
@ -83,7 +88,7 @@ pub async fn require_auth(
|
|||
// Check session expiry
|
||||
let now = chrono::Utc::now();
|
||||
if session.expires_at < now {
|
||||
let username = session.username.clone();
|
||||
let username = session.username;
|
||||
// Delete expired session in a bounded background task
|
||||
if let Ok(permit) = state.session_semaphore.clone().try_acquire_owned() {
|
||||
let storage = state.storage.clone();
|
||||
|
|
@ -124,7 +129,7 @@ pub async fn require_auth(
|
|||
|
||||
// Inject role and username into request extensions
|
||||
request.extensions_mut().insert(role);
|
||||
request.extensions_mut().insert(session.username.clone());
|
||||
request.extensions_mut().insert(session.username);
|
||||
} else {
|
||||
// Legacy API key auth
|
||||
let api_key = std::env::var("PINAKES_API_KEY")
|
||||
|
|
@ -202,7 +207,7 @@ pub async fn require_admin(request: Request, next: Next) -> Response {
|
|||
}
|
||||
}
|
||||
|
||||
/// Resolve the authenticated username (from request extensions) to a UserId.
|
||||
/// Resolve the authenticated username (from request extensions) to a `UserId`.
|
||||
///
|
||||
/// Returns an error if the user cannot be found.
|
||||
pub async fn resolve_user_id(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue