diff --git a/libazureinit/build.rs b/libazureinit/build.rs index 9f7fe22..b27de5f 100644 --- a/libazureinit/build.rs +++ b/libazureinit/build.rs @@ -6,6 +6,5 @@ fn main() { println!("cargo:rustc-env=PATH_HOSTNAMECTL=hostnamectl"); println!("cargo:rustc-env=PATH_USERADD=useradd"); // The list of supplementary groups to add a provisioned user to. - println!("cargo:rustc-env=USERADD_GROUPS=adm,audio,cdrom,dialout,dip,floppy,lxd,netdev,plugdev,sudo,video"); println!("cargo:rustc-env=PATH_PASSWD=passwd"); } diff --git a/libazureinit/src/lib.rs b/libazureinit/src/lib.rs index a88764f..d5b5b67 100644 --- a/libazureinit/src/lib.rs +++ b/libazureinit/src/lib.rs @@ -10,7 +10,8 @@ mod provision; pub use provision::{ hostname::Provisioner as HostnameProvisioner, password::Provisioner as PasswordProvisioner, - user::Provisioner as UserProvisioner, Provision, + user::{Provisioner as UserProvisioner, User}, + Provision, }; // Re-export as the Client is used in our API. diff --git a/libazureinit/src/provision/mod.rs b/libazureinit/src/provision/mod.rs index 5bff6ff..725f861 100644 --- a/libazureinit/src/provision/mod.rs +++ b/libazureinit/src/provision/mod.rs @@ -8,7 +8,7 @@ pub mod user; use strum::IntoEnumIterator; use tracing::instrument; -use crate::{error::Error, imds::PublicKeys}; +use crate::error::Error; /// The interface for applying the desired configuration to the host. /// @@ -20,28 +20,23 @@ use crate::{error::Error, imds::PublicKeys}; /// etc). /// /// To actually apply the configuration, use [`Provision::provision`]. -#[derive(Default, Clone)] +#[derive(Clone)] pub struct Provision { hostname: String, - username: String, - keys: Vec, - password: Option, + user: user::User, hostname_backends: Option>, user_backends: Option>, password_backends: Option>, } impl Provision { - pub fn new( - hostname: impl Into, - username: impl Into, - ssh_keys: impl Into>, - ) -> Self { + pub fn new(hostname: impl Into, user: user::User) -> Self { Self { hostname: hostname.into(), - username: username.into(), - keys: ssh_keys.into(), - ..Default::default() + user, + hostname_backends: None, + user_backends: None, + password_backends: None, } } @@ -85,12 +80,6 @@ impl Provision { self } - /// Set the given password for the provisioned user. - pub fn password(mut self, password: String) -> Self { - self.password = Some(password); - self - } - /// Provision the host. #[instrument(skip_all)] pub fn provision(self) -> Result<(), Error> { @@ -99,7 +88,7 @@ impl Provision { .iter() .find_map(|backend| { backend - .create(&self.username) + .create(&self.user) .map_err(|e| { tracing::info!( error=?e, @@ -118,7 +107,7 @@ impl Provision { .iter() .find_map(|backend| { backend - .set(&self.username, self.password.as_deref().unwrap_or("")) + .set(&self.user) .map_err(|e| { tracing::info!( error=?e, @@ -132,13 +121,13 @@ impl Provision { }) .ok_or(Error::NoPasswordProvisioner)?; - if !self.keys.is_empty() { - let user = nix::unistd::User::from_name(&self.username)?.ok_or( + if !self.user.ssh_keys.is_empty() { + let user = nix::unistd::User::from_name(&self.user.name)?.ok_or( Error::UserMissing { - user: self.username, + user: self.user.name, }, )?; - ssh::provision_ssh(&user, &self.keys)?; + ssh::provision_ssh(&user, &self.user.ssh_keys)?; } self.hostname_backends @@ -167,18 +156,18 @@ impl Provision { #[cfg(test)] mod tests { + use crate::User; + use super::{hostname, password, user, Provision}; #[test] fn test_successful_provision() { let _p = Provision::new( "my-hostname".to_string(), - "my-user".to_string(), - vec![], + User::new("azureuser", vec![]), ) .hostname_provisioners([hostname::Provisioner::FakeHostnamectl]) .user_provisioners([user::Provisioner::FakeUseradd]) - .password("password".to_string()) .password_provisioners([password::Provisioner::FakePasswd]) .provision() .unwrap(); diff --git a/libazureinit/src/provision/password.rs b/libazureinit/src/provision/password.rs index 547d7f0..8f9a173 100644 --- a/libazureinit/src/provision/password.rs +++ b/libazureinit/src/provision/password.rs @@ -5,7 +5,7 @@ use std::process::Command; use tracing::instrument; -use crate::error::Error; +use crate::{error::Error, User}; #[derive(strum::EnumIter, Debug, Clone)] #[non_exhaustive] @@ -16,13 +16,9 @@ pub enum Provisioner { } impl Provisioner { - pub(crate) fn set( - &self, - username: impl AsRef, - password: impl AsRef, - ) -> Result<(), Error> { + pub(crate) fn set(&self, user: &User) -> Result<(), Error> { match self { - Self::Passwd => passwd(username.as_ref(), password.as_ref()), + Self::Passwd => passwd(user), #[cfg(test)] Self::FakePasswd => Ok(()), } @@ -30,12 +26,14 @@ impl Provisioner { } #[instrument(skip_all)] -fn passwd(username: &str, password: &str) -> Result<(), Error> { +fn passwd(user: &User) -> Result<(), Error> { let path_passwd = env!("PATH_PASSWD"); - if password.is_empty() { - let status = - Command::new(path_passwd).arg("-d").arg(username).status()?; + if user.password.is_none() { + let status = Command::new(path_passwd) + .arg("-d") + .arg(&user.name) + .status()?; if !status.success() { return Err(Error::SubprocessFailed { command: path_passwd.to_string(), diff --git a/libazureinit/src/provision/user.rs b/libazureinit/src/provision/user.rs index bc8ecef..35a74f0 100644 --- a/libazureinit/src/provision/user.rs +++ b/libazureinit/src/provision/user.rs @@ -5,7 +5,57 @@ use std::process::Command; use tracing::instrument; -use crate::error::Error; +use crate::{error::Error, imds::PublicKeys}; + +/// Configuration for the provisioned user. +#[derive(Clone)] +pub struct User { + pub(crate) name: String, + pub(crate) groups: Vec, + pub(crate) ssh_keys: Vec, + pub(crate) password: Option, +} + +impl core::fmt::Debug for User { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // This is manually implemented to avoid printing the password if it's set + f.debug_struct("User") + .field("name", &self.name) + .field("groups", &self.groups) + .field("ssh_keys", &self.ssh_keys) + .field("password", &self.password.is_some()) + .finish() + } +} + +impl User { + /// Configure the user being provisioned on the host. + pub fn new( + name: impl Into, + ssh_keys: impl Into>, + ) -> Self { + Self { + name: name.into(), + groups: vec!["wheel".into()], + ssh_keys: ssh_keys.into(), + password: None, + } + } + + /// Set a password for the user; this is optional. + pub fn with_password(mut self, password: impl Into) -> Self { + self.password = Some(password.into()); + self + } + + /// A list of supplemental group names to add the user to. + /// + /// If any of the groups do not exist on the host, provisioning will fail. + pub fn with_groups(mut self, groups: impl Into>) -> Self { + self.groups = groups.into(); + self + } +} #[derive(strum::EnumIter, Debug, Clone)] #[non_exhaustive] @@ -16,9 +66,9 @@ pub enum Provisioner { } impl Provisioner { - pub(crate) fn create(&self, name: impl AsRef) -> Result<(), Error> { + pub(crate) fn create(&self, user: &User) -> Result<(), Error> { match self { - Self::Useradd => useradd(name.as_ref()), + Self::Useradd => useradd(user), #[cfg(test)] Self::FakeUseradd => Ok(()), } @@ -26,18 +76,18 @@ impl Provisioner { } #[instrument(skip_all)] -fn useradd(name: &str) -> Result<(), Error> { +fn useradd(user: &User) -> Result<(), Error> { let path_useradd = env!("PATH_USERADD"); - let home_path = format!("/home/{name}"); + let home_path = format!("/home/{}", user.name); let status = Command::new(path_useradd) - .arg(name) + .arg(&user.name) .arg("--comment") .arg( "Provisioning agent created this user based on username provided in IMDS", ) .arg("--groups") - .arg(env!("USERADD_GROUPS")) + .arg(user.groups.join(",")) .arg("-d") .arg(home_path) .arg("-m") diff --git a/src/main.rs b/src/main.rs index 99466aa..3eb1a77 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ use std::process::ExitCode; use anyhow::Context; use libazureinit::imds::InstanceMetadata; +use libazureinit::User; use libazureinit::{ error::Error as LibError, goalstate, imds, media, @@ -87,26 +88,22 @@ async fn provision() -> Result<(), anyhow::Error> { let instance_metadata = imds::query(&client).await?; let username = get_username(&instance_metadata, &get_environment()?)?; + let user = User::new(username, instance_metadata.compute.public_keys); - Provision::new( - instance_metadata.compute.os_profile.computer_name, - username, - instance_metadata.compute.public_keys, - ) - .hostname_provisioners([ - #[cfg(feature = "hostnamectl")] - HostnameProvisioner::Hostnamectl, - ]) - .user_provisioners([ - #[cfg(feature = "useradd")] - UserProvisioner::Useradd, - ]) - .password("".to_string()) - .password_provisioners([ - #[cfg(feature = "passwd")] - PasswordProvisioner::Passwd, - ]) - .provision()?; + Provision::new(instance_metadata.compute.os_profile.computer_name, user) + .hostname_provisioners([ + #[cfg(feature = "hostnamectl")] + HostnameProvisioner::Hostnamectl, + ]) + .user_provisioners([ + #[cfg(feature = "useradd")] + UserProvisioner::Useradd, + ]) + .password_provisioners([ + #[cfg(feature = "passwd")] + PasswordProvisioner::Passwd, + ]) + .provision()?; let vm_goalstate = goalstate::get_goalstate(&client) .await diff --git a/tests/functional_tests.rs b/tests/functional_tests.rs index 3cfde94..f49256a 100644 --- a/tests/functional_tests.rs +++ b/tests/functional_tests.rs @@ -2,6 +2,7 @@ // Licensed under the MIT License. use libazureinit::imds::PublicKeys; +use libazureinit::User; use libazureinit::{ goalstate, reqwest::{header, Client}, @@ -66,10 +67,9 @@ async fn main() { }, ]; - Provision::new("my-hostname".to_string(), username.to_string(), keys) + Provision::new("my-hostname".to_string(), User::new(username, keys)) .hostname_provisioners([HostnameProvisioner::Hostnamectl]) .user_provisioners([UserProvisioner::Useradd]) - .password("".to_string()) .password_provisioners([PasswordProvisioner::Passwd]) .provision() .expect("Failed to provision host");