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
This commit is contained in:
Ana Hobden 2023-06-23 10:29:47 -07:00 committed by GitHub
parent db316614f2
commit a962b3390b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 1075 additions and 74 deletions

View file

@ -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<StatefulAction<Self>, 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<ActionDescription> {
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<ActionDescription> {
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(())
}
}

View file

@ -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<StatefulAction<Self>, 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<ActionDescription> {
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<ActionDescription> {
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(())
}
}

View file

@ -1,21 +1,25 @@
//! Base [`Action`](crate::action::Action)s that themselves have no other actions as dependencies //! 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_directory;
pub(crate) mod create_file; pub(crate) mod create_file;
pub(crate) mod create_group; pub(crate) mod create_group;
pub(crate) mod create_or_insert_into_file; pub(crate) mod create_or_insert_into_file;
pub(crate) mod create_or_merge_nix_config; pub(crate) mod create_or_merge_nix_config;
pub(crate) mod create_user;
pub(crate) mod delete_user; pub(crate) mod delete_user;
pub(crate) mod fetch_and_unpack_nix; pub(crate) mod fetch_and_unpack_nix;
pub(crate) mod move_unpacked_nix; pub(crate) mod move_unpacked_nix;
pub(crate) mod remove_directory; pub(crate) mod remove_directory;
pub(crate) mod setup_default_profile; pub(crate) mod setup_default_profile;
pub use add_user_to_group::AddUserToGroup;
pub use create_directory::CreateDirectory; pub use create_directory::CreateDirectory;
pub use create_file::CreateFile; pub use create_file::CreateFile;
pub use create_group::CreateGroup; pub use create_group::CreateGroup;
pub use create_or_insert_into_file::CreateOrInsertIntoFile; pub use create_or_insert_into_file::CreateOrInsertIntoFile;
pub use create_or_merge_nix_config::CreateOrMergeNixConfig; pub use create_or_merge_nix_config::CreateOrMergeNixConfig;
pub use create_user::CreateUser;
pub use delete_user::DeleteUser; pub use delete_user::DeleteUser;
pub use fetch_and_unpack_nix::{FetchAndUnpackNix, FetchUrlError}; pub use fetch_and_unpack_nix::{FetchAndUnpackNix, FetchUrlError};
pub use move_unpacked_nix::{MoveUnpackedNix, MoveUnpackedNixError}; pub use move_unpacked_nix::{MoveUnpackedNix, MoveUnpackedNixError};

View file

@ -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<CreateGroup>,
create_users: Vec<StatefulAction<CreateUser>>,
add_users_to_groups: Vec<StatefulAction<AddUserToGroup>>,
}
impl CreateUsersAndGroups {
#[tracing::instrument(level = "debug", skip_all)]
pub async fn plan(settings: CommonSettings) -> Result<StatefulAction<Self>, 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<ActionDescription> {
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<Box<ActionError>> = 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<ActionDescription> {
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)))
}
}
}

View file

@ -4,6 +4,7 @@ pub(crate) mod configure_init_service;
pub(crate) mod configure_nix; pub(crate) mod configure_nix;
pub(crate) mod configure_shell_profile; pub(crate) mod configure_shell_profile;
pub(crate) mod create_nix_tree; pub(crate) mod create_nix_tree;
pub(crate) mod create_users_and_groups;
pub(crate) mod delete_users; pub(crate) mod delete_users;
pub(crate) mod place_nix_configuration; pub(crate) mod place_nix_configuration;
pub(crate) mod provision_nix; pub(crate) mod provision_nix;
@ -12,6 +13,7 @@ pub use configure_init_service::{ConfigureInitService, ConfigureNixDaemonService
pub use configure_nix::ConfigureNix; pub use configure_nix::ConfigureNix;
pub use configure_shell_profile::ConfigureShellProfile; pub use configure_shell_profile::ConfigureShellProfile;
pub use create_nix_tree::CreateNixTree; pub use create_nix_tree::CreateNixTree;
pub use create_users_and_groups::CreateUsersAndGroups;
pub use delete_users::DeleteUsersInGroup; pub use delete_users::DeleteUsersInGroup;
pub use place_nix_configuration::PlaceNixConfiguration; pub use place_nix_configuration::PlaceNixConfiguration;
pub use provision_nix::ProvisionNix; pub use provision_nix::ProvisionNix;

View file

@ -57,6 +57,10 @@ impl PlaceNixConfiguration {
"extra-nix-path".to_string(), "extra-nix-path".to_string(),
"nixpkgs=flake:nixpkgs".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()); settings.insert("auto-allocate-uids".to_string(), "true".to_string());
let create_directory = CreateDirectory::plan(NIX_CONF_FOLDER, None, None, 0o0755, force) let create_directory = CreateDirectory::plan(NIX_CONF_FOLDER, None, None, 0o0755, force)

View file

@ -17,8 +17,6 @@ Place Nix and it's requirements onto the target
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct ProvisionNix { pub struct ProvisionNix {
fetch_nix: StatefulAction<FetchAndUnpackNix>, fetch_nix: StatefulAction<FetchAndUnpackNix>,
delete_users_in_group: Option<StatefulAction<DeleteUsersInGroup>>,
create_group: StatefulAction<CreateGroup>,
create_nix_tree: StatefulAction<CreateNixTree>, create_nix_tree: StatefulAction<CreateNixTree>,
move_unpacked_nix: StatefulAction<MoveUnpackedNix>, move_unpacked_nix: StatefulAction<MoveUnpackedNix>,
} }
@ -34,49 +32,12 @@ impl ProvisionNix {
) )
.await?; .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 create_nix_tree = CreateNixTree::plan().await.map_err(Self::error)?;
let move_unpacked_nix = MoveUnpackedNix::plan(PathBuf::from(SCRATCH_DIR)) let move_unpacked_nix = MoveUnpackedNix::plan(PathBuf::from(SCRATCH_DIR))
.await .await
.map_err(Self::error)?; .map_err(Self::error)?;
Ok(Self { Ok(Self {
fetch_nix, fetch_nix,
delete_users_in_group,
create_group,
create_nix_tree, create_nix_tree,
move_unpacked_nix, move_unpacked_nix,
} }
@ -101,8 +62,6 @@ impl Action for ProvisionNix {
fn execute_description(&self) -> Vec<ActionDescription> { fn execute_description(&self) -> Vec<ActionDescription> {
let Self { let Self {
fetch_nix, fetch_nix,
delete_users_in_group,
create_group,
create_nix_tree, create_nix_tree,
move_unpacked_nix, move_unpacked_nix,
} = &self; } = &self;
@ -110,11 +69,6 @@ impl Action for ProvisionNix {
let mut buf = Vec::default(); let mut buf = Vec::default();
buf.append(&mut fetch_nix.describe_execute()); 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 create_nix_tree.describe_execute());
buf.append(&mut move_unpacked_nix.describe_execute()); buf.append(&mut move_unpacked_nix.describe_execute());
@ -130,14 +84,6 @@ impl Action for ProvisionNix {
Result::<_, ActionError>::Ok(fetch_nix_clone) 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 self.create_nix_tree
.try_execute() .try_execute()
.await .await
@ -158,8 +104,6 @@ impl Action for ProvisionNix {
fn revert_description(&self) -> Vec<ActionDescription> { fn revert_description(&self) -> Vec<ActionDescription> {
let Self { let Self {
fetch_nix, fetch_nix,
delete_users_in_group,
create_group,
create_nix_tree, create_nix_tree,
move_unpacked_nix, move_unpacked_nix,
} = &self; } = &self;
@ -167,11 +111,6 @@ impl Action for ProvisionNix {
let mut buf = Vec::default(); let mut buf = Vec::default();
buf.append(&mut move_unpacked_nix.describe_revert()); buf.append(&mut move_unpacked_nix.describe_revert());
buf.append(&mut create_nix_tree.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.append(&mut fetch_nix.describe_revert());
buf buf
@ -185,16 +124,6 @@ impl Action for ProvisionNix {
errors.push(err) 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 { if let Err(err) = self.create_nix_tree.try_revert().await {
errors.push(err) errors.push(err)
} }

View file

@ -1,7 +1,7 @@
use crate::{ use crate::{
action::{ action::{
base::{CreateDirectory, RemoveDirectory}, base::{CreateDirectory, RemoveDirectory},
common::{ConfigureInitService, ConfigureNix, ProvisionNix}, common::{ConfigureInitService, ConfigureNix, CreateUsersAndGroups, ProvisionNix},
linux::ProvisionSelinux, linux::ProvisionSelinux,
StatefulAction, StatefulAction,
}, },
@ -65,6 +65,12 @@ impl Planner for Linux {
.map_err(PlannerError::Action)? .map_err(PlannerError::Action)?
.boxed(), .boxed(),
); );
plan.push(
CreateUsersAndGroups::plan(self.settings.clone())
.await
.map_err(PlannerError::Action)?
.boxed(),
);
plan.push( plan.push(
ConfigureNix::plan(ShellProfileLocations::default(), &self.settings) ConfigureNix::plan(ShellProfileLocations::default(), &self.settings)
.await .await

View file

@ -9,7 +9,7 @@ use super::ShellProfileLocations;
use crate::{ use crate::{
action::{ action::{
base::RemoveDirectory, base::RemoveDirectory,
common::{ConfigureInitService, ConfigureNix, ProvisionNix}, common::{ConfigureInitService, ConfigureNix, CreateUsersAndGroups, ProvisionNix},
macos::{CreateNixVolume, SetTmutilExclusions}, macos::{CreateNixVolume, SetTmutilExclusions},
StatefulAction, StatefulAction,
}, },
@ -143,6 +143,12 @@ impl Planner for Macos {
.await .await
.map_err(PlannerError::Action)? .map_err(PlannerError::Action)?
.boxed(), .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")]) SetTmutilExclusions::plan(vec![PathBuf::from("/nix/store"), PathBuf::from("/nix/var")])
.await .await
.map_err(PlannerError::Action)? .map_err(PlannerError::Action)?

View file

@ -103,7 +103,7 @@ use tokio::process::Command;
use crate::{ use crate::{
action::{ action::{
base::{CreateDirectory, CreateFile, RemoveDirectory}, base::{CreateDirectory, CreateFile, RemoveDirectory},
common::{ConfigureInitService, ConfigureNix, ProvisionNix}, common::{ConfigureInitService, ConfigureNix, CreateUsersAndGroups, ProvisionNix},
linux::{ linux::{
EnsureSteamosNixDirectory, RevertCleanSteamosNixOffload, StartSystemdUnit, EnsureSteamosNixDirectory, RevertCleanSteamosNixOffload, StartSystemdUnit,
SystemctlDaemonReload, SystemctlDaemonReload,
@ -325,6 +325,10 @@ impl Planner for SteamDeck {
.await .await
.map_err(PlannerError::Action)? .map_err(PlannerError::Action)?
.boxed(), .boxed(),
CreateUsersAndGroups::plan(self.settings.clone())
.await
.map_err(PlannerError::Action)?
.boxed(),
ConfigureNix::plan(shell_profile_locations, &self.settings) ConfigureNix::plan(shell_profile_locations, &self.settings)
.await .await
.map_err(PlannerError::Action)? .map_err(PlannerError::Action)?

View file

@ -94,6 +94,55 @@ pub struct CommonSettings {
)] )]
pub nix_build_group_id: u32, 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 /// The Nix package URL
#[cfg_attr( #[cfg_attr(
feature = "cli", feature = "cli",
@ -189,30 +238,48 @@ impl CommonSettings {
/// The default settings for the given Architecture & Operating System /// The default settings for the given Architecture & Operating System
pub async fn default() -> Result<Self, InstallSettingsError> { pub async fn default() -> Result<Self, InstallSettingsError> {
let url; let url;
let nix_build_user_prefix;
let nix_build_user_id_base;
let nix_build_user_count;
use target_lexicon::{Architecture, OperatingSystem}; use target_lexicon::{Architecture, OperatingSystem};
match (Architecture::host(), OperatingSystem::host()) { match (Architecture::host(), OperatingSystem::host()) {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
(Architecture::X86_64, OperatingSystem::Linux) => { (Architecture::X86_64, OperatingSystem::Linux) => {
url = NIX_X64_64_LINUX_URL; 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")] #[cfg(target_os = "linux")]
(Architecture::X86_32(_), OperatingSystem::Linux) => { (Architecture::X86_32(_), OperatingSystem::Linux) => {
url = NIX_I686_LINUX_URL; 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")] #[cfg(target_os = "linux")]
(Architecture::Aarch64(_), OperatingSystem::Linux) => { (Architecture::Aarch64(_), OperatingSystem::Linux) => {
url = NIX_AARCH64_LINUX_URL; 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")] #[cfg(target_os = "macos")]
(Architecture::X86_64, OperatingSystem::MacOSX { .. }) (Architecture::X86_64, OperatingSystem::MacOSX { .. })
| (Architecture::X86_64, OperatingSystem::Darwin) => { | (Architecture::X86_64, OperatingSystem::Darwin) => {
url = NIX_X64_64_DARWIN_URL; 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")] #[cfg(target_os = "macos")]
(Architecture::Aarch64(_), OperatingSystem::MacOSX { .. }) (Architecture::Aarch64(_), OperatingSystem::MacOSX { .. })
| (Architecture::Aarch64(_), OperatingSystem::Darwin) => { | (Architecture::Aarch64(_), OperatingSystem::Darwin) => {
url = NIX_AARCH64_DARWIN_URL; 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( return Err(InstallSettingsError::UnsupportedArchitecture(
@ -225,6 +292,9 @@ impl CommonSettings {
modify_profile: true, modify_profile: true,
nix_build_group_name: String::from("nixbld"), nix_build_group_name: String::from("nixbld"),
nix_build_group_id: 30_000, 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()?, nix_package_url: url.parse()?,
proxy: Default::default(), proxy: Default::default(),
extra_conf: Default::default(), extra_conf: Default::default(),
@ -241,6 +311,9 @@ impl CommonSettings {
modify_profile, modify_profile,
nix_build_group_name, nix_build_group_name,
nix_build_group_id, nix_build_group_id,
nix_build_user_prefix,
nix_build_user_id_base,
nix_build_user_count,
nix_package_url, nix_package_url,
proxy, proxy,
extra_conf, extra_conf,
@ -263,6 +336,18 @@ impl CommonSettings {
"nix_build_group_id".into(), "nix_build_group_id".into(),
serde_json::to_value(nix_build_group_id)?, 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( map.insert(
"nix_package_url".into(), "nix_package_url".into(),
serde_json::to_value(nix_package_url)?, serde_json::to_value(nix_package_url)?,

View file

@ -343,6 +343,9 @@
"modify_profile": true, "modify_profile": true,
"nix_build_group_name": "nixbld", "nix_build_group_name": "nixbld",
"nix_build_group_id": 30000, "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", "nix_package_url": "https://releases.nixos.org/nix/nix-2.15.0/nix-2.15.0-x86_64-linux.tar.xz",
"proxy": null, "proxy": null,
"ssl_cert_file": null, "ssl_cert_file": null,

View file

@ -373,6 +373,9 @@
"modify_profile": true, "modify_profile": true,
"nix_build_group_name": "nixbld", "nix_build_group_name": "nixbld",
"nix_build_group_id": 30000, "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", "nix_package_url": "https://releases.nixos.org/nix/nix-2.15.0/nix-2.15.0-x86_64-linux.tar.xz",
"proxy": null, "proxy": null,
"ssl_cert_file": null, "ssl_cert_file": null,

View file

@ -399,6 +399,9 @@
"modify_profile": true, "modify_profile": true,
"nix_build_group_name": "nixbld", "nix_build_group_name": "nixbld",
"nix_build_group_id": 30000, "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", "nix_package_url": "https://releases.nixos.org/nix/nix-2.15.0/nix-2.15.0-aarch64-darwin.tar.xz",
"proxy": null, "proxy": null,
"ssl_cert_file": null, "ssl_cert_file": null,