Extract the inline path-traversal check from PendingStore into a shared storage::safe_user::join_user_segment helper. RecoveryStore (next) reuses it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
97 lines
2.9 KiB
Rust
97 lines
2.9 KiB
Rust
use authforge_common::types::PendingFlag;
|
|
use std::path::PathBuf;
|
|
use thiserror::Error;
|
|
|
|
#[derive(Debug, Error)]
|
|
pub(crate) enum PendingError {
|
|
#[error("io: {0}")]
|
|
Io(#[from] std::io::Error),
|
|
#[error("invalid username: {0:?}")]
|
|
InvalidUser(String),
|
|
#[error("json: {0}")]
|
|
Json(#[from] serde_json::Error),
|
|
}
|
|
|
|
#[allow(dead_code)] // wired through AppState in Task 2.15.
|
|
pub(crate) struct PendingStore {
|
|
dir: PathBuf,
|
|
}
|
|
|
|
#[allow(dead_code)] // wired through AppState in Task 2.15.
|
|
impl PendingStore {
|
|
pub fn new(dir: PathBuf) -> Self {
|
|
Self { dir }
|
|
}
|
|
|
|
fn user_path(&self, user: &str) -> Result<PathBuf, PendingError> {
|
|
super::safe_user::join_user_segment(&self.dir, user).map_err(|e| match e {
|
|
super::safe_user::SegmentError::Invalid(s) => PendingError::InvalidUser(s),
|
|
})
|
|
}
|
|
|
|
pub fn set(&self, user: &str, flag: &PendingFlag) -> Result<(), PendingError> {
|
|
std::fs::create_dir_all(&self.dir)?;
|
|
let path = self.user_path(user)?;
|
|
let body = serde_json::to_vec_pretty(flag)?;
|
|
std::fs::write(path, body)?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn clear(&self, user: &str) -> Result<bool, PendingError> {
|
|
let path = self.user_path(user)?;
|
|
match std::fs::remove_file(path) {
|
|
Ok(()) => Ok(true),
|
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
|
|
Err(e) => Err(PendingError::Io(e)),
|
|
}
|
|
}
|
|
|
|
pub fn get(&self, user: &str) -> Result<Option<PendingFlag>, PendingError> {
|
|
let path = self.user_path(user)?;
|
|
match std::fs::read(&path) {
|
|
Ok(b) => Ok(Some(serde_json::from_slice(&b)?)),
|
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
|
|
Err(e) => Err(PendingError::Io(e)),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use authforge_common::types::Method;
|
|
use tempfile::tempdir;
|
|
|
|
fn flag() -> PendingFlag {
|
|
PendingFlag {
|
|
required_methods: vec![Method::Fido2],
|
|
created_unix: 1,
|
|
deadline_unix: 0,
|
|
re_enroll: false,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn set_get_clear_roundtrip() {
|
|
let d = tempdir().unwrap();
|
|
let s = PendingStore::new(d.path().to_path_buf());
|
|
assert!(s.get("alice").unwrap().is_none());
|
|
s.set("alice", &flag()).unwrap();
|
|
assert_eq!(s.get("alice").unwrap().unwrap(), flag());
|
|
assert!(s.clear("alice").unwrap());
|
|
assert!(s.get("alice").unwrap().is_none());
|
|
assert!(!s.clear("alice").unwrap());
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_path_traversal() {
|
|
let d = tempdir().unwrap();
|
|
let s = PendingStore::new(d.path().to_path_buf());
|
|
for evil in ["../etc", "a/b", "", ".", "..", "x\0y"] {
|
|
assert!(s.set(evil, &flag()).is_err());
|
|
assert!(s.get(evil).is_err());
|
|
assert!(s.clear(evil).is_err());
|
|
}
|
|
}
|
|
}
|