From a962b3390be199f3a3a165101327ec6b0496d220 Mon Sep 17 00:00:00 2001 From: Ana Hobden Date: Fri, 23 Jun 2023 10:29:47 -0700 Subject: [PATCH] Restore user creation on Mac (#524) * Restore user creation on Mac * Repair MacOS build * Pass clone not borrow * Fixup double group create * Add some links --- src/action/base/add_user_to_group.rs | 330 ++++++++++++++++++ src/action/base/create_user.rs | 344 +++++++++++++++++++ src/action/base/mod.rs | 4 + src/action/common/create_users_and_groups.rs | 278 +++++++++++++++ src/action/common/mod.rs | 2 + src/action/common/place_nix_configuration.rs | 4 + src/action/common/provision_nix.rs | 71 ---- src/planner/linux.rs | 8 +- src/planner/macos.rs | 8 +- src/planner/steam_deck.rs | 6 +- src/settings.rs | 85 +++++ tests/fixtures/linux/linux.json | 3 + tests/fixtures/linux/steam-deck.json | 3 + tests/fixtures/macos/macos.json | 3 + 14 files changed, 1075 insertions(+), 74 deletions(-) create mode 100644 src/action/base/add_user_to_group.rs create mode 100644 src/action/base/create_user.rs create mode 100644 src/action/common/create_users_and_groups.rs diff --git a/src/action/base/add_user_to_group.rs b/src/action/base/add_user_to_group.rs new file mode 100644 index 0000000..bf88b37 --- /dev/null +++ b/src/action/base/add_user_to_group.rs @@ -0,0 +1,330 @@ +use std::process::Stdio; + +use nix::unistd::User; +use target_lexicon::OperatingSystem; +use tokio::process::Command; +use tracing::{span, Span}; + +use crate::action::{ActionError, ActionErrorKind}; +use crate::execute_command; + +use crate::action::{Action, ActionDescription, StatefulAction}; + +/** +Create an operating system level user in the given group +*/ +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct AddUserToGroup { + name: String, + uid: u32, + groupname: String, + gid: u32, +} + +impl AddUserToGroup { + #[tracing::instrument(level = "debug", skip_all)] + pub async fn plan( + name: String, + uid: u32, + groupname: String, + gid: u32, + ) -> Result, ActionError> { + let this = Self { + name: name.clone(), + uid, + groupname, + gid, + }; + + match OperatingSystem::host() { + OperatingSystem::MacOSX { .. } | OperatingSystem::Darwin => (), + _ => { + if !(which::which("addgroup").is_ok() || which::which("gpasswd").is_ok()) { + return Err(Self::error(ActionErrorKind::MissingAddUserToGroupCommand)); + } + if !(which::which("delgroup").is_ok() || which::which("gpasswd").is_ok()) { + return Err(Self::error( + ActionErrorKind::MissingRemoveUserFromGroupCommand, + )); + } + }, + } + + // Ensure user does not exists + if let Some(user) = User::from_name(name.as_str()) + .map_err(|e| ActionErrorKind::GettingUserId(name.clone(), e)) + .map_err(Self::error)? + { + if user.uid.as_raw() != uid { + return Err(Self::error(ActionErrorKind::UserUidMismatch( + name.clone(), + user.uid.as_raw(), + uid, + ))); + } + + if user.gid.as_raw() != gid { + return Err(Self::error(ActionErrorKind::UserGidMismatch( + name.clone(), + user.gid.as_raw(), + gid, + ))); + } + + // See if group membership needs to be done + match OperatingSystem::host() { + OperatingSystem::MacOSX { + major: _, + minor: _, + patch: _, + } + | OperatingSystem::Darwin => { + let mut command = Command::new("/usr/sbin/dseditgroup"); + command.process_group(0); + command.args(["-o", "checkmember", "-m"]); + command.arg(&this.name); + command.arg(&this.groupname); + command.stdout(Stdio::piped()); + command.stderr(Stdio::piped()); + tracing::trace!("Executing `{:?}`", command.as_std()); + let output = command + .output() + .await + .map_err(|e| ActionErrorKind::command(&command, e)) + .map_err(Self::error)?; + match output.status.code() { + Some(0) => { + // yes {user} is a member of {groupname} + // Since the user exists, and is already a member of the group, we have truly nothing to do here + tracing::debug!( + "Adding user `{}` to group `{}` already complete", + this.name, + this.groupname + ); + return Ok(StatefulAction::completed(this)); + }, + Some(64) => { + // 64 is the exit code for "Group not found" + tracing::trace!( + "Will add user `{}` to newly created group `{}`", + this.name, + this.groupname + ); + // The group will be created by the installer + () + }, + _ => { + // Some other issue + return Err(Self::error(ActionErrorKind::command_output( + &command, output, + ))); + }, + }; + }, + _ => { + let output = execute_command( + Command::new("groups") + .process_group(0) + .arg(&this.name) + .stdin(std::process::Stdio::null()), + ) + .await + .map_err(Self::error)?; + let output_str = String::from_utf8(output.stdout).map_err(Self::error)?; + let user_in_group = output_str.split(" ").any(|v| v == &this.groupname); + + if user_in_group { + tracing::debug!( + "Adding user `{}` to group `{}` already complete", + this.name, + this.groupname + ); + return Ok(StatefulAction::completed(this)); + } + }, + } + } + + Ok(StatefulAction::uncompleted(this)) + } +} + +#[async_trait::async_trait] +#[typetag::serde(name = "add_user_to_group")] +impl Action for AddUserToGroup { + fn action_tag() -> crate::action::ActionTag { + crate::action::ActionTag("add_user_to_group") + } + fn tracing_synopsis(&self) -> String { + format!( + "Add user `{}` (UID {}) to group `{}` (GID {})", + self.name, self.uid, self.groupname, self.gid + ) + } + + fn tracing_span(&self) -> Span { + span!( + tracing::Level::DEBUG, + "add_user_to_group", + user = self.name, + uid = self.uid, + groupname = self.groupname, + gid = self.gid, + ) + } + + fn execute_description(&self) -> Vec { + vec![ActionDescription::new( + self.tracing_synopsis(), + vec![format!( + "The Nix daemon requires the build users to be in a defined group" + )], + )] + } + + #[tracing::instrument(level = "debug", skip_all)] + async fn execute(&mut self) -> Result<(), ActionError> { + let Self { + name, + uid: _, + groupname, + gid: _, + } = self; + + use target_lexicon::OperatingSystem; + match OperatingSystem::host() { + OperatingSystem::MacOSX { + major: _, + minor: _, + patch: _, + } + | OperatingSystem::Darwin => { + execute_command( + Command::new("/usr/bin/dscl") + .process_group(0) + .args([ + ".", + "-append", + &format!("/Groups/{groupname}"), + "GroupMembership", + ]) + .arg(&name) + .stdin(std::process::Stdio::null()), + ) + .await + .map_err(Self::error)?; + execute_command( + Command::new("/usr/sbin/dseditgroup") + .process_group(0) + .args(["-o", "edit"]) + .arg("-a") + .arg(&name) + .arg("-t") + .arg(&name) + .arg(groupname) + .stdin(std::process::Stdio::null()), + ) + .await + .map_err(Self::error)?; + }, + _ => { + if which::which("gpasswd").is_ok() { + execute_command( + Command::new("gpasswd") + .process_group(0) + .args(["-a"]) + .args([name, groupname]) + .stdin(std::process::Stdio::null()), + ) + .await + .map_err(Self::error)?; + } else if which::which("addgroup").is_ok() { + execute_command( + Command::new("addgroup") + .process_group(0) + .args([name, groupname]) + .stdin(std::process::Stdio::null()), + ) + .await + .map_err(Self::error)?; + } else { + return Err(Self::error(Self::error( + ActionErrorKind::MissingAddUserToGroupCommand, + ))); + } + }, + } + + Ok(()) + } + + fn revert_description(&self) -> Vec { + vec![ActionDescription::new( + format!( + "Remove user `{}` (UID {}) from group {} (GID {})", + self.name, self.uid, self.groupname, self.gid + ), + vec![format!( + "The Nix daemon requires system users it can act as in order to build" + )], + )] + } + + #[tracing::instrument(level = "debug", skip_all)] + async fn revert(&mut self) -> Result<(), ActionError> { + let Self { + name, + uid: _, + groupname, + gid: _, + } = self; + + use target_lexicon::OperatingSystem; + match target_lexicon::OperatingSystem::host() { + OperatingSystem::MacOSX { + major: _, + minor: _, + patch: _, + } + | OperatingSystem::Darwin => { + execute_command( + Command::new("/usr/bin/dscl") + .process_group(0) + .args([".", "-delete", &format!("/Groups/{groupname}"), "users"]) + .arg(&name) + .stdin(std::process::Stdio::null()), + ) + .await + .map_err(Self::error)?; + }, + _ => { + if which::which("gpasswd").is_ok() { + execute_command( + Command::new("gpasswd") + .process_group(0) + .args(["-d"]) + .args([&name.to_string(), &groupname.to_string()]) + .stdin(std::process::Stdio::null()), + ) + .await + .map_err(Self::error)?; + } else if which::which("delgroup").is_ok() { + execute_command( + Command::new("delgroup") + .process_group(0) + .args([name, groupname]) + .stdin(std::process::Stdio::null()), + ) + .await + .map_err(Self::error)?; + } else { + return Err(Self::error( + ActionErrorKind::MissingRemoveUserFromGroupCommand, + )); + } + }, + }; + + Ok(()) + } +} diff --git a/src/action/base/create_user.rs b/src/action/base/create_user.rs new file mode 100644 index 0000000..a1637ad --- /dev/null +++ b/src/action/base/create_user.rs @@ -0,0 +1,344 @@ +use nix::unistd::User; +use target_lexicon::OperatingSystem; +use tokio::process::Command; +use tracing::{span, Span}; + +use crate::action::{ActionError, ActionErrorKind, ActionTag}; +use crate::execute_command; + +use crate::action::{Action, ActionDescription, StatefulAction}; + +/** +Create an operating system level user in the given group +*/ +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct CreateUser { + name: String, + uid: u32, + groupname: String, + gid: u32, + comment: String, +} + +impl CreateUser { + #[tracing::instrument(level = "debug", skip_all)] + pub async fn plan( + name: String, + uid: u32, + groupname: String, + gid: u32, + comment: String, + ) -> Result, ActionError> { + let this = Self { + name: name.clone(), + uid, + groupname, + gid, + comment, + }; + + match OperatingSystem::host() { + OperatingSystem::MacOSX { .. } | OperatingSystem::Darwin => (), + _ => { + if !(which::which("useradd").is_ok() || which::which("adduser").is_ok()) { + return Err(Self::error(ActionErrorKind::MissingUserCreationCommand)); + } + if !(which::which("userdel").is_ok() || which::which("deluser").is_ok()) { + return Err(Self::error(ActionErrorKind::MissingUserDeletionCommand)); + } + }, + } + + // Ensure user does not exists + if let Some(user) = User::from_name(name.as_str()) + .map_err(|e| ActionErrorKind::GettingUserId(name.clone(), e)) + .map_err(Self::error)? + { + if user.uid.as_raw() != uid { + return Err(Self::error(ActionErrorKind::UserUidMismatch( + name.clone(), + user.uid.as_raw(), + uid, + ))); + } + + if user.gid.as_raw() != gid { + return Err(Self::error(ActionErrorKind::UserGidMismatch( + name.clone(), + user.gid.as_raw(), + gid, + ))); + } + + tracing::debug!("Creating user `{}` already complete", this.name); + return Ok(StatefulAction::completed(this)); + } + + Ok(StatefulAction::uncompleted(this)) + } +} + +#[async_trait::async_trait] +#[typetag::serde(name = "create_user")] +impl Action for CreateUser { + fn action_tag() -> ActionTag { + ActionTag("create_user") + } + fn tracing_synopsis(&self) -> String { + format!( + "Create user `{}` (UID {}) in group `{}` (GID {})", + self.name, self.uid, self.groupname, self.gid + ) + } + + fn tracing_span(&self) -> Span { + span!( + tracing::Level::DEBUG, + "create_user", + user = self.name, + uid = self.uid, + groupname = self.groupname, + gid = self.gid, + ) + } + + fn execute_description(&self) -> Vec { + vec![ActionDescription::new( + self.tracing_synopsis(), + vec![format!( + "The Nix daemon requires system users it can act as in order to build" + )], + )] + } + + #[tracing::instrument(level = "debug", skip_all)] + async fn execute(&mut self) -> Result<(), ActionError> { + let Self { + name, + uid, + groupname, + gid, + comment, + } = self; + + use OperatingSystem; + match OperatingSystem::host() { + OperatingSystem::MacOSX { + major: _, + minor: _, + patch: _, + } + | OperatingSystem::Darwin => { + execute_command( + Command::new("/usr/bin/dscl") + .process_group(0) + .args([".", "-create", &format!("/Users/{name}")]) + .stdin(std::process::Stdio::null()), + ) + .await + .map_err(Self::error)?; + execute_command( + Command::new("/usr/bin/dscl") + .process_group(0) + .args([ + ".", + "-create", + &format!("/Users/{name}"), + "UniqueID", + &format!("{uid}"), + ]) + .stdin(std::process::Stdio::null()), + ) + .await + .map_err(Self::error)?; + execute_command( + Command::new("/usr/bin/dscl") + .process_group(0) + .args([ + ".", + "-create", + &format!("/Users/{name}"), + "PrimaryGroupID", + &format!("{gid}"), + ]) + .stdin(std::process::Stdio::null()), + ) + .await + .map_err(Self::error)?; + execute_command( + Command::new("/usr/bin/dscl") + .process_group(0) + .args([ + ".", + "-create", + &format!("/Users/{name}"), + "NFSHomeDirectory", + "/var/empty", + ]) + .stdin(std::process::Stdio::null()), + ) + .await + .map_err(Self::error)?; + execute_command( + Command::new("/usr/bin/dscl") + .process_group(0) + .args([ + ".", + "-create", + &format!("/Users/{name}"), + "UserShell", + "/sbin/nologin", + ]) + .stdin(std::process::Stdio::null()), + ) + .await + .map_err(Self::error)?; + execute_command( + Command::new("/usr/bin/dscl") + .process_group(0) + .args([".", "-create", &format!("/Users/{name}"), "IsHidden", "1"]) + .stdin(std::process::Stdio::null()), + ) + .await + .map_err(Self::error)?; + }, + _ => { + if which::which("useradd").is_ok() { + execute_command( + Command::new("useradd") + .process_group(0) + .args([ + "--home-dir", + "/var/empty", + "--comment", + &comment, + "--gid", + &gid.to_string(), + "--groups", + &gid.to_string(), + "--no-user-group", + "--system", + "--shell", + "/sbin/nologin", + "--uid", + &uid.to_string(), + "--password", + "!", + name, + ]) + .stdin(std::process::Stdio::null()), + ) + .await + .map_err(Self::error)?; + } else if which::which("adduser").is_ok() { + execute_command( + Command::new("adduser") + .process_group(0) + .args([ + "--home", + "/var/empty", + "--gecos", + &comment, + "--ingroup", + groupname, + "--system", + "--shell", + "/sbin/nologin", + "--uid", + &uid.to_string(), + "--disabled-password", + name, + ]) + .stdin(std::process::Stdio::null()), + ) + .await + .map_err(Self::error)?; + } else { + return Err(Self::error(ActionErrorKind::MissingUserCreationCommand)); + } + }, + } + + Ok(()) + } + + fn revert_description(&self) -> Vec { + vec![ActionDescription::new( + format!( + "Delete user `{}` (UID {}) in group {} (GID {})", + self.name, self.uid, self.groupname, self.gid + ), + vec![format!( + "The Nix daemon requires system users it can act as in order to build" + )], + )] + } + + #[tracing::instrument(level = "debug", skip_all)] + async fn revert(&mut self) -> Result<(), ActionError> { + use OperatingSystem; + match OperatingSystem::host() { + OperatingSystem::MacOSX { + major: _, + minor: _, + patch: _, + } + | OperatingSystem::Darwin => { + // MacOS is a "Special" case + // It's only possible to delete users under certain conditions. + // Documentation on https://it.megocollector.com/macos/cant-delete-a-macos-user-with-dscl-resolution/ and http://www.aixperts.co.uk/?p=214 suggested it was a secure token + // That is correct, however it's a bit more nuanced. It appears to be that a user must be graphically logged in for some other user on the system to be deleted. + let mut command = Command::new("/usr/bin/dscl"); + command.args([".", "-delete", &format!("/Users/{}", self.name)]); + command.process_group(0); + command.stdin(std::process::Stdio::null()); + + let output = command + .output() + .await + .map_err(|e| ActionErrorKind::command(&command, e)) + .map_err(Self::error)?; + let stderr = String::from_utf8_lossy(&output.stderr); + match output.status.code() { + Some(0) => (), + Some(40) if stderr.contains("-14120") => { + // The user is on an ephemeral Mac, like detsys uses + // These Macs cannot always delete users, as sometimes there is no graphical login + tracing::warn!("Encountered an exit code 40 with -14120 error while removing user, this is likely because the initial executing user did not have a secure token, or that there was no graphical login session. To delete the user, log in graphically, then run `/usr/bin/dscl . -delete /Users/{}", self.name); + }, + _ => { + // Something went wrong + return Err(Self::error(ActionErrorKind::command_output( + &command, output, + ))); + }, + } + }, + _ => { + if which::which("userdel").is_ok() { + execute_command( + Command::new("userdel") + .process_group(0) + .arg(&self.name) + .stdin(std::process::Stdio::null()), + ) + .await + .map_err(Self::error)?; + } else if which::which("deluser").is_ok() { + execute_command( + Command::new("deluser") + .process_group(0) + .arg(&self.name) + .stdin(std::process::Stdio::null()), + ) + .await + .map_err(Self::error)?; + } else { + return Err(Self::error(ActionErrorKind::MissingUserDeletionCommand)); + } + }, + }; + + Ok(()) + } +} diff --git a/src/action/base/mod.rs b/src/action/base/mod.rs index 4601cc9..71a4c02 100644 --- a/src/action/base/mod.rs +++ b/src/action/base/mod.rs @@ -1,21 +1,25 @@ //! Base [`Action`](crate::action::Action)s that themselves have no other actions as dependencies +pub(crate) mod add_user_to_group; pub(crate) mod create_directory; pub(crate) mod create_file; pub(crate) mod create_group; pub(crate) mod create_or_insert_into_file; pub(crate) mod create_or_merge_nix_config; +pub(crate) mod create_user; pub(crate) mod delete_user; pub(crate) mod fetch_and_unpack_nix; pub(crate) mod move_unpacked_nix; pub(crate) mod remove_directory; pub(crate) mod setup_default_profile; +pub use add_user_to_group::AddUserToGroup; pub use create_directory::CreateDirectory; pub use create_file::CreateFile; pub use create_group::CreateGroup; pub use create_or_insert_into_file::CreateOrInsertIntoFile; pub use create_or_merge_nix_config::CreateOrMergeNixConfig; +pub use create_user::CreateUser; pub use delete_user::DeleteUser; pub use fetch_and_unpack_nix::{FetchAndUnpackNix, FetchUrlError}; pub use move_unpacked_nix::{MoveUnpackedNix, MoveUnpackedNixError}; diff --git a/src/action/common/create_users_and_groups.rs b/src/action/common/create_users_and_groups.rs new file mode 100644 index 0000000..e460734 --- /dev/null +++ b/src/action/common/create_users_and_groups.rs @@ -0,0 +1,278 @@ +use crate::{ + action::{ + base::{AddUserToGroup, CreateGroup, CreateUser}, + Action, ActionDescription, ActionError, ActionErrorKind, ActionTag, StatefulAction, + }, + settings::CommonSettings, +}; +use tracing::{span, Span}; + +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct CreateUsersAndGroups { + nix_build_user_count: u32, + nix_build_group_name: String, + nix_build_group_id: u32, + nix_build_user_prefix: String, + nix_build_user_id_base: u32, + create_group: StatefulAction, + create_users: Vec>, + add_users_to_groups: Vec>, +} + +impl CreateUsersAndGroups { + #[tracing::instrument(level = "debug", skip_all)] + pub async fn plan(settings: CommonSettings) -> Result, ActionError> { + let create_group = CreateGroup::plan( + settings.nix_build_group_name.clone(), + settings.nix_build_group_id, + )?; + let mut create_users = Vec::with_capacity(settings.nix_build_user_count as usize); + let mut add_users_to_groups = Vec::with_capacity(settings.nix_build_user_count as usize); + for index in 1..=settings.nix_build_user_count { + create_users.push( + CreateUser::plan( + format!("{}{index}", settings.nix_build_user_prefix), + settings.nix_build_user_id_base + index, + settings.nix_build_group_name.clone(), + settings.nix_build_group_id, + format!("Nix build user {index}"), + ) + .await + .map_err(Self::error)?, + ); + add_users_to_groups.push( + AddUserToGroup::plan( + format!("{}{index}", settings.nix_build_user_prefix), + settings.nix_build_user_id_base + index, + settings.nix_build_group_name.clone(), + settings.nix_build_group_id, + ) + .await + .map_err(Self::error)?, + ); + } + Ok(Self { + nix_build_user_count: settings.nix_build_user_count, + nix_build_group_name: settings.nix_build_group_name, + nix_build_group_id: settings.nix_build_group_id, + nix_build_user_prefix: settings.nix_build_user_prefix, + nix_build_user_id_base: settings.nix_build_user_id_base, + create_group, + create_users, + add_users_to_groups, + } + .into()) + } +} + +#[async_trait::async_trait] +#[typetag::serde(name = "create_users_and_group")] +impl Action for CreateUsersAndGroups { + fn action_tag() -> ActionTag { + ActionTag("create_users_and_group") + } + fn tracing_synopsis(&self) -> String { + format!( + "Create build users (UID {}-{}) and group (GID {})", + self.nix_build_user_id_base, + self.nix_build_user_id_base + self.nix_build_user_count, + self.nix_build_group_id + ) + } + + fn tracing_span(&self) -> Span { + span!( + tracing::Level::DEBUG, + "create_users_and_group", + nix_build_user_count = self.nix_build_user_count, + nix_build_group_name = self.nix_build_group_name, + nix_build_group_id = self.nix_build_group_id, + nix_build_user_prefix = self.nix_build_user_prefix, + nix_build_user_id_base = self.nix_build_user_id_base, + ) + } + + fn execute_description(&self) -> Vec { + let Self { + nix_build_user_count: _, + nix_build_group_name: _, + nix_build_group_id: _, + nix_build_user_prefix: _, + nix_build_user_id_base: _, + create_group, + create_users, + add_users_to_groups, + } = &self; + + let mut create_users_descriptions = Vec::new(); + for create_user in create_users { + if let Some(val) = create_user.describe_execute().iter().next() { + create_users_descriptions.push(val.description.clone()) + } + } + + let mut add_user_to_group_descriptions = Vec::new(); + for add_user_to_group in add_users_to_groups { + if let Some(val) = add_user_to_group.describe_execute().iter().next() { + add_user_to_group_descriptions.push(val.description.clone()) + } + } + + let mut explanation = vec![ + format!("The Nix daemon requires system users (and a group they share) which it can act as in order to build"), + ]; + if let Some(val) = create_group.describe_execute().iter().next() { + explanation.push(val.description.clone()) + } + explanation.append(&mut create_users_descriptions); + explanation.append(&mut add_user_to_group_descriptions); + + vec![ActionDescription::new(self.tracing_synopsis(), explanation)] + } + + #[tracing::instrument(level = "debug", skip_all)] + async fn execute(&mut self) -> Result<(), ActionError> { + let Self { + create_users, + create_group, + add_users_to_groups, + nix_build_user_count: _, + nix_build_group_name: _, + nix_build_group_id: _, + nix_build_user_prefix: _, + nix_build_user_id_base: _, + } = self; + + // Create group + create_group.try_execute().await?; + + // Mac is apparently not threadsafe here... + use target_lexicon::OperatingSystem; + match OperatingSystem::host() { + OperatingSystem::MacOSX { + major: _, + minor: _, + patch: _, + } + | OperatingSystem::Darwin => { + for create_user in create_users.iter_mut() { + create_user.try_execute().await.map_err(Self::error)?; + } + }, + _ => { + for create_user in create_users.iter_mut() { + create_user.try_execute().await.map_err(Self::error)?; + } + // While we may be tempted to do something like this, it can break on many older OSes like Ubuntu 18.04: + // ``` + // useradd: cannot lock /etc/passwd; try again later. + // ``` + // So, instead, we keep this here in hopes one day we can enable it for some detected OS: + // + // let mut set = JoinSet::new(); + // let mut errors: Vec> = Vec::new(); + // for (idx, create_user) in create_users.iter_mut().enumerate() { + // let span = tracing::Span::current().clone(); + // let mut create_user_clone = create_user.clone(); + // let _abort_handle = set.spawn(async move { + // create_user_clone.try_execute().instrument(span).await?; + // Result::<_, _>::Ok((idx, create_user_clone)) + // }); + // } + + // while let Some(result) = set.join_next().await { + // match result { + // Ok(Ok((idx, success))) => create_users[idx] = success, + // Ok(Err(e)) => errors.push(Box::new(e)), + // Err(e) => return Err(ActionErrorKind::Join(e))?, + // }; + // } + + // if !errors.is_empty() { + // if errors.len() == 1 { + // return Err(errors.into_iter().next().unwrap().into()); + // } else { + // return Err(ActionErrorKind::Children(errors)); + // } + // } + }, + }; + + for add_user_to_group in add_users_to_groups.iter_mut() { + add_user_to_group.try_execute().await.map_err(Self::error)?; + } + + Ok(()) + } + + fn revert_description(&self) -> Vec { + let Self { + nix_build_user_count: _, + nix_build_group_name: _, + nix_build_group_id: _, + nix_build_user_prefix: _, + nix_build_user_id_base: _, + create_group, + create_users, + add_users_to_groups, + } = &self; + let mut create_users_descriptions = Vec::new(); + for create_user in create_users { + if let Some(val) = create_user.describe_revert().iter().next() { + create_users_descriptions.push(val.description.clone()) + } + } + + let mut add_user_to_group_descriptions = Vec::new(); + for add_user_to_group in add_users_to_groups { + if let Some(val) = add_user_to_group.describe_revert().iter().next() { + add_user_to_group_descriptions.push(val.description.clone()) + } + } + + let mut explanation = vec![ + format!("The Nix daemon requires system users (and a group they share) which it can act as in order to build"), + ]; + if let Some(val) = create_group.describe_revert().iter().next() { + explanation.push(val.description.clone()) + } + explanation.append(&mut create_users_descriptions); + explanation.append(&mut add_user_to_group_descriptions); + + vec![ActionDescription::new( + format!("Remove Nix users and group"), + explanation, + )] + } + + #[tracing::instrument(level = "debug", skip_all)] + async fn revert(&mut self) -> Result<(), ActionError> { + let mut errors = vec![]; + for create_user in self.create_users.iter_mut() { + if let Err(err) = create_user.try_revert().await { + errors.push(err); + } + } + + // We don't actually need to do this, when a user is deleted they are removed from groups + // for add_user_to_group in add_users_to_groups.iter_mut() { + // add_user_to_group.try_revert().await?; + // } + + // Create group + if let Err(err) = self.create_group.try_revert().await { + errors.push(err); + } + + if errors.is_empty() { + Ok(()) + } else if errors.len() == 1 { + Err(errors + .into_iter() + .next() + .expect("Expected 1 len Vec to have at least 1 item")) + } else { + Err(Self::error(ActionErrorKind::MultipleChildren(errors))) + } + } +} diff --git a/src/action/common/mod.rs b/src/action/common/mod.rs index f2cb8c9..9ca6198 100644 --- a/src/action/common/mod.rs +++ b/src/action/common/mod.rs @@ -4,6 +4,7 @@ pub(crate) mod configure_init_service; pub(crate) mod configure_nix; pub(crate) mod configure_shell_profile; pub(crate) mod create_nix_tree; +pub(crate) mod create_users_and_groups; pub(crate) mod delete_users; pub(crate) mod place_nix_configuration; pub(crate) mod provision_nix; @@ -12,6 +13,7 @@ pub use configure_init_service::{ConfigureInitService, ConfigureNixDaemonService pub use configure_nix::ConfigureNix; pub use configure_shell_profile::ConfigureShellProfile; pub use create_nix_tree::CreateNixTree; +pub use create_users_and_groups::CreateUsersAndGroups; pub use delete_users::DeleteUsersInGroup; pub use place_nix_configuration::PlaceNixConfiguration; pub use provision_nix::ProvisionNix; diff --git a/src/action/common/place_nix_configuration.rs b/src/action/common/place_nix_configuration.rs index 8a86bfe..d7d09bc 100644 --- a/src/action/common/place_nix_configuration.rs +++ b/src/action/common/place_nix_configuration.rs @@ -57,6 +57,10 @@ impl PlaceNixConfiguration { "extra-nix-path".to_string(), "nixpkgs=flake:nixpkgs".to_string(), ); + + // Auto-allocate uids is broken on Mac. Tools like `whoami` don't work. + // e.g. https://github.com/NixOS/nix/issues/8444 + #[cfg(not(target_os = "macos"))] settings.insert("auto-allocate-uids".to_string(), "true".to_string()); let create_directory = CreateDirectory::plan(NIX_CONF_FOLDER, None, None, 0o0755, force) diff --git a/src/action/common/provision_nix.rs b/src/action/common/provision_nix.rs index abfebe5..0c28278 100644 --- a/src/action/common/provision_nix.rs +++ b/src/action/common/provision_nix.rs @@ -17,8 +17,6 @@ Place Nix and it's requirements onto the target #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct ProvisionNix { fetch_nix: StatefulAction, - delete_users_in_group: Option>, - create_group: StatefulAction, create_nix_tree: StatefulAction, move_unpacked_nix: StatefulAction, } @@ -34,49 +32,12 @@ impl ProvisionNix { ) .await?; - let delete_users_in_group = if let Some(group) = - Group::from_name(settings.nix_build_group_name.as_str()) - .map_err(|e| { - ActionErrorKind::GettingGroupId(settings.nix_build_group_name.clone(), e) - }) - .map_err(Self::error)? - { - if group.gid.as_raw() != settings.nix_build_group_id { - return Err(Self::error(ActionErrorKind::GroupGidMismatch( - settings.nix_build_group_name.clone(), - group.gid.as_raw(), - settings.nix_build_group_id, - ))); - } - if group.mem.is_empty() { - None - } else { - Some( - DeleteUsersInGroup::plan( - settings.nix_build_group_name.clone(), - settings.nix_build_group_id, - group.mem, - ) - .await?, - ) - } - } else { - None - }; - - let create_group = CreateGroup::plan( - settings.nix_build_group_name.clone(), - settings.nix_build_group_id, - ) - .map_err(Self::error)?; let create_nix_tree = CreateNixTree::plan().await.map_err(Self::error)?; let move_unpacked_nix = MoveUnpackedNix::plan(PathBuf::from(SCRATCH_DIR)) .await .map_err(Self::error)?; Ok(Self { fetch_nix, - delete_users_in_group, - create_group, create_nix_tree, move_unpacked_nix, } @@ -101,8 +62,6 @@ impl Action for ProvisionNix { fn execute_description(&self) -> Vec { let Self { fetch_nix, - delete_users_in_group, - create_group, create_nix_tree, move_unpacked_nix, } = &self; @@ -110,11 +69,6 @@ impl Action for ProvisionNix { let mut buf = Vec::default(); buf.append(&mut fetch_nix.describe_execute()); - if let Some(delete_users_in_group) = delete_users_in_group { - buf.append(&mut delete_users_in_group.describe_execute()); - } - - buf.append(&mut create_group.describe_execute()); buf.append(&mut create_nix_tree.describe_execute()); buf.append(&mut move_unpacked_nix.describe_execute()); @@ -130,14 +84,6 @@ impl Action for ProvisionNix { Result::<_, ActionError>::Ok(fetch_nix_clone) }); - if let Some(delete_users_in_group) = &mut self.delete_users_in_group { - delete_users_in_group - .try_execute() - .await - .map_err(Self::error)?; - } - - self.create_group.try_execute().await.map_err(Self::error)?; self.create_nix_tree .try_execute() .await @@ -158,8 +104,6 @@ impl Action for ProvisionNix { fn revert_description(&self) -> Vec { let Self { fetch_nix, - delete_users_in_group, - create_group, create_nix_tree, move_unpacked_nix, } = &self; @@ -167,11 +111,6 @@ impl Action for ProvisionNix { let mut buf = Vec::default(); buf.append(&mut move_unpacked_nix.describe_revert()); buf.append(&mut create_nix_tree.describe_revert()); - buf.append(&mut create_group.describe_revert()); - - if let Some(delete_users_in_group) = delete_users_in_group { - buf.append(&mut delete_users_in_group.describe_execute()); - } buf.append(&mut fetch_nix.describe_revert()); buf @@ -185,16 +124,6 @@ impl Action for ProvisionNix { errors.push(err) } - if let Some(delete_users_in_group) = &mut self.delete_users_in_group { - delete_users_in_group - .try_revert() - .await - .map_err(Self::error)?; - } - - if let Err(err) = self.create_group.try_revert().await { - errors.push(err) - } if let Err(err) = self.create_nix_tree.try_revert().await { errors.push(err) } diff --git a/src/planner/linux.rs b/src/planner/linux.rs index 26b5f39..8f7e906 100644 --- a/src/planner/linux.rs +++ b/src/planner/linux.rs @@ -1,7 +1,7 @@ use crate::{ action::{ base::{CreateDirectory, RemoveDirectory}, - common::{ConfigureInitService, ConfigureNix, ProvisionNix}, + common::{ConfigureInitService, ConfigureNix, CreateUsersAndGroups, ProvisionNix}, linux::ProvisionSelinux, StatefulAction, }, @@ -65,6 +65,12 @@ impl Planner for Linux { .map_err(PlannerError::Action)? .boxed(), ); + plan.push( + CreateUsersAndGroups::plan(self.settings.clone()) + .await + .map_err(PlannerError::Action)? + .boxed(), + ); plan.push( ConfigureNix::plan(ShellProfileLocations::default(), &self.settings) .await diff --git a/src/planner/macos.rs b/src/planner/macos.rs index 077bdbd..6cc1c6d 100644 --- a/src/planner/macos.rs +++ b/src/planner/macos.rs @@ -9,7 +9,7 @@ use super::ShellProfileLocations; use crate::{ action::{ base::RemoveDirectory, - common::{ConfigureInitService, ConfigureNix, ProvisionNix}, + common::{ConfigureInitService, ConfigureNix, CreateUsersAndGroups, ProvisionNix}, macos::{CreateNixVolume, SetTmutilExclusions}, StatefulAction, }, @@ -143,6 +143,12 @@ impl Planner for Macos { .await .map_err(PlannerError::Action)? .boxed(), + // Auto-allocate uids is broken on Mac. Tools like `whoami` don't work. + // e.g. https://github.com/NixOS/nix/issues/8444 + CreateUsersAndGroups::plan(self.settings.clone()) + .await + .map_err(PlannerError::Action)? + .boxed(), SetTmutilExclusions::plan(vec![PathBuf::from("/nix/store"), PathBuf::from("/nix/var")]) .await .map_err(PlannerError::Action)? diff --git a/src/planner/steam_deck.rs b/src/planner/steam_deck.rs index 142a6c2..b6abe0e 100644 --- a/src/planner/steam_deck.rs +++ b/src/planner/steam_deck.rs @@ -103,7 +103,7 @@ use tokio::process::Command; use crate::{ action::{ base::{CreateDirectory, CreateFile, RemoveDirectory}, - common::{ConfigureInitService, ConfigureNix, ProvisionNix}, + common::{ConfigureInitService, ConfigureNix, CreateUsersAndGroups, ProvisionNix}, linux::{ EnsureSteamosNixDirectory, RevertCleanSteamosNixOffload, StartSystemdUnit, SystemctlDaemonReload, @@ -325,6 +325,10 @@ impl Planner for SteamDeck { .await .map_err(PlannerError::Action)? .boxed(), + CreateUsersAndGroups::plan(self.settings.clone()) + .await + .map_err(PlannerError::Action)? + .boxed(), ConfigureNix::plan(shell_profile_locations, &self.settings) .await .map_err(PlannerError::Action)? diff --git a/src/settings.rs b/src/settings.rs index d41bcee..748db4f 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -94,6 +94,55 @@ pub struct CommonSettings { )] pub nix_build_group_id: u32, + /// The Nix build user prefix (user numbers will be postfixed) + #[cfg_attr( + feature = "cli", + clap(long, env = "NIX_INSTALLER_NIX_BUILD_USER_PREFIX", global = true) + )] + #[cfg_attr( + all(target_os = "macos", feature = "cli"), + clap(default_value = "_nixbld") + )] + #[cfg_attr( + all(target_os = "linux", feature = "cli"), + clap(default_value = "nixbld") + )] + pub nix_build_user_prefix: String, + + /// Number of build users to create + #[cfg_attr( + all(target_os = "linux", feature = "cli"), + doc = "On Linux, Nix's `auto-allocate-uids` feature will be enabled, so users don't need to be created." + )] + #[cfg_attr( + feature = "cli", + clap( + long, + alias = "daemon-user-count", + env = "NIX_INSTALLER_NIX_BUILD_USER_COUNT", + global = true + ) + )] + #[cfg_attr(all(target_os = "macos", feature = "cli"), clap(default_value = "32"))] + #[cfg_attr(all(target_os = "linux", feature = "cli"), clap(default_value = "0"))] + pub nix_build_user_count: u32, + + /// The Nix build user base UID (ascending) + #[cfg_attr( + feature = "cli", + clap(long, env = "NIX_INSTALLER_NIX_BUILD_USER_ID_BASE", global = true) + )] + #[cfg_attr( + all(target_os = "macos", feature = "cli"), + doc = "Service users on Mac should be between 200-400" + )] + #[cfg_attr(all(target_os = "macos", feature = "cli"), clap(default_value_t = 300))] + #[cfg_attr( + all(target_os = "linux", feature = "cli"), + clap(default_value_t = 30_000) + )] + pub nix_build_user_id_base: u32, + /// The Nix package URL #[cfg_attr( feature = "cli", @@ -189,30 +238,48 @@ impl CommonSettings { /// The default settings for the given Architecture & Operating System pub async fn default() -> Result { let url; + let nix_build_user_prefix; + let nix_build_user_id_base; + let nix_build_user_count; use target_lexicon::{Architecture, OperatingSystem}; match (Architecture::host(), OperatingSystem::host()) { #[cfg(target_os = "linux")] (Architecture::X86_64, OperatingSystem::Linux) => { url = NIX_X64_64_LINUX_URL; + nix_build_user_prefix = "nixbld"; + nix_build_user_id_base = 30000; + nix_build_user_count = 0; }, #[cfg(target_os = "linux")] (Architecture::X86_32(_), OperatingSystem::Linux) => { url = NIX_I686_LINUX_URL; + nix_build_user_prefix = "nixbld"; + nix_build_user_id_base = 30000; + nix_build_user_count = 0; }, #[cfg(target_os = "linux")] (Architecture::Aarch64(_), OperatingSystem::Linux) => { url = NIX_AARCH64_LINUX_URL; + nix_build_user_prefix = "nixbld"; + nix_build_user_id_base = 30000; + nix_build_user_count = 0; }, #[cfg(target_os = "macos")] (Architecture::X86_64, OperatingSystem::MacOSX { .. }) | (Architecture::X86_64, OperatingSystem::Darwin) => { url = NIX_X64_64_DARWIN_URL; + nix_build_user_prefix = "_nixbld"; + nix_build_user_id_base = 300; + nix_build_user_count = 32; }, #[cfg(target_os = "macos")] (Architecture::Aarch64(_), OperatingSystem::MacOSX { .. }) | (Architecture::Aarch64(_), OperatingSystem::Darwin) => { url = NIX_AARCH64_DARWIN_URL; + nix_build_user_prefix = "_nixbld"; + nix_build_user_id_base = 300; + nix_build_user_count = 32; }, _ => { return Err(InstallSettingsError::UnsupportedArchitecture( @@ -225,6 +292,9 @@ impl CommonSettings { modify_profile: true, nix_build_group_name: String::from("nixbld"), nix_build_group_id: 30_000, + nix_build_user_id_base, + nix_build_user_count, + nix_build_user_prefix: nix_build_user_prefix.to_string(), nix_package_url: url.parse()?, proxy: Default::default(), extra_conf: Default::default(), @@ -241,6 +311,9 @@ impl CommonSettings { modify_profile, nix_build_group_name, nix_build_group_id, + nix_build_user_prefix, + nix_build_user_id_base, + nix_build_user_count, nix_package_url, proxy, extra_conf, @@ -263,6 +336,18 @@ impl CommonSettings { "nix_build_group_id".into(), serde_json::to_value(nix_build_group_id)?, ); + map.insert( + "nix_build_user_prefix".into(), + serde_json::to_value(nix_build_user_prefix)?, + ); + map.insert( + "nix_build_user_id_base".into(), + serde_json::to_value(nix_build_user_id_base)?, + ); + map.insert( + "nix_build_user_count".into(), + serde_json::to_value(nix_build_user_count)?, + ); map.insert( "nix_package_url".into(), serde_json::to_value(nix_package_url)?, diff --git a/tests/fixtures/linux/linux.json b/tests/fixtures/linux/linux.json index 09bcc9f..3ebf63f 100644 --- a/tests/fixtures/linux/linux.json +++ b/tests/fixtures/linux/linux.json @@ -343,6 +343,9 @@ "modify_profile": true, "nix_build_group_name": "nixbld", "nix_build_group_id": 30000, + "nix_build_user_count": 0, + "nix_build_user_prefix": "nixbld", + "nix_build_user_id_base": 30000, "nix_package_url": "https://releases.nixos.org/nix/nix-2.15.0/nix-2.15.0-x86_64-linux.tar.xz", "proxy": null, "ssl_cert_file": null, diff --git a/tests/fixtures/linux/steam-deck.json b/tests/fixtures/linux/steam-deck.json index 700637b..191878f 100644 --- a/tests/fixtures/linux/steam-deck.json +++ b/tests/fixtures/linux/steam-deck.json @@ -373,6 +373,9 @@ "modify_profile": true, "nix_build_group_name": "nixbld", "nix_build_group_id": 30000, + "nix_build_user_count": 0, + "nix_build_user_prefix": "nixbld", + "nix_build_user_id_base": 30000, "nix_package_url": "https://releases.nixos.org/nix/nix-2.15.0/nix-2.15.0-x86_64-linux.tar.xz", "proxy": null, "ssl_cert_file": null, diff --git a/tests/fixtures/macos/macos.json b/tests/fixtures/macos/macos.json index f14b8dc..12b6bb6 100644 --- a/tests/fixtures/macos/macos.json +++ b/tests/fixtures/macos/macos.json @@ -399,6 +399,9 @@ "modify_profile": true, "nix_build_group_name": "nixbld", "nix_build_group_id": 30000, + "nix_build_user_count": 32, + "nix_build_user_prefix": "_nixbld", + "nix_build_user_id_base": 300, "nix_package_url": "https://releases.nixos.org/nix/nix-2.15.0/nix-2.15.0-aarch64-darwin.tar.xz", "proxy": null, "ssl_cert_file": null,