diff --git a/daemon/src/storage/mod.rs b/daemon/src/storage/mod.rs index 008a024..a914602 100644 --- a/daemon/src/storage/mod.rs +++ b/daemon/src/storage/mod.rs @@ -1 +1,2 @@ +pub(crate) mod pending; pub(crate) mod policy; diff --git a/daemon/src/storage/pending.rs b/daemon/src/storage/pending.rs new file mode 100644 index 0000000..cd2cc26 --- /dev/null +++ b/daemon/src/storage/pending.rs @@ -0,0 +1,102 @@ +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 { + if user.is_empty() + || user.contains('/') + || user.contains('\0') + || user == "." + || user == ".." + { + return Err(PendingError::InvalidUser(user.to_string())); + } + Ok(self.dir.join(user)) + } + + 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()); + } + } +}