fix: use SystemTime instead of Instant for session TTL
Instant is monotonic but drifts on sleep/hibernate common on NUC hardware. SystemTime gives proper wall-clock expiry for sessions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
354a495a28
commit
c47a811a14
@ -2,7 +2,7 @@ use sha2::{Digest, Sha256};
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::net::IpAddr;
|
use std::net::IpAddr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Instant;
|
use std::time::SystemTime;
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
use zeroize::Zeroize;
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
@ -30,8 +30,8 @@ impl Drop for SessionType {
|
|||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct Session {
|
struct Session {
|
||||||
created_at: Instant,
|
created_at: SystemTime,
|
||||||
last_activity: Instant,
|
last_activity: SystemTime,
|
||||||
session_type: SessionType,
|
session_type: SessionType,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,7 +53,7 @@ impl SessionStore {
|
|||||||
let token_bytes: [u8; 32] = rand::random();
|
let token_bytes: [u8; 32] = rand::random();
|
||||||
let token = hex::encode(token_bytes);
|
let token = hex::encode(token_bytes);
|
||||||
let hash = hash_token(&token);
|
let hash = hash_token(&token);
|
||||||
let now = Instant::now();
|
let now = SystemTime::now();
|
||||||
let session = Session {
|
let session = Session {
|
||||||
created_at: now,
|
created_at: now,
|
||||||
last_activity: now,
|
last_activity: now,
|
||||||
@ -72,7 +72,7 @@ impl SessionStore {
|
|||||||
let token_bytes: [u8; 32] = rand::random();
|
let token_bytes: [u8; 32] = rand::random();
|
||||||
let token = hex::encode(token_bytes);
|
let token = hex::encode(token_bytes);
|
||||||
let hash = hash_token(&token);
|
let hash = hash_token(&token);
|
||||||
let now = Instant::now();
|
let now = SystemTime::now();
|
||||||
let session = Session {
|
let session = Session {
|
||||||
created_at: now,
|
created_at: now,
|
||||||
last_activity: now,
|
last_activity: now,
|
||||||
@ -94,11 +94,11 @@ impl SessionStore {
|
|||||||
if !matches!(session.session_type, SessionType::Full) {
|
if !matches!(session.session_type, SessionType::Full) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if session.last_activity.elapsed().as_secs() >= FULL_SESSION_TTL {
|
if session.last_activity.elapsed().unwrap_or_default().as_secs() >= FULL_SESSION_TTL {
|
||||||
sessions.remove(&hash);
|
sessions.remove(&hash);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
session.last_activity = Instant::now();
|
session.last_activity = SystemTime::now();
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
@ -111,7 +111,7 @@ impl SessionStore {
|
|||||||
let hash = hash_token(token);
|
let hash = hash_token(token);
|
||||||
let mut sessions = self.sessions.write().await;
|
let mut sessions = self.sessions.write().await;
|
||||||
if let Some(session) = sessions.get_mut(&hash) {
|
if let Some(session) = sessions.get_mut(&hash) {
|
||||||
if session.created_at.elapsed().as_secs() >= PENDING_SESSION_TTL {
|
if session.created_at.elapsed().unwrap_or_default().as_secs() >= PENDING_SESSION_TTL {
|
||||||
sessions.remove(&hash);
|
sessions.remove(&hash);
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@ -137,7 +137,7 @@ impl SessionStore {
|
|||||||
let mut sessions = self.sessions.write().await;
|
let mut sessions = self.sessions.write().await;
|
||||||
if let Some(session) = sessions.get_mut(&hash) {
|
if let Some(session) = sessions.get_mut(&hash) {
|
||||||
session.session_type = SessionType::Full;
|
session.session_type = SessionType::Full;
|
||||||
let now = Instant::now();
|
let now = SystemTime::now();
|
||||||
session.created_at = now;
|
session.created_at = now;
|
||||||
session.last_activity = now;
|
session.last_activity = now;
|
||||||
}
|
}
|
||||||
@ -163,7 +163,7 @@ impl SessionStore {
|
|||||||
let new_token_bytes: [u8; 32] = rand::random();
|
let new_token_bytes: [u8; 32] = rand::random();
|
||||||
let new_token = hex::encode(new_token_bytes);
|
let new_token = hex::encode(new_token_bytes);
|
||||||
let new_hash = hash_token(&new_token);
|
let new_hash = hash_token(&new_token);
|
||||||
let now = Instant::now();
|
let now = SystemTime::now();
|
||||||
|
|
||||||
let mut sessions = self.sessions.write().await;
|
let mut sessions = self.sessions.write().await;
|
||||||
sessions.remove(&old_hash);
|
sessions.remove(&old_hash);
|
||||||
@ -183,9 +183,9 @@ impl SessionStore {
|
|||||||
let mut sessions = self.sessions.write().await;
|
let mut sessions = self.sessions.write().await;
|
||||||
sessions.retain(|_, session| {
|
sessions.retain(|_, session| {
|
||||||
match &session.session_type {
|
match &session.session_type {
|
||||||
SessionType::Full => session.last_activity.elapsed().as_secs() < FULL_SESSION_TTL,
|
SessionType::Full => session.last_activity.elapsed().unwrap_or_default().as_secs() < FULL_SESSION_TTL,
|
||||||
SessionType::PendingTotp { .. } => {
|
SessionType::PendingTotp { .. } => {
|
||||||
session.created_at.elapsed().as_secs() < PENDING_SESSION_TTL
|
session.created_at.elapsed().unwrap_or_default().as_secs() < PENDING_SESSION_TTL
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -218,7 +218,7 @@ impl SessionStore {
|
|||||||
.values()
|
.values()
|
||||||
.filter(|s| {
|
.filter(|s| {
|
||||||
matches!(s.session_type, SessionType::Full)
|
matches!(s.session_type, SessionType::Full)
|
||||||
&& s.last_activity.elapsed().as_secs() < FULL_SESSION_TTL
|
&& s.last_activity.elapsed().unwrap_or_default().as_secs() < FULL_SESSION_TTL
|
||||||
})
|
})
|
||||||
.count()
|
.count()
|
||||||
}
|
}
|
||||||
@ -262,7 +262,7 @@ impl LoginRateLimiter {
|
|||||||
|
|
||||||
pub async fn check(&self, ip: IpAddr) -> bool {
|
pub async fn check(&self, ip: IpAddr) -> bool {
|
||||||
let mut attempts = self.attempts.write().await;
|
let mut attempts = self.attempts.write().await;
|
||||||
let now = Instant::now();
|
let now = SystemTime::now();
|
||||||
let entry = attempts.entry(ip).or_default();
|
let entry = attempts.entry(ip).or_default();
|
||||||
entry.retain(|t| now.duration_since(*t).as_secs() < WINDOW_SECS);
|
entry.retain(|t| now.duration_since(*t).as_secs() < WINDOW_SECS);
|
||||||
entry.len() < MAX_ATTEMPTS
|
entry.len() < MAX_ATTEMPTS
|
||||||
@ -271,7 +271,7 @@ impl LoginRateLimiter {
|
|||||||
pub async fn record_failure(&self, ip: IpAddr) {
|
pub async fn record_failure(&self, ip: IpAddr) {
|
||||||
let mut attempts = self.attempts.write().await;
|
let mut attempts = self.attempts.write().await;
|
||||||
let entry = attempts.entry(ip).or_default();
|
let entry = attempts.entry(ip).or_default();
|
||||||
entry.push(Instant::now());
|
entry.push(SystemTime::now());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -338,7 +338,7 @@ impl EndpointRateLimiter {
|
|||||||
|
|
||||||
let key = (method.to_string(), ip);
|
let key = (method.to_string(), ip);
|
||||||
let mut requests = self.requests.write().await;
|
let mut requests = self.requests.write().await;
|
||||||
let now = Instant::now();
|
let now = SystemTime::now();
|
||||||
let entry = requests.entry(key).or_default();
|
let entry = requests.entry(key).or_default();
|
||||||
entry.retain(|t| now.duration_since(*t).as_secs() < window);
|
entry.retain(|t| now.duration_since(*t).as_secs() < window);
|
||||||
entry.len() < max_req
|
entry.len() < max_req
|
||||||
@ -352,13 +352,13 @@ impl EndpointRateLimiter {
|
|||||||
let key = (method.to_string(), ip);
|
let key = (method.to_string(), ip);
|
||||||
let mut requests = self.requests.write().await;
|
let mut requests = self.requests.write().await;
|
||||||
let entry = requests.entry(key).or_default();
|
let entry = requests.entry(key).or_default();
|
||||||
entry.push(Instant::now());
|
entry.push(SystemTime::now());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Periodic cleanup of expired entries.
|
/// Periodic cleanup of expired entries.
|
||||||
pub async fn cleanup(&self) {
|
pub async fn cleanup(&self) {
|
||||||
let mut requests = self.requests.write().await;
|
let mut requests = self.requests.write().await;
|
||||||
let now = Instant::now();
|
let now = SystemTime::now();
|
||||||
requests.retain(|(method, _), timestamps| {
|
requests.retain(|(method, _), timestamps| {
|
||||||
let window = self
|
let window = self
|
||||||
.limits
|
.limits
|
||||||
|
|||||||
@ -72,7 +72,7 @@
|
|||||||
|
|
||||||
## Phase 6: Backend Critical Fixes
|
## Phase 6: Backend Critical Fixes
|
||||||
|
|
||||||
- [ ] **Fix session TTL clock bug — use SystemTime instead of Instant**: Read `core/archipelago/src/session.rs`. Find where `Instant::now()` is used for session TTL/expiry (around line 97). `Instant` is monotonic but can drift on sleep/hibernate — common on NUC/Pi hardware. Replace with `SystemTime::now()` for absolute time comparison. The `FULL_SESSION_TTL` (24 hours) and `PENDING_TOTP_TTL` (5 minutes) checks should use `SystemTime::elapsed()` or store `SystemTime` timestamps and compare with `SystemTime::now()`. Run `cargo test --all-features` in `core/` on the dev server.
|
- [x] **Fix session TTL clock bug — use SystemTime instead of Instant**: Read `core/archipelago/src/session.rs`. Find where `Instant::now()` is used for session TTL/expiry (around line 97). `Instant` is monotonic but can drift on sleep/hibernate — common on NUC/Pi hardware. Replace with `SystemTime::now()` for absolute time comparison. The `FULL_SESSION_TTL` (24 hours) and `PENDING_TOTP_TTL` (5 minutes) checks should use `SystemTime::elapsed()` or store `SystemTime` timestamps and compare with `SystemTime::now()`. Run `cargo test --all-features` in `core/` on the dev server.
|
||||||
|
|
||||||
- [ ] **Enforce RBAC in RPC handler**: Read `core/archipelago/src/auth.rs` — find the `UserRole` enum and `can_access()` method. Then read `core/archipelago/src/api/rpc/mod.rs` — find where authenticated requests are dispatched to handlers. Add a role check before dispatching: after validating the session, get the user's role, call `role.can_access(method_name)`, and return an authorization error if denied. For now, all users created via onboarding should default to `Admin` role (single-user system), but this lays the groundwork for multi-user. Run `cargo clippy --all-targets --all-features && cargo test --all-features` on the dev server.
|
- [ ] **Enforce RBAC in RPC handler**: Read `core/archipelago/src/auth.rs` — find the `UserRole` enum and `can_access()` method. Then read `core/archipelago/src/api/rpc/mod.rs` — find where authenticated requests are dispatched to handlers. Add a role check before dispatching: after validating the session, get the user's role, call `role.can_access(method_name)`, and return an authorization error if denied. For now, all users created via onboarding should default to `Admin` role (single-user system), but this lays the groundwork for multi-user. Run `cargo clippy --all-targets --all-features && cargo test --all-features` on the dev server.
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user