From fb18cb3c4f4e6fb959fe78424999da641668197b Mon Sep 17 00:00:00 2001 From: Antoine Gersant <antoine.gersant@lesforges.org> Date: Tue, 8 Oct 2024 23:03:15 -0700 Subject: [PATCH] Test config round trip --- src/app.rs | 11 ++++--- src/app/auth.rs | 18 +++++++++--- src/app/config.rs | 62 +++++++++++++++++++++++++++++++--------- src/app/config/mounts.rs | 19 ++++++------ src/server/axum/api.rs | 2 +- test-data/config.toml | 3 +- 6 files changed, 80 insertions(+), 35 deletions(-) diff --git a/src/app.rs b/src/app.rs index b9060d7..49c3d70 100644 --- a/src/app.rs +++ b/src/app.rs @@ -195,15 +195,14 @@ impl App { async fn get_or_create_auth_secret(path: &Path) -> Result<auth::Secret, Error> { match tokio::fs::read(&path).await { - Ok(s) => Ok(auth::Secret { - key: s - .try_into() + Ok(s) => Ok(auth::Secret( + s.try_into() .map_err(|_| Error::AuthenticationSecretInvalid)?, - }), + )), Err(e) if e.kind() == std::io::ErrorKind::NotFound => { let mut secret = auth::Secret::default(); - OsRng.fill_bytes(&mut secret.key); - tokio::fs::write(&path, &secret.key) + OsRng.fill_bytes(secret.as_mut()); + tokio::fs::write(&path, &secret) .await .map_err(|_| Error::AuthenticationSecretInvalid)?; Ok(secret) diff --git a/src/app/auth.rs b/src/app/auth.rs index 6887898..f452f4b 100644 --- a/src/app/auth.rs +++ b/src/app/auth.rs @@ -9,8 +9,18 @@ use serde::{Deserialize, Serialize}; use crate::app::Error; #[derive(Clone, Default)] -pub struct Secret { - pub key: [u8; 32], +pub struct Secret(pub [u8; 32]); + +impl AsRef<[u8]> for Secret { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl AsMut<[u8]> for Secret { + fn as_mut(&mut self) -> &mut [u8] { + &mut self.0 + } } #[derive(Debug)] @@ -55,7 +65,7 @@ pub fn generate_auth_token( serde_json::to_string(&authorization).or(Err(Error::AuthorizationTokenEncoding))?; branca::encode( serialized_authorization.as_bytes(), - &auth_secret.key, + auth_secret.as_ref(), SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() @@ -75,7 +85,7 @@ pub fn decode_auth_token( Scope::PolarisAuth => 0, // permanent }; let authorization = - branca::decode(data, &auth_secret.key, ttl).map_err(|_| Error::InvalidAuthToken)?; + branca::decode(data, auth_secret.as_ref(), ttl).map_err(|_| Error::InvalidAuthToken)?; let authorization: Authorization = serde_json::from_slice(&authorization[..]).map_err(|_| Error::InvalidAuthToken)?; if authorization.scope != scope { diff --git a/src/app/config.rs b/src/app/config.rs index 4e3e6f3..c7adac9 100644 --- a/src/app/config.rs +++ b/src/app/config.rs @@ -24,7 +24,7 @@ pub struct Config { pub reindex_every_n_seconds: Option<u64>, pub album_art_pattern: Option<Regex>, pub ddns_url: Option<http::Uri>, - pub mount_dirs: Vec<MountDir>, + pub mount_dirs: HashMap<String, MountDir>, pub users: HashMap<String, User>, } @@ -34,16 +34,15 @@ impl TryFrom<storage::Config> for Config { fn try_from(c: storage::Config) -> Result<Self, Self::Error> { let mut users: HashMap<String, User> = HashMap::new(); for user in c.users { - if let Ok(user) = <storage::User as TryInto<User>>::try_into(user) { - users.insert(user.name.clone(), user); - } + let user = <storage::User as TryInto<User>>::try_into(user)?; + users.insert(user.name.clone(), user); } - let mount_dirs = c - .mount_dirs - .into_iter() - .filter_map(|m| m.try_into().ok()) - .collect(); + let mut mount_dirs: HashMap<String, MountDir> = HashMap::new(); + for mount_dir in c.mount_dirs { + let mount_dir = <storage::MountDir as TryInto<MountDir>>::try_into(mount_dir)?; + mount_dirs.insert(mount_dir.name.clone(), mount_dir); + } let ddns_url = match c.ddns_url.map(http::Uri::try_from) { Some(Ok(u)) => Some(u), @@ -58,7 +57,7 @@ impl TryFrom<storage::Config> for Config { }; Ok(Config { - reindex_every_n_seconds: c.reindex_every_n_seconds, // TODO validate and warn + reindex_every_n_seconds: c.reindex_every_n_seconds, album_art_pattern, ddns_url, mount_dirs, @@ -72,7 +71,7 @@ impl From<Config> for storage::Config { Self { reindex_every_n_seconds: c.reindex_every_n_seconds, album_art_pattern: c.album_art_pattern.map(|p| p.as_str().to_owned()), - mount_dirs: c.mount_dirs.into_iter().map(|d| d.into()).collect(), + mount_dirs: c.mount_dirs.into_iter().map(|(_, d)| d.into()).collect(), ddns_url: c.ddns_url.map(|u| u.to_string()), users: c.users.into_iter().map(|(_, u)| u.into()).collect(), } @@ -208,7 +207,8 @@ impl Manager { } pub async fn get_mounts(&self) -> Vec<MountDir> { - self.config.read().await.mount_dirs.clone() + let config = self.config.read().await; + config.mount_dirs.values().cloned().collect() } pub async fn resolve_virtual_path<P: AsRef<Path>>( @@ -219,8 +219,42 @@ impl Manager { config.resolve_virtual_path(virtual_path) } - pub async fn set_mounts(&self, mount_dirs: Vec<storage::MountDir>) { - self.config.write().await.set_mounts(mount_dirs); + pub async fn set_mounts(&self, mount_dirs: Vec<storage::MountDir>) -> Result<(), Error> { + self.config.write().await.set_mounts(mount_dirs) // TODO persistence } } + +#[cfg(test)] +mod test { + use super::*; + + #[tokio::test] + async fn can_read_config() { + let config_path = PathBuf::from_iter(["test-data", "config.toml"]); + let manager = Manager::new(&config_path, auth::Secret([0; 32])) + .await + .unwrap(); + let config: storage::Config = manager.config.read().await.clone().into(); + + assert_eq!(config.reindex_every_n_seconds, None); + assert_eq!( + config.album_art_pattern, + Some(r#"^Folder\.(png|jpg|jpeg)$"#.to_owned()) + ); + assert_eq!( + config.mount_dirs, + vec![storage::MountDir { + source: PathBuf::from("test-data/small-collection"), + name: "root".to_owned(), + }] + ); + assert_eq!(config.users[0].name, "test_user"); + assert_eq!(config.users[0].admin, Some(true)); + assert_eq!( + config.users[0].initial_password, + Some("very_secret_password".to_owned()) + ); + assert!(config.users[0].hashed_password.is_some()); + } +} diff --git a/src/app/config/mounts.rs b/src/app/config/mounts.rs index ec97d5d..8ca692e 100644 --- a/src/app/config/mounts.rs +++ b/src/app/config/mounts.rs @@ -1,4 +1,5 @@ use std::{ + collections::HashMap, ops::Deref, path::{Path, PathBuf}, }; @@ -38,17 +39,19 @@ impl From<MountDir> for storage::MountDir { } impl Config { - pub fn set_mounts(&mut self, mount_dirs: Vec<storage::MountDir>) { - self.mount_dirs = mount_dirs - .into_iter() - .filter_map(|m| m.try_into().ok()) - .collect(); + pub fn set_mounts(&mut self, mount_dirs: Vec<storage::MountDir>) -> Result<(), Error> { + let mut new_mount_dirs = HashMap::new(); + for mount_dir in mount_dirs { + let mount_dir = <storage::MountDir as TryInto<MountDir>>::try_into(mount_dir)?; + new_mount_dirs.insert(mount_dir.name.clone(), mount_dir); + } + self.mount_dirs = new_mount_dirs; + Ok(()) } pub fn resolve_virtual_path<P: AsRef<Path>>(&self, virtual_path: P) -> Result<PathBuf, Error> { - for mount in &self.mount_dirs { - let mount_path = Path::new(&mount.name); - if let Ok(p) = virtual_path.as_ref().strip_prefix(mount_path) { + for (name, mount) in &self.mount_dirs { + if let Ok(p) = virtual_path.as_ref().strip_prefix(name) { return if p.components().count() == 0 { Ok(mount.source.clone()) } else { diff --git a/src/server/axum/api.rs b/src/server/axum/api.rs index f233ab5..4725778 100644 --- a/src/server/axum/api.rs +++ b/src/server/axum/api.rs @@ -161,7 +161,7 @@ async fn put_mount_dirs( ) -> Result<(), APIError> { let new_mount_dirs: Vec<config::storage::MountDir> = new_mount_dirs.iter().cloned().map(|m| m.into()).collect(); - config_manager.set_mounts(new_mount_dirs).await; + config_manager.set_mounts(new_mount_dirs).await?; Ok(()) } diff --git a/test-data/config.toml b/test-data/config.toml index 9ad682b..61b3244 100644 --- a/test-data/config.toml +++ b/test-data/config.toml @@ -1,4 +1,3 @@ -[settings] album_art_pattern = '^Folder\.(png|jpg|jpeg)$' [[mount_dirs]] @@ -7,5 +6,5 @@ source = 'test-data/small-collection' [[users]] name = 'test_user' -password = 'very_secret_password' +initial_password = 'very_secret_password' admin = true