diff --git a/src/nix/deployment.rs b/src/nix/deployment.rs index 02f4f7d..9ade322 100644 --- a/src/nix/deployment.rs +++ b/src/nix/deployment.rs @@ -284,7 +284,7 @@ impl Deployment { task.log("Uploading keys..."); - if let Err(e) = target.host.upload_keys(&target.config.keys).await { + if let Err(e) = target.host.upload_keys(&target.config.keys, true).await { task.failure_err(&e); let mut results = arc_self.results.lock().await; @@ -537,7 +537,7 @@ impl Deployment { if self.options.upload_keys && !pre_activation_keys.is_empty() { bar.log("Uploading keys..."); - if let Err(e) = target.host.upload_keys(&pre_activation_keys).await { + if let Err(e) = target.host.upload_keys(&pre_activation_keys, false).await { bar.failure_err(&e); let mut results = self.results.lock().await; @@ -561,7 +561,7 @@ impl Deployment { if self.options.upload_keys && !post_activation_keys.is_empty() { bar.log("Uploading keys (post-activation)..."); - if let Err(e) = target.host.upload_keys(&post_activation_keys).await { + if let Err(e) = target.host.upload_keys(&post_activation_keys, true).await { bar.failure_err(&e); let mut results = self.results.lock().await; diff --git a/src/nix/eval.nix b/src/nix/eval.nix index f32268c..6c9c052 100644 --- a/src/nix/eval.nix +++ b/src/nix/eval.nix @@ -348,10 +348,56 @@ let lib.optional (length remainingKeys != 0) "The following Nixpkgs configuration keys set in meta.nixpkgs will be ignored: ${toString remainingKeys}"; }; + + # Change the ownership of all keys uploaded pre-activation + # + # This is built as part of the system profile. + # We must be careful not to access `text` / `keyCommand` / `keyFile` here + keyChownModule = { lib, config, ... }: let + preActivationKeys = lib.filterAttrs (name: key: key.uploadAt == "pre-activation") config.deployment.keys; + scriptDeps = if config.system.activationScripts ? groups then [ "groups" ] else [ "users" ]; + + commands = lib.mapAttrsToList (name: key: let + keyPath = "${key.destDir}/${name}"; + in '' + if [ -f "${keyPath}" ]; then + if ! chown ${key.user}:${key.group} "${keyPath}"; then + # Error should be visible in stderr + failed=1 + fi + else + >&2 echo "Key ${keyPath} does not exist. Skipping chown." + fi + '') preActivationKeys; + + script = lib.stringAfter scriptDeps '' + # This script is injected by Colmena to change the ownerships + # of keys (`deployment.keys`) deployed before system activation. + + >&2 echo "setting up key ownerships..." + + # We set the ownership of as many keys as possible before failing + failed= + + ${concatStringsSep "\n" commands} + + if [ -n "$failed" ]; then + >&2 echo "Failed to set the ownership of some keys." + + # The activation script has a trap to handle failed + # commands and print out various debug information. + # Let's trigger that instead of `exit 1`. + false + fi + ''; + in { + system.activationScripts.colmena-chown-keys = lib.mkIf (length commands != 0) script; + }; in evalConfig { modules = [ assertionModule nixpkgsModule + keyChownModule deploymentOptions hive.defaults config diff --git a/src/nix/host/key_uploader.rs b/src/nix/host/key_uploader.rs index b3ce0c5..252bc16 100644 --- a/src/nix/host/key_uploader.rs +++ b/src/nix/host/key_uploader.rs @@ -18,12 +18,13 @@ use crate::util::capture_stream; const SCRIPT_TEMPLATE: &'static str = include_str!("./key_uploader.template.sh"); -pub fn generate_script<'a>(key: &'a Key, destination: &'a Path) -> Cow<'a, str> { +pub fn generate_script<'a>(key: &'a Key, destination: &'a Path, require_ownership: bool) -> Cow<'a, str> { let key_script = SCRIPT_TEMPLATE.to_string() .replace("%DESTINATION%", destination.to_str().unwrap()) .replace("%USER%", &escape(key.user().into())) .replace("%GROUP%", &escape(key.group().into())) .replace("%PERMISSIONS%", &escape(key.permissions().into())) + .replace("%REQUIRE_OWNERSHIP%", if require_ownership { "1" } else { "" }) .trim_end_matches('\n').to_string(); escape(key_script.into()) diff --git a/src/nix/host/key_uploader.template.sh b/src/nix/host/key_uploader.template.sh index a0f94d2..3751cc8 100644 --- a/src/nix/host/key_uploader.template.sh +++ b/src/nix/host/key_uploader.template.sh @@ -5,11 +5,12 @@ tmp="${destination}.tmp" user=%USER% group=%GROUP% permissions=%PERMISSIONS% +require_ownership=%REQUIRE_OWNERSHIP% mkdir -p $(dirname "$destination") touch "$tmp" -if getent passwd "$user" >/dev/null && getent group "$group" >/dev/null; then +if [ -n "$require_ownership" ] || getent passwd "$user" >/dev/null && getent group "$group" >/dev/null; then chown "$user:$group" "$tmp" else >&2 echo "User $user and/or group $group do not exist. Skipping chown." diff --git a/src/nix/host/local.rs b/src/nix/host/local.rs index 28c2342..6402443 100644 --- a/src/nix/host/local.rs +++ b/src/nix/host/local.rs @@ -61,9 +61,9 @@ impl Host for Local { Err(e) => Err(e), } } - async fn upload_keys(&mut self, keys: &HashMap) -> NixResult<()> { + async fn upload_keys(&mut self, keys: &HashMap, require_ownership: bool) -> NixResult<()> { for (name, key) in keys { - self.upload_key(&name, &key).await?; + self.upload_key(&name, &key, require_ownership).await?; } Ok(()) @@ -109,11 +109,11 @@ impl Host for Local { impl Local { /// "Uploads" a single key. - async fn upload_key(&mut self, name: &str, key: &Key) -> NixResult<()> { + async fn upload_key(&mut self, name: &str, key: &Key, require_ownership: bool) -> NixResult<()> { self.progress_bar.log(&format!("Deploying key {}", name)); let dest_path = key.dest_dir().join(name); - let key_script = format!("'{}'", key_uploader::generate_script(key, &dest_path)); + let key_script = format!("'{}'", key_uploader::generate_script(key, &dest_path, require_ownership)); let mut command = Command::new("sh"); diff --git a/src/nix/host/mod.rs b/src/nix/host/mod.rs index 2768063..985a989 100644 --- a/src/nix/host/mod.rs +++ b/src/nix/host/mod.rs @@ -96,9 +96,13 @@ pub trait Host: Send + Sync + std::fmt::Debug { Ok(()) } - #[allow(unused_variables)] /// Uploads a set of keys to the host. - async fn upload_keys(&mut self, keys: &HashMap) -> NixResult<()> { + /// + /// If `require_ownership` is false, then the ownership of a key + /// will not be applied if the specified user/group does not + /// exist. + #[allow(unused_variables)] + async fn upload_keys(&mut self, keys: &HashMap, require_ownership: bool) -> NixResult<()> { Err(NixError::Unsupported) } diff --git a/src/nix/host/ssh.rs b/src/nix/host/ssh.rs index 4aa1d97..d143b63 100644 --- a/src/nix/host/ssh.rs +++ b/src/nix/host/ssh.rs @@ -53,9 +53,9 @@ impl Host for Ssh { Err(e) => Err(e), } } - async fn upload_keys(&mut self, keys: &HashMap) -> NixResult<()> { + async fn upload_keys(&mut self, keys: &HashMap, require_ownership: bool) -> NixResult<()> { for (name, key) in keys { - self.upload_key(&name, &key).await?; + self.upload_key(&name, &key, require_ownership).await?; } Ok(()) @@ -227,11 +227,11 @@ impl Ssh { } /// Uploads a single key. - async fn upload_key(&mut self, name: &str, key: &Key) -> NixResult<()> { + async fn upload_key(&mut self, name: &str, key: &Key, require_ownership: bool) -> NixResult<()> { self.progress_bar.log(&format!("Deploying key {}", name)); let dest_path = key.dest_dir().join(name); - let key_script = key_uploader::generate_script(key, &dest_path); + let key_script = key_uploader::generate_script(key, &dest_path, require_ownership); let mut command = self.ssh(&["sh", "-c", &key_script]);