diff --git a/Cargo.lock b/Cargo.lock index f75c27735..e4b897108 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1755,7 +1755,7 @@ dependencies = [ "anyhow", "formatting", "komodo_client", - "tokio", + "secret_file", ] [[package]] @@ -2871,6 +2871,7 @@ dependencies = [ "resolver_api", "response", "rustls 0.23.32", + "secret_file", "serde", "serde_json", "serde_yaml_ng", @@ -2926,6 +2927,7 @@ dependencies = [ "response", "run_command", "rustls 0.23.32", + "secret_file", "serde", "serde_json", "serde_yaml_ng", @@ -3297,6 +3299,7 @@ dependencies = [ "komodo_client", "pem-rfc7468", "pkcs8", + "secret_file", "serde_json", "snow", "spki", @@ -4661,6 +4664,13 @@ dependencies = [ "zeroize", ] +[[package]] +name = "secret_file" +version = "2.0.0-dev-42" +dependencies = [ + "tokio", +] + [[package]] name = "security-framework" version = "2.11.1" diff --git a/Cargo.toml b/Cargo.toml index bf07d4f86..791929330 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ periphery_client = { path = "client/periphery/rs" } environment_file = { path = "lib/environment_file" } environment = { path = "lib/environment" } interpolate = { path = "lib/interpolate" } +secret_file = { path = "lib/secret_file" } formatting = { path = "lib/formatting" } transport = { path = "lib/transport" } database = { path = "lib/database" } diff --git a/bin/core/Cargo.toml b/bin/core/Cargo.toml index 2719dc34d..5aa0ea07f 100644 --- a/bin/core/Cargo.toml +++ b/bin/core/Cargo.toml @@ -19,6 +19,7 @@ komodo_client = { workspace = true, features = ["mongo"] } periphery_client.workspace = true environment_file.workspace = true interpolate.workspace = true +secret_file.workspace = true formatting.workspace = true transport.workspace = true database.workspace = true diff --git a/bin/core/src/api/execute/action.rs b/bin/core/src/api/execute/action.rs index e86912cff..be781b943 100644 --- a/bin/core/src/api/execute/action.rs +++ b/bin/core/src/api/execute/action.rs @@ -142,15 +142,11 @@ impl Resolve for RunAction { let file = format!("{}.ts", random_string(10)); let path = core_config().action_directory.join(&file); - if let Some(parent) = path.parent() { - fs::create_dir_all(parent) - .await - .with_context(|| format!("Failed to initialize Action file parent directory {parent:?}"))?; - } - - fs::write(&path, contents).await.with_context(|| { - format!("Failed to write action file to {path:?}") - })?; + secret_file::write_async(&path, contents) + .await + .with_context(|| { + format!("Failed to write action file to {path:?}") + })?; let CoreConfig { ssl_enabled, .. } = core_config(); diff --git a/bin/core/src/api/write/build.rs b/bin/core/src/api/write/build.rs index 73265e3c0..063914efc 100644 --- a/bin/core/src/api/write/build.rs +++ b/bin/core/src/api/write/build.rs @@ -272,8 +272,9 @@ async fn write_dockerfile_contents_git( return Ok(update); } - if let Err(e) = - fs::write(&full_path, &contents).await.with_context(|| { + if let Err(e) = secret_file::write_async(&full_path, &contents) + .await + .with_context(|| { format!("Failed to write dockerfile contents to {full_path:?}") }) { diff --git a/bin/core/src/api/write/sync.rs b/bin/core/src/api/write/sync.rs index 01133eb4e..f3603c44c 100644 --- a/bin/core/src/api/write/sync.rs +++ b/bin/core/src/api/write/sync.rs @@ -206,15 +206,7 @@ async fn write_sync_file_contents_on_host( .context("Invalid resource path")?; let full_path = root.join(&resource_path).join(&file_path); - if let Some(parent) = full_path.parent() { - tokio::fs::create_dir_all(parent).await.with_context(|| { - format!( - "Failed to initialize resource file parent directory {parent:?}" - ) - })?; - } - - if let Err(e) = tokio::fs::write(&full_path, &contents) + if let Err(e) = secret_file::write_async(&full_path, &contents) .await .with_context(|| { format!( @@ -491,12 +483,7 @@ impl Resolve for CommitSync { .sync_directory .join(to_path_compatible_name(&sync.name)) .join(&resource_path); - if let Some(parent) = file_path.parent() { - tokio::fs::create_dir_all(parent) - .await - .with_context(|| format!("Failed to initialize resource file parent directory {parent:?}"))?; - }; - if let Err(e) = tokio::fs::write(&file_path, &res.toml) + if let Err(e) = secret_file::write_async(&file_path, &res.toml) .await .with_context(|| { format!("Failed to write resource file to {file_path:?}",) diff --git a/bin/periphery/Cargo.toml b/bin/periphery/Cargo.toml index 236c97c8d..096afc1de 100644 --- a/bin/periphery/Cargo.toml +++ b/bin/periphery/Cargo.toml @@ -20,6 +20,7 @@ periphery_client.workspace = true environment_file.workspace = true environment.workspace = true interpolate.workspace = true +secret_file.workspace = true formatting.workspace = true transport.workspace = true response.workspace = true diff --git a/bin/periphery/src/api/build/helpers.rs b/bin/periphery/src/api/build/helpers.rs index 65d7c4af7..0b74f8048 100644 --- a/bin/periphery/src/api/build/helpers.rs +++ b/bin/periphery/src/api/build/helpers.rs @@ -26,14 +26,7 @@ pub async fn write_dockerfile( .components() .collect::(); - // Ensure parent directory exists - if let Some(parent) = full_dockerfile_path.parent() && !parent.exists() { - tokio::fs::create_dir_all(parent) - .await - .with_context(|| format!("Failed to initialize dockerfile parent directory {parent:?}"))?; - } - - tokio::fs::write(&full_dockerfile_path, dockerfile).await.with_context(|| { + secret_file::write_async(&full_dockerfile_path, dockerfile).await.with_context(|| { format!( "Failed to write dockerfile contents to {full_dockerfile_path:?}" ) @@ -86,12 +79,14 @@ pub async fn parse_secret_args( } // Write the value to file to mount let path = build_dir.join(variable); - tokio::fs::write(&path, value).await.with_context(|| { - format!( - "Failed to write build secret {variable} to {}", - path.display() - ) - })?; + secret_file::write_async(&path, value).await.with_context( + || { + format!( + "Failed to write build secret {variable} to {}", + path.display() + ) + }, + )?; // Extend the command write!( &mut res, diff --git a/bin/periphery/src/api/build/mod.rs b/bin/periphery/src/api/build/mod.rs index c8a6c1a72..4a3101bb7 100644 --- a/bin/periphery/src/api/build/mod.rs +++ b/bin/periphery/src/api/build/mod.rs @@ -106,9 +106,13 @@ impl Resolve for WriteDockerfileContentsToHost { .await .with_context(|| format!("Failed to initialize dockerfile parent directory {parent:?}"))?; } - fs::write(&full_path, contents).await.with_context(|| { - format!("Failed to write dockerfile contents to {full_path:?}") - })?; + secret_file::write_async(&full_path, contents) + .await + .with_context(|| { + format!( + "Failed to write dockerfile contents to {full_path:?}" + ) + })?; Ok(Log::simple( "Write dockerfile to host", format!("dockerfile contents written to {full_path:?}"), diff --git a/bin/periphery/src/api/compose/mod.rs b/bin/periphery/src/api/compose/mod.rs index 67a26c72e..4ec8bd555 100644 --- a/bin/periphery/src/api/compose/mod.rs +++ b/bin/periphery/src/api/compose/mod.rs @@ -1,3 +1,5 @@ +use std::{borrow::Cow, path::PathBuf}; + use anyhow::{Context, anyhow}; use command::{ run_komodo_command, run_komodo_command_with_sanitization, @@ -18,8 +20,6 @@ use periphery_client::api::compose::*; use resolver_api::Resolve; use serde::{Deserialize, Serialize}; use shell_escape::unix::escape; -use std::{borrow::Cow, path::PathBuf}; -use tokio::fs; use crate::{ config::periphery_config, @@ -160,12 +160,6 @@ impl Resolve for GetComposeContentsOnHost { let run_directory = root.join(&run_directory).components().collect::(); - if !run_directory.exists() { - fs::create_dir_all(&run_directory) - .await - .context("Failed to initialize run directory")?; - } - let mut res = GetComposeContentsOnHostResponse::default(); for file in file_paths { @@ -173,11 +167,13 @@ impl Resolve for GetComposeContentsOnHost { .join(&file.path) .components() .collect::(); - match fs::read_to_string(&full_path).await.with_context(|| { - format!( - "Failed to read compose file contents at {full_path:?}" - ) - }) { + match tokio::fs::read_to_string(&full_path).await.with_context( + || { + format!( + "Failed to read compose file contents at {full_path:?}" + ) + }, + ) { Ok(contents) => { // The path we store here has to be the same as incoming file path in the array, // in order for WriteComposeContentsToHost to write to the correct path. @@ -227,17 +223,13 @@ impl Resolve for WriteComposeContentsToHost { .join(file_path) .components() .collect::(); - // Ensure parent directory exists - if let Some(parent) = file_path.parent() { - fs::create_dir_all(&parent) - .await - .with_context(|| format!("Failed to initialize compose file parent directory {parent:?}"))?; - } - fs::write(&file_path, contents).await.with_context(|| { - format!( - "Failed to write compose file contents to {file_path:?}" - ) - })?; + secret_file::write_async(&file_path, contents) + .await + .with_context(|| { + format!( + "Failed to write compose file contents to {file_path:?}" + ) + })?; Ok(Log::simple( "Write contents to host", format!("File contents written to {file_path:?}"), diff --git a/bin/periphery/src/api/compose/write.rs b/bin/periphery/src/api/compose/write.rs index ab1fcabe3..5953d4d91 100644 --- a/bin/periphery/src/api/compose/write.rs +++ b/bin/periphery/src/api/compose/write.rs @@ -354,7 +354,7 @@ async fn write_stack_ui_defined( .components() .collect::(); - fs::write(&file_path, &stack.config.file_contents) + secret_file::write_async(&file_path, &stack.config.file_contents) .await .with_context(|| { format!("Failed to write compose file to {file_path:?}") diff --git a/bin/periphery/src/api/keys.rs b/bin/periphery/src/api/keys.rs index 07e2a4769..cf948fd5b 100644 --- a/bin/periphery/src/api/keys.rs +++ b/bin/periphery/src/api/keys.rs @@ -60,7 +60,7 @@ impl Resolve for RotateCorePublicKey { }; SpkiPublicKey::from(self.public_key) - .write_pem(core_public_key_path)?; + .write_pem_sync(core_public_key_path)?; core_public_keys().refresh(); diff --git a/lib/environment/Cargo.toml b/lib/environment/Cargo.toml index 9d18efe51..0ddb5866a 100644 --- a/lib/environment/Cargo.toml +++ b/lib/environment/Cargo.toml @@ -9,7 +9,7 @@ homepage.workspace = true [dependencies] komodo_client.workspace = true +secret_file.workspace = true formatting.workspace = true # -anyhow.workspace = true -tokio.workspace = true \ No newline at end of file +anyhow.workspace = true \ No newline at end of file diff --git a/lib/environment/src/lib.rs b/lib/environment/src/lib.rs index 0089dae17..0c370666d 100644 --- a/lib/environment/src/lib.rs +++ b/lib/environment/src/lib.rs @@ -32,19 +32,7 @@ pub async fn write_env_file( .collect::>() .join("\n"); - if let Some(parent) = env_file_path.parent() - && let Err(e) = tokio::fs::create_dir_all(parent) - .await - .with_context(|| format!("Failed to initialize environment file parent directory {parent:?}")) - { - logs.push(Log::error( - "Write Environment File", - format_serror(&e.into()), - )); - return None; - } - - if let Err(e) = tokio::fs::write(&env_file_path, contents) + if let Err(e) = secret_file::write_async(&env_file_path, contents) .await .with_context(|| { format!("Failed to write environment file to {env_file_path:?}") diff --git a/lib/git/src/commit.rs b/lib/git/src/commit.rs index f0baecffb..84c31ac68 100644 --- a/lib/git/src/commit.rs +++ b/lib/git/src/commit.rs @@ -7,7 +7,6 @@ use komodo_client::entities::{ RepoExecutionResponse, all_logs_success, update::Log, }; use run_command::async_run_command; -use tokio::fs; use crate::get_commit_hash_log; @@ -35,12 +34,12 @@ pub async fn write_commit_file( .collect::(); if let Some(parent) = full_file_path.parent() { - fs::create_dir_all(parent).await.with_context(|| { + tokio::fs::create_dir_all(parent).await.with_context(|| { format!("Failed to initialize file parent directory {parent:?}") })?; } - fs::write(&full_file_path, contents) + tokio::fs::write(&full_file_path, contents) .await .with_context(|| { format!("Failed to write contents to {full_file_path:?}") diff --git a/lib/noise/Cargo.toml b/lib/noise/Cargo.toml index 6f181fe73..57aec77d1 100644 --- a/lib/noise/Cargo.toml +++ b/lib/noise/Cargo.toml @@ -9,6 +9,7 @@ homepage.workspace = true [dependencies] komodo_client.workspace = true +secret_file.workspace = true # pem-rfc7468.workspace = true serde_json.workspace = true diff --git a/lib/noise/src/key/mod.rs b/lib/noise/src/key/mod.rs index 6fb4d7577..213310f9f 100644 --- a/lib/noise/src/key/mod.rs +++ b/lib/noise/src/key/mod.rs @@ -73,7 +73,7 @@ pub fn generate_write_keys( let path = path.as_ref(); // Generate and write pems to path let keys = EncodedKeyPair::generate()?; - keys.private.write_pem(path)?; - keys.public.write_pem(path.with_extension("pub"))?; + keys.private.write_pem_sync(path)?; + keys.public.write_pem_sync(path.with_extension("pub"))?; Ok(keys) } diff --git a/lib/noise/src/key/private.rs b/lib/noise/src/key/private.rs index 8ebe06828..11c3a13b5 100644 --- a/lib/noise/src/key/private.rs +++ b/lib/noise/src/key/private.rs @@ -42,25 +42,32 @@ impl Pkcs8PrivateKey { ) } - pub fn write_pem>( + pub fn write_pem_sync( &self, - path: P, + path: impl AsRef, ) -> anyhow::Result<()> { let path = path.as_ref(); // Ensure the parent directory exists - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent).with_context(|| { - format!( - "Failed to create private key parent directory {parent:?}" - ) - })?; - } tracing::info!("Writing private key to {path:?}"); - std::fs::write(path, self.as_pem()).with_context(|| { + secret_file::write_sync(path, self.as_pem()).with_context(|| { format!("Failed to write private key pem to {path:?}") }) } + pub async fn write_pem_async( + &self, + path: impl AsRef, + ) -> anyhow::Result<()> { + let path = path.as_ref(); + // Ensure the parent directory exists + tracing::info!("Writing private key to {path:?}"); + secret_file::write_async(path, self.as_pem()) + .await + .with_context(|| { + format!("Failed to write private key pem to {path:?}") + }) + } + /// - For raw bytes: converts to pkcs8 and returns /// - For pkcs8 base64: clones and returns /// - For pkcs8 base64 pem: unwraps and returns diff --git a/lib/noise/src/key/public.rs b/lib/noise/src/key/public.rs index 310beebde..abf56d9a2 100644 --- a/lib/noise/src/key/public.rs +++ b/lib/noise/src/key/public.rs @@ -43,25 +43,30 @@ impl SpkiPublicKey { ) } - pub fn write_pem>( + pub fn write_pem_sync( &self, - path: P, + path: impl AsRef, ) -> anyhow::Result<()> { let path = path.as_ref(); - // Ensure the parent directory exists - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent).with_context(|| { - format!( - "Failed to create private key parent directory {parent:?}" - ) - })?; - } tracing::info!("Writing public key to {path:?}"); - std::fs::write(path, self.as_pem()).with_context(|| { + secret_file::write_sync(path, self.as_pem()).with_context(|| { format!("Failed to write private key pem to {path:?}") }) } + pub async fn write_pem_async( + &self, + path: impl AsRef, + ) -> anyhow::Result<()> { + let path = path.as_ref(); + tracing::info!("Writing public key to {path:?}"); + secret_file::write_async(path, self.as_pem()) + .await + .with_context(|| { + format!("Failed to write private key pem to {path:?}") + }) + } + /// Accepts pem rfc7468 (openssl) or base64 der (second line of rfc7468 pem). pub fn from_maybe_pem( public_key_maybe_pem: &str, diff --git a/lib/secret_file/Cargo.toml b/lib/secret_file/Cargo.toml new file mode 100644 index 000000000..e0c5ec684 --- /dev/null +++ b/lib/secret_file/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "secret_file" +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +homepage.workspace = true + +[dependencies] +tokio.workspace = true \ No newline at end of file diff --git a/lib/secret_file/src/lib.rs b/lib/secret_file/src/lib.rs new file mode 100644 index 000000000..a5473dffb --- /dev/null +++ b/lib/secret_file/src/lib.rs @@ -0,0 +1,64 @@ +use std::path::Path; + +/// Writes data to path, setting permissions to 0600. +/// `std::fs` sync version. +/// +/// Also ensures parent directory exists. +pub fn write_sync( + path: impl AsRef, + contents: impl AsRef<[u8]>, +) -> std::io::Result<()> { + use std::{io::Write, os::unix::fs::OpenOptionsExt}; + + let path = path.as_ref(); + + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + + let mut file = std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + // Only sets mode if file is created. + // This leaves existing permissions intact. + .mode(0o600) + .open(path)?; + + file.write_all(contents.as_ref())?; + file.flush()?; + + Ok(()) +} + +/// Writes data to path, setting permissions to 0600. +/// `tokio::fs` async version. +/// +/// Also ensures parent directory exists. +pub async fn write_async( + path: impl AsRef, + contents: impl AsRef<[u8]>, +) -> std::io::Result<()> { + use tokio::io::AsyncWriteExt; + + let path = path.as_ref(); + + if let Some(parent) = path.parent() { + tokio::fs::create_dir_all(parent).await?; + } + + let mut file = tokio::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + // Only sets mode if file is created. + // This leaves existing permissions intact. + .mode(0o600) + .open(path) + .await?; + + file.write_all(contents.as_ref()).await?; + file.flush().await?; + + Ok(()) +}