#![cfg(all(
unix,
any(
target_os = "linux",
target_os = "android",
target_os = "freebsd",
target_os = "openbsd",
target_os = "dragonfly",
)
))]
use std::io::ErrorKind;
use camino::{Utf8Path, Utf8PathBuf};
use cap_std::{
ambient_authority,
fs::{Dir, DirEntry},
};
use color_eyre::eyre::{Context, eyre};
use nix::unistd::{Uid, User, chown};
use tracing::{info, info_span};
use crate::{
error::{PrivilegeError, PrivilegeResult},
fs::{ensure_dir_exists, set_permissions},
observability::LOG_TARGET,
};
pub(crate) fn ensure_dir_for_user<P: AsRef<Utf8Path>>(
directory: P,
user: &User,
mode: u32,
) -> PrivilegeResult<()> {
let dir_path = directory.as_ref();
let span = dir_for_user_span(dir_path, user, mode);
let _entered = span.enter();
ensure_dir_for_user_inner(dir_path, user, mode)?;
log_dir_for_user_success(dir_path, user);
Ok(())
}
fn dir_for_user_span(dir_path: &Utf8Path, user: &User, mode: u32) -> tracing::Span {
info_span!(
target: LOG_TARGET,
"ensure_dir_for_user",
path = %dir_path,
user = %user.name,
uid = user.uid.as_raw(),
gid = user.gid.as_raw(),
mode_octal = format_args!("{mode:o}")
)
}
fn ensure_dir_for_user_inner(dir_path: &Utf8Path, user: &User, mode: u32) -> PrivilegeResult<()> {
ensure_dir_exists(dir_path)?;
chown_entry(dir_path, user)?;
set_permissions(dir_path, mode)?;
Ok(())
}
fn log_dir_for_user_success(dir_path: &Utf8Path, user: &User) {
info!(
target: LOG_TARGET,
path = %dir_path,
user = %user.name,
"ensured directory ownership and permissions for user"
);
}
pub fn make_dir_accessible<P: AsRef<Utf8Path>>(dir: P, user: &User) -> PrivilegeResult<()> {
ensure_dir_for_user(dir, user, 0o755)
}
pub fn make_data_dir_private<P: AsRef<Utf8Path>>(dir: P, user: &User) -> PrivilegeResult<()> {
ensure_dir_for_user(dir, user, 0o700)
}
#[expect(
clippy::cognitive_complexity,
reason = "walk traversal and contextual logging require nested branching"
)]
pub(crate) fn ensure_tree_owned_by_user<P: AsRef<Utf8Path>>(
root: P,
user: &User,
) -> PrivilegeResult<()> {
let span = info_span!(
target: LOG_TARGET,
"ensure_tree_owned_by_user",
root = %root.as_ref(),
user = %user.name,
uid = user.uid.as_raw(),
gid = user.gid.as_raw()
);
let _entered = span.enter();
let mut stack = vec![root.as_ref().to_path_buf()];
let mut updated = 0usize;
while let Some(path_buf) = stack.pop() {
let path = path_buf.as_path();
let Some(dir_result) = open_directory_if_exists(path) else {
continue;
};
let dir = dir_result?;
for dir_entry_result in dir
.entries()
.with_context(|| format!("read_dir {}", path.as_str()))?
{
let dir_entry =
dir_entry_result.with_context(|| format!("iterate {}", path.as_str()))?;
let entry_path = resolve_entry_path(path, &dir_entry)?;
chown_entry(&entry_path, user)?;
updated += 1;
if is_directory(&dir_entry) {
stack.push(entry_path);
}
}
}
info!(
target: LOG_TARGET,
root = %root.as_ref(),
updated_entries = updated,
"ensured tree ownership for user"
);
Ok(())
}
fn open_directory_if_exists(path: &Utf8Path) -> Option<PrivilegeResult<Dir>> {
match Dir::open_ambient_dir(path.as_std_path(), ambient_authority()) {
Ok(dir) => Some(Ok(dir)),
Err(err) if err.kind() == ErrorKind::NotFound => None,
Err(err) => {
let report = eyre!(err).wrap_err(format!("open directory {}", path.as_str()));
Some(Err(PrivilegeError::from(report)))
}
}
}
fn resolve_entry_path(path: &Utf8Path, entry: &DirEntry) -> PrivilegeResult<Utf8PathBuf> {
let joined = path.as_std_path().join(entry.file_name());
let entry_path = Utf8PathBuf::from_path_buf(joined)
.map_err(|_| eyre!("non-UTF-8 path under {}", path.as_str()))?;
Ok(entry_path)
}
fn chown_entry(path: &Utf8Path, user: &User) -> PrivilegeResult<()> {
chown(path.as_std_path(), Some(user.uid), Some(user.gid))
.with_context(|| format!("chown {}", path.as_str()))?;
Ok(())
}
fn is_directory(entry: &DirEntry) -> bool { entry.file_type().is_ok_and(|ft| ft.is_dir()) }
#[must_use]
pub fn nobody_uid() -> Uid {
use nix::unistd::User;
User::from_name("nobody").map_or_else(
|_| Uid::from_raw(65534),
|maybe_user| maybe_user.map_or_else(|| Uid::from_raw(65534), |user| user.uid),
)
}
#[must_use]
pub fn default_paths_for(uid: Uid) -> (Utf8PathBuf, Utf8PathBuf) {
let base = Utf8PathBuf::from(format!("/var/tmp/pg-embed-{}", uid.as_raw()));
(base.join("install"), base.join("data"))
}
#[cfg(feature = "privileged-tests")]
#[deprecated(note = "with_temp_euid() is unsupported; use the worker-based privileged path")]
pub fn with_temp_euid<F, R>(target: Uid, _body: F) -> crate::Result<R>
where
F: FnOnce() -> crate::Result<R>,
{
let _ = target;
Err(PrivilegeError::from(eyre!(
"with_temp_euid() is unsupported; use the worker-based privileged path"
))
.into())
}
#[cfg(test)]
#[path = "privileges_tests.rs"]
mod tests;