Refactor session ID handling and improve error management
- Introduced `SessionId` newtype for validated session identifiers, ensuring safety against path traversal attacks. - Updated session repository methods to accept `SessionId` instead of raw strings, enhancing type safety. - Removed redundant error handling in repository methods by leveraging the new `Error` type from `zesdex_utils`. - Simplified atomic JSON write operations by eliminating unnecessary error conversions. - Enhanced integer casting with a new `CastOr` trait for safer narrowing conversions. - Removed deprecated error handling code and consolidated error types across the codebase. - Updated HTTP handlers to utilize the new session ID validation, improving overall robustness.
This commit is contained in:
@@ -7,14 +7,15 @@
|
||||
//!
|
||||
//! - **`list_sessions`** — enumerate `<base_dir>/sessions/` subdirectories,
|
||||
//! attempt `load_session` on each (silently skipping failures).
|
||||
//! - **`load_session`** — validates id (path-traversal check), reads JSON.
|
||||
//! - **`load_session`** — reads and deserialises `session.json`.
|
||||
//! - **`save_session`** — creates session directory, writes JSON atomically.
|
||||
//! - **`delete_session`** — validates id, removes the session directory.
|
||||
//! - **`delete_session`** — removes the session directory.
|
||||
//!
|
||||
//! # Security
|
||||
//!
|
||||
//! All methods that accept a user-supplied `id` string reject ids containing
|
||||
//! `/`, `\\`, or `..` to prevent directory-traversal attacks.
|
||||
//! Session IDs are validated at construction via [`SessionId::new`], so
|
||||
//! directory-traversal attacks are prevented by the type system — no
|
||||
//! per-method checks needed.
|
||||
//!
|
||||
//! # Components
|
||||
//!
|
||||
@@ -22,22 +23,13 @@
|
||||
use std::path::Path;
|
||||
use tracing;
|
||||
|
||||
use zesdex_entities::domain::auth::SessionId;
|
||||
use zesdex_utils::write_json_atomic;
|
||||
|
||||
use crate::domain::error::RepositoryError;
|
||||
use crate::domain::repository::SessionRepository;
|
||||
use crate::domain::session::Session;
|
||||
|
||||
/// Validate a session id, rejecting path-traversal patterns.
|
||||
fn validate_id(id: &str) -> Result<(), RepositoryError> {
|
||||
if id.contains('/') || id.contains('\\') || id.contains("..") {
|
||||
return Err(RepositoryError::InvalidId(format!(
|
||||
"session id '{id}' must not contain path separators"
|
||||
)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Concrete filesystem session repository.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct FileSystemSessionRepository;
|
||||
@@ -65,20 +57,25 @@ impl SessionRepository for FileSystemSessionRepository {
|
||||
if !entry.path().is_dir() {
|
||||
continue;
|
||||
}
|
||||
let id = entry.file_name().to_string_lossy().to_string();
|
||||
if let Ok(session) = self.load_session(base_dir, &id) {
|
||||
sessions.push(session);
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
// Directory names from UUIDs are always valid session IDs.
|
||||
if let Ok(sid) = SessionId::new(&name) {
|
||||
if let Ok(session) = self.load_session(base_dir, &sid) {
|
||||
sessions.push(session);
|
||||
}
|
||||
}
|
||||
}
|
||||
tracing::debug!(count = sessions.len(), "listed sessions");
|
||||
Ok(sessions)
|
||||
}
|
||||
|
||||
fn load_session(&self, base_dir: &Path, id: &str) -> Result<Session, RepositoryError> {
|
||||
validate_id(id)?;
|
||||
let path = base_dir.join("sessions").join(id).join("session.json");
|
||||
fn load_session(&self, base_dir: &Path, id: &SessionId) -> Result<Session, RepositoryError> {
|
||||
let path = base_dir.join("sessions").join(id.as_str()).join("session.json");
|
||||
if !path.exists() {
|
||||
return Err(RepositoryError::NotFound(format!("session not found: {id}")));
|
||||
return Err(RepositoryError::NotFound(format!(
|
||||
"session not found: {}",
|
||||
id.as_str()
|
||||
)));
|
||||
}
|
||||
tracing::debug!(session_id = %id, path = %path.display(), "loading session");
|
||||
let data = std::fs::read_to_string(&path)?; // → RepositoryError
|
||||
@@ -91,13 +88,12 @@ impl SessionRepository for FileSystemSessionRepository {
|
||||
std::fs::create_dir_all(&dir)?; // → RepositoryError
|
||||
let path = dir.join("session.json");
|
||||
tracing::debug!(session_id = %session.id, path = %path.display(), "saving session");
|
||||
write_json_atomic(&path, session, None).map_err(RepositoryError::from_anyhow)?;
|
||||
write_json_atomic(&path, session, None)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn delete_session(&self, base_dir: &Path, id: &str) -> Result<(), RepositoryError> {
|
||||
validate_id(id)?;
|
||||
let dir = base_dir.join("sessions").join(id);
|
||||
fn delete_session(&self, base_dir: &Path, id: &SessionId) -> Result<(), RepositoryError> {
|
||||
let dir = base_dir.join("sessions").join(id.as_str());
|
||||
tracing::debug!(session_id = %id, path = %dir.display(), "deleting session");
|
||||
if dir.exists() {
|
||||
std::fs::remove_dir_all(&dir)?; // → RepositoryError
|
||||
|
||||
Reference in New Issue
Block a user