Files
zesdex/crates/zesdex-iam/src/infrastructure/persistence/session_lock_repo.rs
T
asepharyana e9a8e93c83 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.
2026-07-20 06:39:30 +07:00

183 lines
6.5 KiB
Rust

//! Filesystem-backed `SessionLockRepository` implementation.
//!
//! Ported from `zesdex_entities::domain::auth::session_lock::SessionLock`'s
//! inherent methods — same atomic-create-based locking, same stale-PID
//! recovery via `libc::kill(pid, 0)` plus a `/proc/<pid>/exe` identity
//! check to guard against PID reuse. This repository is stateless (no
//! `Drop`-based auto-release) — callers that need panic-safety should wrap
//! acquisition in their own RAII guard (see `zesdex-backend`'s
//! `main.rs::SessionLockGuard`).
//!
//! # Flow
//!
//! 1. **`try_lock`** — attempt `O_CREAT|O_EXCL` open on `<session_dir>/.lock`;
//! if it already exists, check PID liveness; if stale, overwrite atomically.
//! 2. **`unlock`** — remove the `.lock` file.
//! 3. **`is_alive`** — `libc::kill(pid, 0)` + `/proc/<pid>/exe` identity check.
//!
//! # Components
//!
//! - `FileSystemSessionLockRepository` — stateless singleton implementing
//! `SessionLockRepository`
use std::convert::TryInto;
use std::fs;
use std::io::Write;
use std::path::Path;
use tracing;
use crate::domain::error::RepositoryError;
use crate::domain::repository::SessionLockRepository;
/// Concrete filesystem session-lock repository, using a PID file
/// (`<session_dir>/.lock`) with atomic `O_CREAT|O_EXCL` acquisition.
#[derive(Debug, Clone, Default)]
pub struct FileSystemSessionLockRepository;
impl FileSystemSessionLockRepository {
/// Create a new filesystem session-lock repository.
pub fn new() -> Self {
FileSystemSessionLockRepository
}
}
impl SessionLockRepository for FileSystemSessionLockRepository {
fn try_lock(&self, session_dir: &Path) -> Result<bool, RepositoryError> {
let path = session_dir.join(".lock");
let pid = std::process::id();
match fs::OpenOptions::new()
.create_new(true)
.write(true)
.open(&path)
{
Ok(mut file) => {
write!(file, "{pid}")?; // → RepositoryError via From<io::Error>
file.sync_all()?; // → RepositoryError via From<io::Error>
tracing::debug!(path = %path.display(), pid, "session lock acquired");
return Ok(true);
}
Err(ref e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
tracing::debug!(path = %path.display(), "lock file exists, checking staleness");
}
Err(e) => return Err(RepositoryError::Io(e)),
}
let content = fs::read_to_string(&path).unwrap_or_default();
if let Ok(existing_pid) = content.trim().parse::<u32>() {
if self.is_alive(existing_pid) {
tracing::warn!(existing_pid, path = %path.display(), "session lock held by live process");
return Ok(false);
}
tracing::debug!(existing_pid, "stale lock detected, overwriting");
}
let tmp = path.with_extension("lock.tmp");
{
let mut tmp_file = fs::OpenOptions::new()
.create(true)
.truncate(true)
.write(true)
.open(&tmp)?; // → RepositoryError via From<io::Error>
write!(tmp_file, "{pid}")?; // → RepositoryError
tmp_file.sync_all()?; // → RepositoryError
}
fs::rename(&tmp, &path)?; // → RepositoryError
if let Some(parent) = path.parent() {
let _ = fs::File::open(parent).and_then(|d| d.sync_all());
}
Ok(true)
}
fn unlock(&self, session_dir: &Path) -> Result<(), RepositoryError> {
let path = session_dir.join(".lock");
let _ = fs::remove_file(path);
Ok(())
}
fn is_alive(&self, pid: u32) -> bool {
// SAFETY: `libc::kill(pid, 0)` sends no signal; it only probes
// whether the process exists and is signalable by us.
// PIDs on Linux fit in i32 (default pid_max ≈ 4 million).
let pid_signed: i32 = pid.try_into()
.expect("PID exceeds i32 range — kernel pid_max > 2^31");
if unsafe { libc::kill(pid_signed, 0) != 0 } {
return false;
}
let proc_exe = std::path::PathBuf::from(format!("/proc/{pid}/exe"));
if let Ok(target) = std::fs::read_link(&proc_exe) {
if let Ok(exe) = std::env::current_exe() {
if target != exe {
return false;
}
}
}
true
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tmp_dir() -> std::path::PathBuf {
let dir =
std::env::temp_dir().join(format!("zesdex-iam-lock-test-{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn try_lock_succeeds_when_no_lock_file_exists() {
let dir = tmp_dir();
let repo = FileSystemSessionLockRepository::new();
assert!(repo.try_lock(&dir).unwrap());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn try_lock_fails_when_held_by_a_live_process() {
let dir = tmp_dir();
let repo = FileSystemSessionLockRepository::new();
assert!(repo.try_lock(&dir).unwrap());
// A second acquisition attempt (simulating our own still-live PID)
// must fail since the lock file already holds a live PID.
assert!(!repo.try_lock(&dir).unwrap());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn try_lock_recovers_a_stale_lock() {
let dir = tmp_dir();
let repo = FileSystemSessionLockRepository::new();
// Write a lock file with a PID that cannot possibly be alive.
std::fs::write(dir.join(".lock"), "999999999").unwrap();
assert!(
repo.try_lock(&dir).unwrap(),
"a stale lock (dead PID) must be recoverable"
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn unlock_removes_the_lock_file() {
let dir = tmp_dir();
let repo = FileSystemSessionLockRepository::new();
assert!(repo.try_lock(&dir).unwrap());
repo.unlock(&dir).unwrap();
assert!(!dir.join(".lock").exists());
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn is_alive_returns_true_for_current_process() {
let repo = FileSystemSessionLockRepository::new();
assert!(repo.is_alive(std::process::id()));
}
#[test]
fn is_alive_returns_false_for_implausible_pid() {
let repo = FileSystemSessionLockRepository::new();
assert!(!repo.is_alive(999_999_999));
}
}