pg_embedded_setup_unpriv/lib.rs
1//! Facilitates preparing an embedded `PostgreSQL` instance while dropping root
2//! privileges.
3//!
4//! The library owns the lifecycle for configuring paths, permissions, and
5//! process identity so the bundled `PostgreSQL` binaries can initialise safely
6//! under an unprivileged account.
7
8mod bootstrap;
9pub mod cache;
10mod cluster;
11mod env;
12mod error;
13mod fs;
14mod observability;
15#[cfg(all(
16 unix,
17 any(
18 target_os = "linux",
19 target_os = "android",
20 target_os = "freebsd",
21 target_os = "openbsd",
22 target_os = "dragonfly",
23 ),
24))]
25mod privileges;
26#[doc(hidden)]
27pub mod test_support;
28#[doc(hidden)]
29pub mod worker;
30pub(crate) mod worker_process;
31
32#[doc(hidden)]
33pub mod worker_process_test_api {
34 //! Integration test shims for worker process orchestration.
35
36 pub use crate::cluster::WorkerOperation;
37 use crate::worker_process;
38 pub use crate::worker_process::WorkerRequestArgs;
39
40 #[cfg(all(
41 unix,
42 any(
43 target_os = "linux",
44 target_os = "android",
45 target_os = "freebsd",
46 target_os = "openbsd",
47 target_os = "dragonfly",
48 ),
49 any(test, doc, feature = "privileged-tests"),
50 ))]
51 use crate::worker_process::PrivilegeDropGuard as InnerPrivilegeDropGuard;
52
53 /// Test-visible wrapper around the internal worker request.
54 ///
55 /// Use this helper when integration tests need to exercise worker process
56 /// orchestration without exposing the internals as part of the public API.
57 pub struct WorkerRequest<'a>(worker_process::WorkerRequest<'a>);
58
59 impl<'a> WorkerRequest<'a> {
60 /// Constructs a worker request for invoking an operation in tests.
61 ///
62 /// # Examples
63 ///
64 /// ```ignore
65 /// # use std::time::Duration;
66 /// # use camino::Utf8Path;
67 /// # use postgresql_embedded::Settings;
68 /// # use pg_embedded_setup_unpriv::{
69 /// # WorkerOperation,
70 /// # worker_process_test_api::{WorkerRequest, WorkerRequestArgs},
71 /// # };
72 /// # let worker = Utf8Path::new("/tmp/worker");
73 /// # let settings = Settings::default();
74 /// # let env_vars: Vec<(String, Option<String>)> = Vec::new();
75 /// let args = WorkerRequestArgs {
76 /// worker,
77 /// settings: &settings,
78 /// env_vars: &env_vars,
79 /// operation: WorkerOperation::Setup,
80 /// timeout: Duration::from_secs(1),
81 /// };
82 /// let request = WorkerRequest::new(args);
83 /// # let _ = request;
84 /// ```
85 #[must_use]
86 pub const fn new(args: WorkerRequestArgs<'a>) -> Self {
87 Self(worker_process::WorkerRequest::new(args))
88 }
89
90 /// Returns a reference to the wrapped worker request.
91 pub(crate) const fn inner(&self) -> &worker_process::WorkerRequest<'a> {
92 &self.0
93 }
94 }
95
96 /// Executes a worker request whilst returning crate-level errors.
97 pub fn run(request: &WorkerRequest<'_>) -> crate::BootstrapResult<()> {
98 worker_process::run(request.inner())
99 }
100
101 /// Guard that restores the privilege-drop toggle when tests finish.
102 #[cfg(all(
103 unix,
104 any(
105 target_os = "linux",
106 target_os = "android",
107 target_os = "freebsd",
108 target_os = "openbsd",
109 target_os = "dragonfly",
110 ),
111 any(test, doc, feature = "privileged-tests"),
112 ))]
113 pub struct PrivilegeDropGuard {
114 _inner: InnerPrivilegeDropGuard,
115 }
116
117 /// Temporarily disables privilege dropping so tests can run deterministic
118 /// worker binaries without adjusting file ownership.
119 #[cfg(all(
120 unix,
121 any(
122 target_os = "linux",
123 target_os = "android",
124 target_os = "freebsd",
125 target_os = "openbsd",
126 target_os = "dragonfly",
127 ),
128 any(test, doc, feature = "privileged-tests"),
129 ))]
130 #[must_use]
131 pub fn disable_privilege_drop_for_tests() -> PrivilegeDropGuard {
132 PrivilegeDropGuard {
133 _inner: worker_process::disable_privilege_drop_for_tests(),
134 }
135 }
136
137 /// Renders a worker failure for assertion-friendly error strings.
138 #[must_use]
139 pub fn render_failure_for_tests(
140 context: &str,
141 output: &std::process::Output,
142 ) -> crate::BootstrapError {
143 worker_process::render_failure_for_tests(context, output)
144 }
145}
146
147/// Resolves a path to an ambient directory handle paired with the relative path component.
148///
149/// This function provides capability-based filesystem access by opening paths relative to
150/// ambient authority. Absolute paths are opened relative to their parent directory; relative
151/// paths reuse the current working directory.
152///
153/// # Returns
154///
155/// Returns a tuple containing:
156/// - A [`cap_std::fs::Dir`] handle for the parent directory
157/// - A [`camino::Utf8PathBuf`] with the relative component
158///
159/// For absolute paths like `/foo/bar`, returns `(Dir("/foo"), "bar")`.
160/// For relative paths like `baz/qux`, returns `(Dir("."), "baz/qux")`.
161/// For root paths like `/`, returns `(Dir("/"), "")` with an empty relative component.
162///
163/// # Errors
164///
165/// Returns an error if the path cannot be opened as a directory or if path operations fail.
166///
167/// # Examples
168///
169/// ```no_run
170/// use pg_embedded_setup_unpriv::ambient_dir_and_path;
171/// use camino::Utf8Path;
172///
173/// # fn main() -> color_eyre::Result<()> {
174/// let (dir, relative) = ambient_dir_and_path(Utf8Path::new("./data"))?;
175/// // Use dir handle for capability-based operations on relative path
176/// # Ok(())
177/// # }
178/// ```
179pub use crate::fs::ambient_dir_and_path;
180
181#[doc(hidden)]
182pub use crate::env::ScopedEnv;
183pub use bootstrap::{
184 ExecutionMode, ExecutionPrivileges, TestBootstrapEnvironment, TestBootstrapSettings,
185 bootstrap_for_tests, detect_execution_privileges, find_timezone_dir, run,
186};
187#[cfg(any(doc, test, feature = "cluster-unit-tests", feature = "dev-worker"))]
188#[doc(hidden)]
189pub use cluster::WorkerInvoker;
190#[cfg(any(test, feature = "cluster-unit-tests"))]
191#[doc(hidden)]
192pub use cluster::WorkerOperation;
193pub use cluster::{
194 ConnectionMetadata, DatabaseName, TemporaryDatabase, TestCluster, TestClusterConnection,
195};
196#[doc(hidden)]
197pub use error::BootstrapResult;
198pub use error::PgEmbeddedError as Error;
199pub use error::{
200 BootstrapError, BootstrapErrorKind, PgEmbeddedError, PrivilegeError, PrivilegeResult, Result,
201};
202#[cfg(feature = "privileged-tests")]
203#[cfg(all(
204 unix,
205 any(
206 target_os = "linux",
207 target_os = "android",
208 target_os = "freebsd",
209 target_os = "openbsd",
210 target_os = "dragonfly",
211 ),
212))]
213#[expect(
214 deprecated,
215 reason = "with_temp_euid() remains exported for backward compatibility whilst deprecated"
216)]
217pub use privileges::with_temp_euid;
218#[cfg(all(
219 unix,
220 any(
221 target_os = "linux",
222 target_os = "android",
223 target_os = "freebsd",
224 target_os = "openbsd",
225 target_os = "dragonfly",
226 ),
227))]
228pub use privileges::{default_paths_for, make_data_dir_private, make_dir_accessible, nobody_uid};
229
230use color_eyre::eyre::{Context, eyre};
231use ortho_config::OrthoConfig;
232use postgresql_embedded::{Settings, VersionReq};
233use serde::{Deserialize, Serialize};
234
235use crate::error::{ConfigError, ConfigResult};
236use camino::Utf8PathBuf;
237use std::ffi::OsString;
238
239/// Captures `PostgreSQL` settings supplied via environment variables.
240#[derive(Debug, Clone, Serialize, Deserialize, OrthoConfig, Default)]
241#[ortho_config(prefix = "PG")]
242///
243/// # Examples
244/// ```
245/// use pg_embedded_setup_unpriv::PgEnvCfg;
246///
247/// let cfg = PgEnvCfg::default();
248/// assert!(cfg.port.is_none());
249/// ```
250pub struct PgEnvCfg {
251 /// Optional semver requirement that constrains the `PostgreSQL` version.
252 pub version_req: Option<String>,
253 /// Port assigned to the embedded `PostgreSQL` server.
254 pub port: Option<u16>,
255 /// Name of the administrative user created for the cluster.
256 pub superuser: Option<String>,
257 /// Password provisioned for the administrative user.
258 pub password: Option<String>,
259 /// Directory used for `PostgreSQL` data files when provided.
260 pub data_dir: Option<Utf8PathBuf>,
261 /// Directory containing the `PostgreSQL` binaries when provided.
262 pub runtime_dir: Option<Utf8PathBuf>,
263 /// Locale applied to `initdb` when specified.
264 pub locale: Option<String>,
265 /// Encoding applied to `initdb` when specified.
266 pub encoding: Option<String>,
267 /// Directory for sharing downloaded `PostgreSQL` binaries across test runs.
268 ///
269 /// When `Some`, this explicit path is used directly by `TestCluster`, bypassing
270 /// the automatic resolution chain. When `None`, the cache directory is resolved
271 /// in the following order:
272 ///
273 /// 1. `PG_BINARY_CACHE_DIR` environment variable (if set and non-empty)
274 /// 2. `$XDG_CACHE_HOME/pg-embedded/binaries` (if `XDG_CACHE_HOME` is set)
275 /// 3. `$HOME/.cache/pg-embedded/binaries` (if `HOME` is set)
276 /// 4. `/tmp/pg-embedded/binaries` (final fallback)
277 pub binary_cache_dir: Option<Utf8PathBuf>,
278}
279
280impl PgEnvCfg {
281 /// Loads configuration from environment variables without parsing CLI arguments.
282 ///
283 /// # Errors
284 /// Returns an error when environment parsing fails or derived configuration
285 /// cannot be represented using UTF-8 paths.
286 pub fn load() -> ConfigResult<Self> {
287 let args = [OsString::from("pg-embedded-setup-unpriv")];
288 Self::load_from_iter(args).map_err(|err| ConfigError::from(eyre!(err)))
289 }
290
291 /// Converts the configuration into a complete `postgresql_embedded::Settings` object.
292 ///
293 /// Applies version, connection, path, and locale settings from the current configuration.
294 /// Returns an error if the version requirement is invalid.
295 ///
296 /// # Returns
297 /// A fully configured `Settings` instance on success, or an error if configuration fails.
298 ///
299 /// # Errors
300 /// Returns an error when the semantic version requirement cannot be parsed.
301 pub fn to_settings(&self) -> Result<Settings> {
302 // Disable the internal postgresql_embedded timeout. This crate wraps lifecycle
303 // operations with tokio::time::timeout using setup_timeout/start_timeout from
304 // TestBootstrapSettings, providing consistent timeout behaviour for both
305 // privileged (subprocess) and unprivileged (in-process) execution paths.
306 // The default 5-second timeout is too short for initdb on slower systems.
307 let mut s = Settings {
308 timeout: None,
309 ..Settings::default()
310 };
311
312 self.apply_version(&mut s)?;
313 self.apply_connection(&mut s);
314 self.apply_paths(&mut s);
315 self.apply_locale(&mut s);
316
317 Ok(s)
318 }
319
320 fn apply_version(&self, settings: &mut Settings) -> ConfigResult<()> {
321 if let Some(ref vr) = self.version_req {
322 settings.version =
323 VersionReq::parse(vr).context("PG_VERSION_REQ invalid semver spec")?;
324 }
325 Ok(())
326 }
327
328 fn apply_connection(&self, settings: &mut Settings) {
329 if let Some(p) = self.port {
330 settings.port = p;
331 }
332 if let Some(ref u) = self.superuser {
333 settings.username.clone_from(u);
334 }
335 if let Some(ref pw) = self.password {
336 settings.password.clone_from(pw);
337 }
338 }
339
340 fn apply_paths(&self, settings: &mut Settings) {
341 if let Some(ref dir) = self.data_dir {
342 settings.data_dir = dir.clone().into_std_path_buf();
343 }
344 if let Some(ref dir) = self.runtime_dir {
345 settings.installation_dir = dir.clone().into_std_path_buf();
346 }
347 }
348
349 /// Applies locale and encoding settings to the `PostgreSQL` configuration if specified
350 /// in the environment.
351 ///
352 /// Inserts the `locale` and `encoding` values into the settings configuration map when
353 /// present in the environment configuration.
354 fn apply_locale(&self, settings: &mut Settings) {
355 if let Some(ref loc) = self.locale {
356 settings.configuration.insert("locale".into(), loc.clone());
357 }
358 if let Some(ref enc) = self.encoding {
359 settings
360 .configuration
361 .insert("encoding".into(), enc.clone());
362 }
363 }
364}