pg_embedded_setup_unpriv/
privileges.rs1#![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
66pub fn make_dir_accessible<P: AsRef<Utf8Path>>(dir: P, user: &User) -> PrivilegeResult<()> {
86 ensure_dir_for_user(dir, user, 0o755)
87}
88
89pub fn make_data_dir_private<P: AsRef<Utf8Path>>(dir: P, user: &User) -> PrivilegeResult<()> {
114 ensure_dir_for_user(dir, user, 0o700)
115}
116
117#[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#[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#[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#[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}