Skip to main content

pg_embedded_setup_unpriv/
privileges.rs

1//! Privilege management helpers for dropping root access safely.
2#![cfg(all(
3    unix,
4    any(
5        target_os = "linux",
6        target_os = "android",
7        target_os = "freebsd",
8        target_os = "openbsd",
9        target_os = "dragonfly",
10    )
11))]
12use crate::error::{PrivilegeError, PrivilegeResult};
13use crate::fs::{ensure_dir_exists, set_permissions};
14use crate::observability::LOG_TARGET;
15use camino::{Utf8Path, Utf8PathBuf};
16use cap_std::{
17    ambient_authority,
18    fs::{Dir, DirEntry},
19};
20use color_eyre::eyre::{Context, eyre};
21use nix::unistd::{Uid, User, chown};
22use std::io::ErrorKind;
23use tracing::{info, info_span};
24
25pub(crate) fn ensure_dir_for_user<P: AsRef<Utf8Path>>(
26    directory: P,
27    user: &User,
28    mode: u32,
29) -> PrivilegeResult<()> {
30    let dir_path = directory.as_ref();
31    let span = dir_for_user_span(dir_path, user, mode);
32    let _entered = span.enter();
33    ensure_dir_for_user_inner(dir_path, user, mode)?;
34    log_dir_for_user_success(dir_path, user);
35    Ok(())
36}
37
38fn dir_for_user_span(dir_path: &Utf8Path, user: &User, mode: u32) -> tracing::Span {
39    info_span!(
40        target: LOG_TARGET,
41        "ensure_dir_for_user",
42        path = %dir_path,
43        user = %user.name,
44        uid = user.uid.as_raw(),
45        gid = user.gid.as_raw(),
46        mode_octal = format_args!("{mode:o}")
47    )
48}
49
50fn ensure_dir_for_user_inner(dir_path: &Utf8Path, user: &User, mode: u32) -> PrivilegeResult<()> {
51    ensure_dir_exists(dir_path)?;
52    chown_entry(dir_path, user)?;
53    set_permissions(dir_path, mode)?;
54    Ok(())
55}
56
57fn log_dir_for_user_success(dir_path: &Utf8Path, user: &User) {
58    info!(
59        target: LOG_TARGET,
60        path = %dir_path,
61        user = %user.name,
62        "ensured directory ownership and permissions for user"
63    );
64}
65
66/// Ensures `dir` exists, is owned by `user`, and grants world-readable access.
67///
68/// # Errors
69/// Returns an error when the directory cannot be created, chowned, or
70/// updated with the target permissions.
71///
72/// The example returns `PrivilegeResult` so callers propagate the helper's
73/// domain-specific error type rather than the opaque crate alias.
74/// # Examples
75/// ```no_run
76/// use nix::unistd::User;
77/// use pg_embedded_setup_unpriv::make_dir_accessible;
78///
79/// # fn demo(user: &User) -> pg_embedded_setup_unpriv::PrivilegeResult<()> {
80/// let dir = camino::Utf8Path::new("/var/tmp/my-install");
81/// make_dir_accessible(dir, user)?;
82/// # Ok(())
83/// # }
84/// ```
85pub fn make_dir_accessible<P: AsRef<Utf8Path>>(dir: P, user: &User) -> PrivilegeResult<()> {
86    ensure_dir_for_user(dir, user, 0o755)
87}
88
89/// Ensures `dir` exists, is owned by `user`, and has `PostgreSQL`-compatible 0700 permissions.
90///
91/// `PostgreSQL` refuses to use a data directory that is accessible to other
92/// users. This helper creates the directory (if needed), chowns it to `user`,
93/// and clamps permissions to `0700` to satisfy that requirement.
94///
95/// The example returns `PrivilegeResult` to demonstrate how callers surface the
96/// helper's domain errors when composing setup flows.
97///
98/// # Examples
99/// ```no_run
100/// use nix::unistd::User;
101/// use pg_embedded_setup_unpriv::make_data_dir_private;
102///
103/// # fn demo(user: &User) -> pg_embedded_setup_unpriv::PrivilegeResult<()> {
104/// let dir = camino::Utf8Path::new("/var/tmp/my-data");
105/// make_data_dir_private(dir, user)?;
106/// # Ok(())
107/// # }
108/// ```
109///
110/// # Errors
111/// Returns an error when the directory cannot be created, chowned, or
112/// updated with the strict permission set required by `PostgreSQL`.
113pub fn make_data_dir_private<P: AsRef<Utf8Path>>(dir: P, user: &User) -> PrivilegeResult<()> {
114    ensure_dir_for_user(dir, user, 0o700)
115}
116
117/// Recursively ensures ownership of all entries under `root` is assigned to `user`.
118///
119/// Traverses the directory tree rooted at `root`, calling `chown` on each entry
120/// and counting updates. Directories are opened using capability handles to
121/// respect the ambient sandbox. Errors surface with contextual path metadata to
122/// aid diagnostics.
123#[expect(
124    clippy::cognitive_complexity,
125    reason = "walk traversal and contextual logging require nested branching"
126)]
127pub(crate) fn ensure_tree_owned_by_user<P: AsRef<Utf8Path>>(
128    root: P,
129    user: &User,
130) -> PrivilegeResult<()> {
131    let span = info_span!(
132        target: LOG_TARGET,
133        "ensure_tree_owned_by_user",
134        root = %root.as_ref(),
135        user = %user.name,
136        uid = user.uid.as_raw(),
137        gid = user.gid.as_raw()
138    );
139    let _entered = span.enter();
140    let mut stack = vec![root.as_ref().to_path_buf()];
141    let mut updated = 0usize;
142
143    while let Some(path_buf) = stack.pop() {
144        let path = path_buf.as_path();
145
146        let Some(dir_result) = open_directory_if_exists(path) else {
147            continue;
148        };
149        let dir = dir_result?;
150        for dir_entry_result in dir
151            .entries()
152            .with_context(|| format!("read_dir {}", path.as_str()))?
153        {
154            let dir_entry =
155                dir_entry_result.with_context(|| format!("iterate {}", path.as_str()))?;
156            let entry_path = resolve_entry_path(path, &dir_entry)?;
157
158            chown_entry(&entry_path, user)?;
159            updated += 1;
160
161            if is_directory(&dir_entry) {
162                stack.push(entry_path);
163            }
164        }
165    }
166
167    info!(
168        target: LOG_TARGET,
169        root = %root.as_ref(),
170        updated_entries = updated,
171        "ensured tree ownership for user"
172    );
173    Ok(())
174}
175
176fn open_directory_if_exists(path: &Utf8Path) -> Option<PrivilegeResult<Dir>> {
177    match Dir::open_ambient_dir(path.as_std_path(), ambient_authority()) {
178        Ok(dir) => Some(Ok(dir)),
179        Err(err) if err.kind() == ErrorKind::NotFound => None,
180        Err(err) => {
181            let report = eyre!(err).wrap_err(format!("open directory {}", path.as_str()));
182            Some(Err(PrivilegeError::from(report)))
183        }
184    }
185}
186
187fn resolve_entry_path(path: &Utf8Path, entry: &DirEntry) -> PrivilegeResult<Utf8PathBuf> {
188    let joined = path.as_std_path().join(entry.file_name());
189    let entry_path = Utf8PathBuf::from_path_buf(joined)
190        .map_err(|_| eyre!("non-UTF-8 path under {}", path.as_str()))?;
191    Ok(entry_path)
192}
193
194fn chown_entry(path: &Utf8Path, user: &User) -> PrivilegeResult<()> {
195    chown(path.as_std_path(), Some(user.uid), Some(user.gid))
196        .with_context(|| format!("chown {}", path.as_str()))?;
197    Ok(())
198}
199
200fn is_directory(entry: &DirEntry) -> bool {
201    entry.file_type().is_ok_and(|ft| ft.is_dir())
202}
203
204/// Retrieves the UID of the `nobody` account, defaulting to 65534 when absent.
205///
206/// # Examples
207/// ```
208/// let uid = pg_embedded_setup_unpriv::nobody_uid();
209/// assert!(uid.as_raw() > 0);
210/// ```
211#[must_use]
212pub fn nobody_uid() -> Uid {
213    use nix::unistd::User;
214    User::from_name("nobody").map_or_else(
215        |_| Uid::from_raw(65534),
216        |maybe_user| maybe_user.map_or_else(|| Uid::from_raw(65534), |user| user.uid),
217    )
218}
219
220/// Computes default installation and data directories for a given uid.
221///
222/// # Examples
223/// ```
224/// use nix::unistd::Uid;
225///
226/// let uid = Uid::from_raw(1000);
227/// let (install, data) = pg_embedded_setup_unpriv::default_paths_for(uid);
228/// assert!(install.as_str().contains("pg-embed-"));
229/// assert!(data.as_str().contains("pg-embed-"));
230/// ```
231#[must_use]
232pub fn default_paths_for(uid: Uid) -> (Utf8PathBuf, Utf8PathBuf) {
233    let base = Utf8PathBuf::from(format!("/var/tmp/pg-embed-{}", uid.as_raw()));
234    (base.join("install"), base.join("data"))
235}
236
237/// DEPRECATED: process-wide UID switching is unsafe and unsupported.
238///
239/// Use the worker-based privileged path instead of relying on temporary
240/// effective UID changes.
241///
242/// # Examples
243/// ```no_run
244/// # use nix::unistd::Uid;
245/// use pg_embedded_setup_unpriv::with_temp_euid;
246/// # fn demo(uid: Uid) {
247/// let _ = with_temp_euid::<_, ()>(uid, || Ok(()));
248/// # }
249/// ```
250///
251/// # Errors
252/// Always returns an error instructing callers to use the worker-based privileged path.
253#[cfg(feature = "privileged-tests")]
254#[deprecated(note = "with_temp_euid() is unsupported; use the worker-based privileged path")]
255pub fn with_temp_euid<F, R>(target: Uid, _body: F) -> crate::Result<R>
256where
257    F: FnOnce() -> crate::Result<R>,
258{
259    let _ = target;
260    Err(PrivilegeError::from(eyre!(
261        "with_temp_euid() is unsupported; use the worker-based privileged path"
262    ))
263    .into())
264}