diff options
author | Rina Fujino <18257209+rina23q@users.noreply.github.com> | 2022-05-18 01:22:31 +0200 |
---|---|---|
committer | Rina Fujino <18257209+rina23q@users.noreply.github.com> | 2022-05-18 01:22:31 +0200 |
commit | 88ef4b2fd0076811687a0b23ec21edad1ca2e0af (patch) | |
tree | d188a55087072d0f8c9ef05e89e34b4ac0126e0c | |
parent | 7e6d95fa0d0289885df2cd533318c9afdd3e5e69 (diff) |
Add user, group, mode support
Signed-off-by: Rina Fujino <18257209+rina23q@users.noreply.github.com>
-rw-r--r-- | crates/common/tedge_utils/Cargo.toml | 2 | ||||
-rw-r--r-- | crates/common/tedge_utils/src/file.rs | 208 | ||||
-rw-r--r-- | plugins/c8y_configuration_plugin/src/config.rs | 145 | ||||
-rw-r--r-- | plugins/c8y_configuration_plugin/src/download.rs | 170 | ||||
-rw-r--r-- | plugins/c8y_configuration_plugin/src/error.rs | 16 | ||||
-rw-r--r-- | plugins/c8y_configuration_plugin/src/main.rs | 11 | ||||
-rw-r--r-- | plugins/c8y_configuration_plugin/src/upload.rs | 13 |
7 files changed, 381 insertions, 184 deletions
diff --git a/crates/common/tedge_utils/Cargo.toml b/crates/common/tedge_utils/Cargo.toml index 7a42e257..8ccb0ed0 100644 --- a/crates/common/tedge_utils/Cargo.toml +++ b/crates/common/tedge_utils/Cargo.toml @@ -16,7 +16,7 @@ logging = ["tracing", "tracing-subscriber"] nix = "0.23.1" tempfile = "3.2" thiserror = "1.0" -tokio = { version = "1.12", default_features = false, features = [ "fs", "io-util", "macros", "signal"] } +tokio = { version = "1.12", default_features = false, features = [ "fs", "io-util", "macros", "signal", "rt-multi-thread"] } tracing = { version = "0.1", features = [], optional = true } tracing-subscriber = { version = "0.3", optional = true, features = [ "time" ] } users = "0.11.0" diff --git a/crates/common/tedge_utils/src/file.rs b/crates/common/tedge_utils/src/file.rs index cc819e05..03cb68b0 100644 --- a/crates/common/tedge_utils/src/file.rs +++ b/crates/common/tedge_utils/src/file.rs @@ -2,6 +2,7 @@ use nix::unistd::*; use std::fs::File; use std::os::linux::fs::MetadataExt; use std::os::unix::fs::PermissionsExt; +use std::path::{Path, PathBuf}; use std::{fs, io}; use users::{get_group_by_name, get_user_by_name}; @@ -24,6 +25,9 @@ pub enum FileError { #[error(transparent)] Errno(#[from] nix::errno::Errno), + + #[error("The path is not accessible. {path:?}")] + PathNotAccessible { path: PathBuf }, } pub fn create_directory_with_user_group( @@ -97,34 +101,91 @@ fn change_owner_and_permission( } }; - let uid = fs::metadata(file) - .map_err(|e| FileError::MetaDataError { - name: file.to_string(), - from: e, - })? - .st_uid(); - let gid = fs::metadata(file) - .map_err(|e| FileError::MetaDataError { - name: file.to_string(), - from: e, - })? - .st_gid(); + let uid = get_metadata(Path::new(file))?.st_uid(); + let gid = get_metadata(Path::new(file))?.st_gid(); // if user and group is same as existing, then do not change if (ud != uid) && (gd != gid) { - chown( - file, - Some(Uid::from_raw(ud.into())), - Some(Gid::from_raw(gd.into())), - )?; + chown(file, Some(Uid::from_raw(ud)), Some(Gid::from_raw(gd)))?; + } + + let mut perm = get_metadata(Path::new(file))?.permissions(); + perm.set_mode(mode); + + fs::set_permissions(file, perm).map_err(|e| FileError::MetaDataError { + name: file.to_string(), + from: e, + })?; + + Ok(()) +} + +#[derive(Debug, PartialEq, Eq, Default, Clone)] +pub struct FilePermissions { + pub user: Option<String>, + pub group: Option<String>, + pub mode: Option<u32>, +} + +impl FilePermissions { + pub fn new(user: Option<String>, group: Option<String>, mode: Option<u32>) -> Self { + Self { user, group, mode } + } + + pub fn change_permissions(&self, file: &str) -> Result<(), FileError> { + if let Some(user) = &self.user { + let () = change_user(file, user)?; + } + if let Some(group) = &self.group { + let () = change_group(file, group)?; + } + if let Some(mode) = &self.mode { + let () = change_mode(file, *mode)?; + } + Ok(()) + } +} + +fn change_user(file: &str, user: &str) -> Result<(), FileError> { + let ud = match get_user_by_name(user) { + Some(user) => user.uid(), + None => { + return Err(FileError::UserNotFound { user: user.into() }); + } + }; + + let uid = get_metadata(Path::new(file))?.st_uid(); + + // if user is same as existing, then do not change + if ud != uid { + chown(file, Some(Uid::from_raw(ud)), None)?; + } + + Ok(()) +} + +fn change_group(file: &str, group: &str) -> Result<(), FileError> { + let gd = match get_group_by_name(group) { + Some(group) => group.gid(), + None => { + return Err(FileError::GroupNotFound { + group: group.into(), + }); + } + }; + + let gid = get_metadata(Path::new(file))?.st_gid(); + + // if group is same as existing, then do not change + if gd != gid { + chown(file, None, Some(Gid::from_raw(gd)))?; } - let mut perm = fs::metadata(file) - .map_err(|e| FileError::MetaDataError { - name: file.to_string(), - from: e, - })? - .permissions(); + Ok(()) +} + +fn change_mode(file: &str, mode: u32) -> Result<(), FileError> { + let mut perm = get_metadata(Path::new(file))?.permissions(); perm.set_mode(mode); fs::set_permissions(file, perm).map_err(|e| FileError::MetaDataError { @@ -134,77 +195,136 @@ fn change_owner_and_permission( Ok(()) } + +/// Return metadata when the given path exists and accessible by user +pub fn get_metadata(path: &Path) -> Result<fs::Metadata, FileError> { + fs::metadata(&path).map_err(|_| FileError::PathNotAccessible { + path: path.to_path_buf(), + }) +} + +/// Return filename if the given path contains a filename +pub fn get_filename(path: PathBuf) -> Option<String> { + let filename = path.file_name()?.to_str()?.to_string(); + Some(filename) +} + #[cfg(test)] mod tests { use super::*; use std::os::unix::fs::PermissionsExt; use std::path::Path; + use tempfile::TempDir; #[test] fn create_file_correct_user_group() { + let temp_dir = TempDir::new().unwrap(); + let file_path = temp_dir.path().join("file").display().to_string(); + let user = whoami::username(); - let _ = create_file_with_user_group("/tmp/fcreate_test", &user, &user, 0o644).unwrap(); - assert!(Path::new("/tmp/fcreate_test").exists()); - let meta = std::fs::metadata("/tmp/fcreate_test").unwrap(); + let _ = create_file_with_user_group(file_path.as_str(), &user, &user, 0o644).unwrap(); + assert!(Path::new(file_path.as_str()).exists()); + let meta = std::fs::metadata(file_path.as_str()).unwrap(); let perm = meta.permissions(); println!("{:o}", perm.mode()); assert!(format!("{:o}", perm.mode()).contains("644")); - fs::remove_file("/tmp/fcreate_test").unwrap(); } #[test] fn create_file_wrong_user() { + let temp_dir = TempDir::new().unwrap(); + let file_path = temp_dir.path().join("file").display().to_string(); + let user = whoami::username(); - let err = create_file_with_user_group("/tmp/fcreate_wrong_user", "test", &user, 0o775) - .unwrap_err(); + let err = + create_file_with_user_group(file_path.as_str(), "test", &user, 0o775).unwrap_err(); assert!(err.to_string().contains("User not found")); - fs::remove_file("/tmp/fcreate_wrong_user").unwrap(); } #[test] fn create_file_wrong_group() { + let temp_dir = TempDir::new().unwrap(); + let file_path = temp_dir.path().join("file").display().to_string(); + let user = whoami::username(); - let err = create_file_with_user_group("/tmp/fcreate_wrong_group", &user, "test", 0o775) - .unwrap_err(); + let err = + create_file_with_user_group(file_path.as_str(), &user, "test", 0o775).unwrap_err(); assert!(err.to_string().contains("Group not found")); - fs::remove_file("/tmp/fcreate_wrong_group").unwrap(); + fs::remove_file(file_path.as_str()).unwrap(); } #[test] fn create_directory_with_correct_user_group() { + let temp_dir = TempDir::new().unwrap(); + let dir_path = temp_dir.path().join("dir").display().to_string(); + let user = whoami::username(); - let _ = - create_directory_with_user_group("/tmp/fcreate_test_dir", &user, &user, 0o775).unwrap(); + let _ = create_directory_with_user_group(dir_path.as_str(), &user, &user, 0o775).unwrap(); - assert!(Path::new("/tmp/fcreate_test_dir").exists()); - let meta = std::fs::metadata("/tmp/fcreate_test_dir").unwrap(); + assert!(Path::new(dir_path.as_str()).exists()); + let meta = fs::metadata(dir_path.as_str()).unwrap(); let perm = meta.permissions(); println!("{:o}", perm.mode()); assert!(format!("{:o}", perm.mode()).contains("775")); - fs::remove_dir("/tmp/fcreate_test_dir").unwrap(); } #[test] fn create_directory_with_wrong_user() { + let temp_dir = TempDir::new().unwrap(); + let dir_path = temp_dir.path().join("dir").display().to_string(); + let user = whoami::username(); - let err = create_directory_with_user_group("/tmp/wrong_user_dir", "test", &user, 0o775) - .unwrap_err(); + let err = + create_directory_with_user_group(dir_path.as_str(), "test", &user, 0o775).unwrap_err(); assert!(err.to_string().contains("User not found")); - fs::remove_dir("/tmp/wrong_user_dir").unwrap(); } #[test] fn create_directory_with_wrong_group() { + let temp_dir = TempDir::new().unwrap(); + let dir_path = temp_dir.path().join("dir").display().to_string(); + let user = whoami::username(); - let err = create_directory_with_user_group("/tmp/wrong_group_dir", &user, "test", 0o775) - .unwrap_err(); + let err = + create_directory_with_user_group(dir_path.as_str(), &user, "test", 0o775).unwrap_err(); assert!(err.to_string().contains("Group not found")); - fs::remove_dir("/tmp/wrong_group_dir").unwrap(); + } + + #[test] + fn change_file_permissions() { + let temp_dir = TempDir::new().unwrap(); + let file_path = temp_dir.path().join("file").display().to_string(); + + let user = whoami::username(); + let _ = create_file_with_user_group(file_path.as_str(), &user, &user, 0o644).unwrap(); + assert!(Path::new(file_path.as_str()).exists()); + + let meta = fs::metadata(file_path.as_str()).unwrap(); + let perm = meta.permissions(); + assert!(format!("{:o}", perm.mode()).contains("644")); + + let file_permissions = FilePermissions::new(None, None, Some(0o444)); + let () = file_permissions + .change_permissions(file_path.as_str()) + .unwrap(); + + let meta = fs::metadata(file_path.as_str()).unwrap(); + let perm = meta.permissions(); + assert!(format!("{:o}", perm.mode()).contains("444")); + } + + #[test] + fn verify_get_file_name() { + assert_eq!( + get_filename(PathBuf::from("/it/is/file.txt")), + Some("file.txt".to_string()) + ); + assert_eq!(get_filename(PathBuf::from("/")), None); } } diff --git a/plugins/c8y_configuration_plugin/src/config.rs b/plugins/c8y_configuration_plugin/src/config.rs index 839eab6d..0c42a804 100644 --- a/plugins/c8y_configuration_plugin/src/config.rs +++ b/plugins/c8y_configuration_plugin/src/config.rs @@ -7,6 +7,7 @@ use std::collections::HashSet; use std::fs; use std::hash::{Hash, Hasher}; use std::path::Path; +use tedge_utils::file::FilePermissions; use tracing::{info, warn}; #[derive(Deserialize, Debug, Default)] @@ -21,6 +22,9 @@ pub struct RawFileEntry { pub path: String, #[serde(rename = "type")] config_type: Option<String>, + user: Option<String>, + group: Option<String>, + mode: Option<u32>, } #[derive(Debug, Eq, PartialEq, Default, Clone)] @@ -32,6 +36,7 @@ pub struct PluginConfig { pub struct FileEntry { pub path: String, config_type: String, + pub file_permissions: FilePermissions, } impl Hash for FileEntry { @@ -53,8 +58,18 @@ impl Borrow<String> for FileEntry { } impl FileEntry { - pub fn new(path: String, config_type: String) -> Self { - Self { path, config_type } + pub fn new( + path: String, + config_type: String, + user: Option<String>, + group: Option<String>, + mode: Option<u32>, + ) -> Self { + Self { + path, + config_type, + file_permissions: FilePermissions { user, group, mode }, + } } } @@ -65,16 +80,13 @@ impl RawPluginConfig { match fs::read_to_string(config_file_path) { Ok(contents) => match toml::from_str(contents.as_str()) { Ok(config) => config, - _ => { - warn!("The config file {} is malformed.", path_str); + Err(err) => { + warn!("The config file {path_str} is malformed. {err}"); Self::default() } }, - Err(_) => { - warn!( - "The config file {} does not exist or is not readable.", - path_str - ); + Err(err) => { + warn!("The config file {path_str} does not exist or is not readable. {err}"); Self::default() } } @@ -92,6 +104,9 @@ impl PluginConfig { let c8y_configuration_plugin = FileEntry::new( config_file_path.display().to_string(), DEFAULT_PLUGIN_CONFIG_TYPE.into(), + None, + None, + None, ); Self { files: HashSet::from([c8y_configuration_plugin]), @@ -104,7 +119,22 @@ impl PluginConfig { let config_type = raw_entry .config_type .unwrap_or_else(|| raw_entry.path.clone()); - let entry = FileEntry::new(raw_entry.path, config_type.clone()); + + if config_type.contains(&['+', '#']) { + warn!( + "The config type '{}' contains the forbidden characters, '+' or '#'.", + config_type + ); + return original_plugin_config; + } + + let entry = FileEntry::new( + raw_entry.path, + config_type.clone(), + raw_entry.user, + raw_entry.group, + raw_entry.mode, + ); if !self.files.insert(entry) { warn!("The config file has the duplicated type '{}'.", config_type); return original_plugin_config; @@ -125,7 +155,10 @@ impl PluginConfig { .collect::<Vec<_>>() } - pub fn get_path_from_type(&self, config_type: &str) -> Result<String, ConfigManagementError> { + pub fn get_file_entry_from_type( + &self, + config_type: &str, + ) -> Result<FileEntry, ConfigManagementError> { let file_entry = self .files .get(&config_type.to_string()) @@ -133,7 +166,7 @@ impl PluginConfig { config_type: config_type.to_owned(), })? .to_owned(); - Ok(file_entry.path) + Ok(file_entry) } // 119,typeA,typeB,... @@ -181,23 +214,23 @@ mod tests { assert_eq!( config.files, vec![ - RawFileEntry::new( + RawFileEntry::new_with_path_and_type( "/etc/tedge/tedge.toml".to_string(), Some("tedge.toml".to_string()) ), - RawFileEntry::new( + RawFileEntry::new_with_path_and_type( "/etc/tedge/tedge.toml".to_string(), Some("tedge.toml".to_string()) ), - RawFileEntry::new( + RawFileEntry::new_with_path_and_type( "/etc/tedge/mosquitto-conf/c8y-bridge.conf".to_string(), None ), - RawFileEntry::new( + RawFileEntry::new_with_path_and_type( "/etc/tedge/mosquitto-conf/tedge-mosquitto.conf".to_string(), Some("\"double quotation\"".to_string()) ), - RawFileEntry::new( + RawFileEntry::new_with_path_and_type( "/etc/mosquitto/mosquitto.conf".to_string(), Some("'single quotation'".to_string()) ) @@ -223,23 +256,23 @@ mod tests { assert_eq!( config.files, vec![ - RawFileEntry::new( + RawFileEntry::new_with_path_and_type( "/etc/tedge/tedge.toml".to_string(), Some("tedge.toml".to_string()) ), - RawFileEntry::new( + RawFileEntry::new_with_path_and_type( "/etc/tedge/tedge.toml".to_string(), Some("tedge.toml".to_string()) ), - RawFileEntry::new( + RawFileEntry::new_with_path_and_type( "/etc/tedge/mosquitto-conf/c8y-bridge.conf".to_string(), None ), - RawFileEntry::new( + RawFileEntry::new_with_path_and_type( "/etc/tedge/mosquitto-conf/tedge-mosquitto.conf".to_string(), Some("\"double quotation\"".to_string()) ), - RawFileEntry::new( + RawFileEntry::new_with_path_and_type( "/etc/mosquitto/mosquitto.conf".to_string(), Some("'single quotation'".to_string()) ) @@ -257,8 +290,8 @@ mod tests { "#, PluginConfig { files: HashSet::from([ - FileEntry::new("/etc/tedge/tedge.toml".to_string(), "tedge".to_string()), - FileEntry::new("/etc/tedge/mosquitto-conf/c8y-bridge.conf".to_string(), "/etc/tedge/mosquitto-conf/c8y-bridge.conf".to_string()), + FileEntry::new_with_path_and_type("/etc/tedge/tedge.toml".to_string(), "tedge".to_string()), + FileEntry::new_with_path_and_type("/etc/tedge/mosquitto-conf/c8y-bridge.conf".to_string(), "/etc/tedge/mosquitto-conf/c8y-bridge.conf".to_string()), ]) }; "standard case" )] @@ -273,8 +306,8 @@ mod tests { "#, PluginConfig { files: HashSet::from([ - FileEntry::new("/etc/tedge/tedge.toml".to_string(), "tedge".to_string()), - FileEntry::new("/etc/tedge/tedge.toml".to_string(), "tedge2".to_string()), + FileEntry::new_with_path_and_type("/etc/tedge/tedge.toml".to_string(), "tedge".to_string()), + FileEntry::new_with_path_and_type("/etc/tedge/tedge.toml".to_string(), "tedge2".to_string()), ]) }; "file path duplication" )] @@ -288,14 +321,36 @@ mod tests { type = "tedge" "#, PluginConfig { - files: HashSet::new() - }; "file type duplication" + files: HashSet::new() + }; "file type duplication" )] #[test_case( - r#"files = []"#, + r#" + [[files]] + path = "/etc/tedge/tedge.toml" + type = "tedge#" + "#, PluginConfig { files: HashSet::new() } + ;"type contains sharp" + )] + #[test_case( + r#" + [[files]] + path = "/etc/tedge/tedge.toml" + type = "tedge+" + "#, + PluginConfig { + files: HashSet::new() + } + ;"type contains plus" + )] + #[test_case( + r#"files = []"#, + PluginConfig { + files: HashSet::new() + } ;"empty case" )] #[test_case( @@ -338,8 +393,24 @@ mod tests { } impl RawFileEntry { - pub fn new(path: String, config_type: Option<String>) -> Self { - Self { path, config_type } + pub fn new_with_path_and_type(path: String, config_type: Option<String>) -> Self { + Self { + path, + config_type, + user: None, + group: None, + mode: None, + } + } + } + + impl FileEntry { + pub fn new_with_path_and_type(path: String, config_type: String) -> Self { + Self { + path, + config_type, + file_permissions: FilePermissions::default(), + } } } @@ -347,7 +418,7 @@ mod tests { impl PluginConfig { fn add_file_entry(&self, path: String, config_type: String) -> Self { let mut files = self.files.clone(); - let _ = files.insert(FileEntry::new(path, config_type)); + let _ = files.insert(FileEntry::new(path, config_type, None, None, None)); Self { files } } } @@ -357,7 +428,7 @@ mod tests { let temp_dir = TempDir::new()?; let config_root = temp_dir.path().to_path_buf(); let config_file_path = config_root.join(PLUGIN_CONFIG_FILE); - let mut file = std::fs::File::create(config_file_path.as_path())?; + let mut file = fs::File::create(config_file_path.as_path())?; file.write_all(content.as_bytes())?; Ok((temp_dir, config_root)) } @@ -365,7 +436,7 @@ mod tests { #[test] fn get_smartrest_single_type() { let plugin_config = PluginConfig { - files: HashSet::from([FileEntry::new( + files: HashSet::from([FileEntry::new_with_path_and_type( "/path/to/file".to_string(), "typeA".to_string(), )]), @@ -378,9 +449,9 @@ mod tests { fn get_smartrest_multiple_types() { let plugin_config = PluginConfig { files: HashSet::from([ - FileEntry::new("path1".to_string(), "typeA".to_string()), - FileEntry::new("path2".to_string(), "typeB".to_string()), - FileEntry::new("path3".to_string(), "typeC".to_string()), + FileEntry::new_with_path_and_type("path1".to_string(), "typeA".to_string()), + FileEntry::new_with_path_and_type("path2".to_string(), "typeB".to_string()), + FileEntry::new_with_path_and_type("path3".to_string(), "typeC".to_string()), ]), }; let output = plugin_config.to_smartrest_payload(); diff --git a/plugins/c8y_configuration_plugin/src/download.rs b/plugins/c8y_configuration_plugin/src/download.rs index e58ef1f6..9a345385 100644 --- a/plugins/c8y_configuration_plugin/src/download.rs +++ b/plugins/c8y_configuration_plugin/src/download.rs @@ -10,11 +10,11 @@ use c8y_smartrest::smartrest_serializer::{ }; use download::{Auth, DownloadInfo, Downloader}; use mqtt_channel::{Connection, Message, SinkExt, Topic}; -use serde::{Deserialize, Serialize}; use serde_json::json; use std::fs; use std::os::unix::fs::PermissionsExt; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; +use tedge_utils::file::{get_filename, get_metadata, FilePermissions}; use tracing::{info, warn}; pub async fn handle_config_download_request( @@ -27,21 +27,31 @@ pub async fn handle_config_download_request( let executing_message = DownloadConfigFileStatusMessage::executing()?; let () = mqtt_client.published.send(executing_message).await?; - let changed_config_type = smartrest_request.config_type.clone(); - - match download_config_file(plugin_config, smartrest_request, tmp_dir, http_client).await { + let target_config_type = smartrest_request.config_type.clone(); + let target_file_entry = plugin_config.get_file_entry_from_type(&target_config_type)?; + + match download_config_file( + smartrest_request.url.as_str(), + PathBuf::from(&target_file_entry.path), + tmp_dir, + target_file_entry.file_permissions, + http_client, + ) + .await + { Ok(_) => { - info!("The configuration download for '{changed_config_type}' is successful."); + info!("The configuration download for '{target_config_type}' is successful."); let successful_message = DownloadConfigFileStatusMessage::successful(None)?; let () = mqtt_client.published.send(successful_message).await?; - let notification_message = get_file_change_notification_message(changed_config_type); + let notification_message = + get_file_change_notification_message(&target_file_entry.path, &target_config_type); let () = mqtt_client.published.send(notification_message).await?; Ok(()) } Err(err) => { - error!("The configuration download for '{changed_config_type}' is failed.",); + error!("The configuration download for '{target_config_type}' is failed.",); let failed_message = DownloadConfigFileStatusMessage::failed(err.to_string())?; let () = mqtt_client.published.send(failed_message).await?; @@ -51,14 +61,15 @@ pub async fn handle_config_download_request( } async fn download_config_file( - plugin_config: &PluginConfig, - smartrest_request: SmartRestConfigDownloadRequest, + download_url: &str, + file_path: PathBuf, tmp_dir: PathBuf, + file_permissions: FilePermissions, http_client: &mut impl C8YHttpProxy, ) -> Result<(), anyhow::Error> { // Convert smartrest request to config download request struct let mut config_download_request = - ConfigDownloadRequest::try_new(smartrest_request, plugin_config, tmp_dir)?; + ConfigDownloadRequest::try_new(download_url, file_path, tmp_dir, file_permissions)?; // Confirm that the file has write access before any http request attempt let () = config_download_request.has_write_access()?; @@ -81,55 +92,59 @@ async fn download_config_file( Ok(()) } -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub struct ConfigDownloadRequest { pub download_info: DownloadInfo, - pub destination_path: PathBuf, + pub file_path: PathBuf, pub tmp_dir: PathBuf, + pub file_permissions: FilePermissions, pub file_name: String, } impl ConfigDownloadRequest { fn try_new( - request: SmartRestConfigDownloadRequest, - plugin_config: &PluginConfig, + download_url: &str, + file_path: PathBuf, tmp_dir: PathBuf, + file_permissions: FilePermissions, ) -> Result<Self, ConfigManagementError> { - let destination_path_string = plugin_config.get_path_from_type(&request.config_type)?; - let destination_path = PathBuf::from(destination_path_string); - let file_name = Self::get_filename(destination_path.clone())?; + let file_name = get_filename(file_path.clone()).ok_or_else(|| { + ConfigManagementError::FileNameNotFound { + path: file_path.clone(), + } + })?; Ok(Self { download_info: DownloadInfo { - url: request.url, + url: download_url.into(), auth: None, }, - destination_path, + file_path, tmp_dir, + file_permissions, file_name, }) } - fn get_filename(path: PathBuf) -> Result<String, ConfigManagementError> { - let filename = path - .file_name() - .ok_or_else(|| ConfigManagementError::FileNameNotFound { path: path.clone() })? - .to_str() - .ok_or_else(|| ConfigManagementError::InvalidFileName { path: path.clone() })? - .to_string(); - Ok(filename) - } - fn has_write_access(&self) -> Result<(), ConfigManagementError> { // The file does not exist before downloading a file - if !&self.destination_path.is_file() { - return Ok(()); - } - // Need a permission check when the file exists already - let metadata = Self::get_metadata(&self.destination_path)?; + let metadata = + if self.file_path.is_file() { + get_metadata(&self.file_path)? + } else { + // If the file does not exist before downloading file, check the directory perms + let parent_dir = &self.file_path.parent().ok_or_else(|| { + ConfigManagementError::NoWriteAccess { + path: self.file_path.clone(), + } + })?; + get_metadata(parent_dir)? + }; + + // Write permission check if metadata.permissions().readonly() { - Err(error::ConfigManagementError::ReadOnlyFile { - path: self.destination_path.clone(), + Err(ConfigManagementError::NoWriteAccess { + path: self.file_path.clone(), }) } else { Ok(()) @@ -140,19 +155,13 @@ impl ConfigDownloadRequest { Downloader::new(&self.file_name, &None, &self.tmp_dir) } - fn get_metadata(path: &Path) -> Result<std::fs::Metadata, ConfigManagementError> { - fs::metadata(&path).map_err(|_| ConfigManagementError::FileNotAccessible { - path: path.to_path_buf(), - }) - } - fn move_file(&self) -> Result<(), ConfigManagementError> { let src = &self.tmp_dir.join(&self.file_name); - let dest = &self.destination_path; + let dest = &self.file_path; - let original_permission_mode = match self.destination_path.is_file() { + let original_permission_mode = match self.file_path.is_file() { true => { - let metadata = Self::get_metadata(&self.destination_path)?; + let metadata = get_metadata(&self.file_path)?; let mode = metadata.permissions().mode(); Some(mode) } @@ -164,19 +173,22 @@ impl ConfigDownloadRequest { dest: dest.to_path_buf(), })?; - // Change the file permission back to the original one - if let Some(mode) = original_permission_mode { - let mut permissions = Self::get_metadata(&self.destination_path)?.permissions(); - let _ = permissions.set_mode(mode); - let _ = std::fs::set_permissions(&self.destination_path, permissions); - } + let file_permissions = if let Some(mode) = original_permission_mode { + // Use the same file permission as the original one + FilePermissions::new(None, None, Some(mode)) + } else { + // Set the user, group, and mode as given for a new file + self.file_permissions.clone() + }; + + let () = file_permissions.change_permissions(&self.file_path.display().to_string())?; Ok(()) } } -pub fn get_file_change_notification_message(config_type: String) -> Message { - let notification = json!({ "changedFile": config_type }).to_string(); +pub fn get_file_change_notification_message(file_path: &str, config_type: &str) -> Message { + let notification = json!({ "path": file_path }).to_string(); let topic = Topic::new(format!("{CONFIG_CHANGE_TOPIC}/{config_type}").as_str()) .unwrap_or_else(|_err| { warn!("The type cannot be used as a part of the topic name. Using {CONFIG_CHANGE_TOPIC} instead."); @@ -212,29 +224,17 @@ impl TryIntoOperationStatusMessage for DownloadConfigFileStatusMessage { #[cfg(test)] mod tests { use super::*; - |