use anyhow::{Context, bail};
use nix::unistd::{Gid, Group, Uid, User, chown, setgid, setgroups, setuid};
use std::path::Path;
pub fn prepare_state_dir(
path: &Path,
user: &str,
group: Option<&str>,
) -> anyhow::Result<()> {
if !nix::unistd::getuid().is_root() {
return Ok(());
}
std::fs::create_dir_all(path)
.with_context(|| format!("creating {}", path.display()))?;
let (uid, gid) = resolve_ids(user, group)?;
chown(path, Some(uid), Some(gid))
.with_context(|| format!("chown {}", path.display()))?;
tracing::info!(
path = %path.display(),
user,
"prepared state directory"
);
Ok(())
}
fn resolve_ids(
user: &str,
group: Option<&str>,
) -> anyhow::Result<(Uid, Gid)> {
let pw = User::from_name(user)
.context("looking up user")?
.ok_or_else(|| anyhow::anyhow!("user '{user}' not found"))?;
let gid: Gid = if let Some(name) = group {
Group::from_name(name)
.context("looking up group")?
.ok_or_else(|| anyhow::anyhow!("group '{name}' not found"))?
.gid
} else {
pw.gid
};
Ok((pw.uid, gid))
}
pub fn drop_privileges(
user: &str,
group: Option<&str>,
inherit_supplementary_groups: bool,
) -> anyhow::Result<()> {
if !nix::unistd::getuid().is_root() {
return Ok(());
}
let (uid, gid) = resolve_ids(user, group)?;
if inherit_supplementary_groups {
tracing::warn!(
"inherit-supplementary-groups enabled: \
supplementary groups are NOT cleared"
);
} else {
setgroups(&[gid]).context("setgroups")?;
}
setgid(gid).context("setgid")?;
setuid(uid).context("setuid")?;
if setuid(Uid::from_raw(0)).is_ok() {
bail!("setuid(0) succeeded after privilege drop -- aborting");
}
tracing::info!(
user,
uid = uid.as_raw(),
gid = gid.as_raw(),
inherit_supplementary_groups,
"dropped privileges"
);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn current_user() -> User {
User::from_uid(nix::unistd::getuid()).unwrap().unwrap()
}
#[test]
fn drop_privileges_is_noop_when_not_root() {
drop_privileges("no-such-user-zz", None, false).unwrap();
}
#[test]
fn prepare_state_dir_is_noop_when_not_root() {
let dir = std::env::temp_dir()
.join("hypershunt-privdrop-test-never-created");
let _ = std::fs::remove_dir_all(&dir);
prepare_state_dir(&dir, "no-such-user-zz", None).unwrap();
assert!(!dir.exists(), "no-op must not create the directory");
}
#[test]
fn resolve_ids_finds_current_user_primary_gid() {
let me = current_user();
let (uid, gid) = resolve_ids(&me.name, None).unwrap();
assert_eq!(uid, me.uid);
assert_eq!(gid, me.gid, "None group must fall back to primary");
}
#[test]
fn resolve_ids_honors_explicit_group() {
let me = current_user();
let group = Group::from_gid(me.gid).unwrap().unwrap();
let (uid, gid) =
resolve_ids(&me.name, Some(&group.name)).unwrap();
assert_eq!(uid, me.uid);
assert_eq!(gid, group.gid);
}
#[test]
fn resolve_ids_rejects_unknown_user() {
let err = resolve_ids("no-such-user-zz", None)
.unwrap_err()
.to_string();
assert!(err.contains("not found"), "got: {err}");
}
#[test]
fn resolve_ids_rejects_unknown_group() {
let me = current_user();
let err = resolve_ids(&me.name, Some("no-such-group-zz"))
.unwrap_err()
.to_string();
assert!(err.contains("not found"), "got: {err}");
}
}