Skip to content

Commit

Permalink
Define a User type for holding user provisioning details
Browse files Browse the repository at this point in the history
The list of supplementary groups isn't portable and needs to be exposed
in the public API somehow. Rather than expand the Provision API to
include it directly, add a User type that holds the username, password,
ssh keys, and supplementary group information and accept that in the
provision interface.

To start with I left the default group list at "wheel" since it covers
most distributions and if the user is in wheel they can add themselves
to the other groups if they need it later.
  • Loading branch information
jeremycline committed Jul 9, 2024
1 parent 966ae29 commit 3b4b94b
Show file tree
Hide file tree
Showing 7 changed files with 103 additions and 69 deletions.
1 change: 0 additions & 1 deletion libazureinit/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
3 changes: 2 additions & 1 deletion libazureinit/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
45 changes: 17 additions & 28 deletions libazureinit/src/provision/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand All @@ -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<PublicKeys>,
password: Option<String>,
user: user::User,
hostname_backends: Option<Vec<hostname::Provisioner>>,
user_backends: Option<Vec<user::Provisioner>>,
password_backends: Option<Vec<password::Provisioner>>,
}

impl Provision {
pub fn new(
hostname: impl Into<String>,
username: impl Into<String>,
ssh_keys: impl Into<Vec<PublicKeys>>,
) -> Self {
pub fn new(hostname: impl Into<String>, 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,
}
}

Expand Down Expand Up @@ -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> {
Expand All @@ -99,7 +88,7 @@ impl Provision {
.iter()
.find_map(|backend| {
backend
.create(&self.username)
.create(&self.user)
.map_err(|e| {
tracing::info!(
error=?e,
Expand All @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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();
Expand Down
20 changes: 9 additions & 11 deletions libazureinit/src/provision/password.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -16,26 +16,24 @@ pub enum Provisioner {
}

impl Provisioner {
pub(crate) fn set(
&self,
username: impl AsRef<str>,
password: impl AsRef<str>,
) -> 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(()),
}
}
}

#[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(),
Expand Down
64 changes: 57 additions & 7 deletions libazureinit/src/provision/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
pub(crate) ssh_keys: Vec<PublicKeys>,
pub(crate) password: Option<String>,
}

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<String>,
ssh_keys: impl Into<Vec<PublicKeys>>,
) -> 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<String>) -> 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<Vec<String>>) -> Self {
self.groups = groups.into();
self
}
}

#[derive(strum::EnumIter, Debug, Clone)]
#[non_exhaustive]
Expand All @@ -16,28 +66,28 @@ pub enum Provisioner {
}

impl Provisioner {
pub(crate) fn create(&self, name: impl AsRef<str>) -> 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(()),
}
}
}

#[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")
Expand Down
35 changes: 16 additions & 19 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions tests/functional_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

use libazureinit::imds::PublicKeys;
use libazureinit::User;
use libazureinit::{
goalstate,
reqwest::{header, Client},
Expand Down Expand Up @@ -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");
Expand Down

0 comments on commit 3b4b94b

Please sign in to comment.