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 { 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 { 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, 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()); } } }