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;
9mod cluster;
10mod env;
11mod error;
12mod fs;
13#[cfg(all(
14    unix,
15    any(
16        target_os = "linux",
17        target_os = "android",
18        target_os = "freebsd",
19        target_os = "openbsd",
20        target_os = "dragonfly",
21    ),
22))]
23mod privileges;
24#[doc(hidden)]
25pub mod test_support;
26#[doc(hidden)]
27pub mod worker;
28pub(crate) mod worker_process;
29
30/// Integration test shims for worker process orchestration.
31#[doc(hidden)]
32pub mod worker_process_test_api {
33    use std::time::Duration;
34
35    use camino::Utf8Path;
36    use postgresql_embedded::Settings;
37
38    pub use crate::cluster::WorkerOperation;
39    use crate::worker_process;
40
41    #[cfg(all(
42        unix,
43        any(
44            target_os = "linux",
45            target_os = "android",
46            target_os = "freebsd",
47            target_os = "openbsd",
48            target_os = "dragonfly",
49        ),
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        #[must_use]
61        #[expect(
62            clippy::too_many_arguments,
63            reason = "test helper mirrors worker request constructor"
64        )]
65        /// Constructs a worker request for invoking an operation in tests.
66        ///
67        /// # Examples
68        ///
69        /// ```ignore
70        /// # use std::time::Duration;
71        /// # use camino::Utf8Path;
72        /// # use postgresql_embedded::Settings;
73        /// # use pg_embedded_setup_unpriv::{WorkerOperation, worker_process_test_api::WorkerRequest};
74        /// # let worker = Utf8Path::new("/tmp/worker");
75        /// # let settings = Settings::default();
76        /// # let env_vars: Vec<(String, Option<String>)> = Vec::new();
77        /// let request = WorkerRequest::new(
78        ///     worker,
79        ///     &settings,
80        ///     &env_vars,
81        ///     WorkerOperation::Setup,
82        ///     Duration::from_secs(1),
83        /// );
84        /// # let _ = request;
85        /// ```
86        pub const fn new(
87            worker: &'a Utf8Path,
88            settings: &'a Settings,
89            env_vars: &'a [(String, Option<String>)],
90            operation: WorkerOperation,
91            timeout: Duration,
92        ) -> Self {
93            Self(worker_process::WorkerRequest::new(
94                worker, settings, env_vars, operation, timeout,
95            ))
96        }
97
98        /// Returns a reference to the wrapped worker request.
99        pub(crate) const fn inner(&self) -> &worker_process::WorkerRequest<'a> {
100            &self.0
101        }
102    }
103
104    /// Executes a worker request whilst returning crate-level errors.
105    pub fn run(request: &WorkerRequest<'_>) -> crate::BootstrapResult<()> {
106        worker_process::run(request.inner())
107    }
108
109    #[cfg(all(
110        unix,
111        any(
112            target_os = "linux",
113            target_os = "android",
114            target_os = "freebsd",
115            target_os = "openbsd",
116            target_os = "dragonfly",
117        ),
118    ))]
119    /// Guard that restores the privilege-drop toggle when tests finish.
120    pub struct PrivilegeDropGuard {
121        _inner: InnerPrivilegeDropGuard,
122    }
123
124    #[cfg(all(
125        unix,
126        any(
127            target_os = "linux",
128            target_os = "android",
129            target_os = "freebsd",
130            target_os = "openbsd",
131            target_os = "dragonfly",
132        ),
133    ))]
134    #[must_use]
135    /// Temporarily disables privilege dropping so tests can run deterministic
136    /// worker binaries without adjusting file ownership.
137    pub fn disable_privilege_drop_for_tests() -> PrivilegeDropGuard {
138        PrivilegeDropGuard {
139            _inner: worker_process::disable_privilege_drop_for_tests(),
140        }
141    }
142
143    #[must_use]
144    /// Renders a worker failure for assertion-friendly error strings.
145    pub fn render_failure_for_tests(
146        context: &str,
147        output: &std::process::Output,
148    ) -> crate::BootstrapError {
149        worker_process::render_failure_for_tests(context, output)
150    }
151}
152
153#[doc(hidden)]
154pub use crate::env::ScopedEnv;
155pub use bootstrap::{
156    ExecutionMode, ExecutionPrivileges, TestBootstrapEnvironment, TestBootstrapSettings,
157    bootstrap_for_tests, detect_execution_privileges, run,
158};
159#[cfg(any(doc, test, feature = "cluster-unit-tests", feature = "dev-worker"))]
160#[doc(hidden)]
161pub use cluster::WorkerInvoker;
162#[cfg(any(test, feature = "cluster-unit-tests"))]
163#[doc(hidden)]
164pub use cluster::WorkerOperation;
165pub use cluster::{ConnectionMetadata, TestCluster, TestClusterConnection};
166#[doc(hidden)]
167pub use error::BootstrapResult;
168pub use error::PgEmbeddedError as Error;
169pub use error::{BootstrapError, BootstrapErrorKind, PgEmbeddedError, Result};
170#[cfg(feature = "privileged-tests")]
171#[cfg(all(
172    unix,
173    any(
174        target_os = "linux",
175        target_os = "android",
176        target_os = "freebsd",
177        target_os = "openbsd",
178        target_os = "dragonfly",
179    ),
180))]
181#[expect(
182    deprecated,
183    reason = "with_temp_euid() remains exported for backward compatibility whilst deprecated"
184)]
185pub use privileges::with_temp_euid;
186#[cfg(all(
187    unix,
188    any(
189        target_os = "linux",
190        target_os = "android",
191        target_os = "freebsd",
192        target_os = "openbsd",
193        target_os = "dragonfly",
194    ),
195))]
196pub use privileges::{default_paths_for, make_data_dir_private, make_dir_accessible, nobody_uid};
197
198use color_eyre::eyre::{Context, eyre};
199use ortho_config::OrthoConfig;
200use postgresql_embedded::{Settings, VersionReq};
201use serde::{Deserialize, Serialize};
202
203use crate::error::{ConfigError, ConfigResult};
204use camino::Utf8PathBuf;
205use std::ffi::OsString;
206
207/// Captures `PostgreSQL` settings supplied via environment variables.
208#[derive(Debug, Clone, Serialize, Deserialize, OrthoConfig, Default)]
209#[ortho_config(prefix = "PG")]
210///
211/// # Examples
212/// ```
213/// use pg_embedded_setup_unpriv::PgEnvCfg;
214///
215/// let cfg = PgEnvCfg::default();
216/// assert!(cfg.port.is_none());
217/// ```
218pub struct PgEnvCfg {
219    /// Optional semver requirement that constrains the `PostgreSQL` version.
220    pub version_req: Option<String>,
221    /// Port assigned to the embedded `PostgreSQL` server.
222    pub port: Option<u16>,
223    /// Name of the administrative user created for the cluster.
224    pub superuser: Option<String>,
225    /// Password provisioned for the administrative user.
226    pub password: Option<String>,
227    /// Directory used for `PostgreSQL` data files when provided.
228    pub data_dir: Option<Utf8PathBuf>,
229    /// Directory containing the `PostgreSQL` binaries when provided.
230    pub runtime_dir: Option<Utf8PathBuf>,
231    /// Locale applied to `initdb` when specified.
232    pub locale: Option<String>,
233    /// Encoding applied to `initdb` when specified.
234    pub encoding: Option<String>,
235}
236
237impl PgEnvCfg {
238    /// Loads configuration from environment variables without parsing CLI arguments.
239    ///
240    /// # Errors
241    /// Returns an error when environment parsing fails or derived configuration
242    /// cannot be represented using UTF-8 paths.
243    pub fn load() -> ConfigResult<Self> {
244        let args = [OsString::from("pg-embedded-setup-unpriv")];
245        Self::load_from_iter(args).map_err(|err| ConfigError::from(eyre!(err)))
246    }
247
248    /// Converts the configuration into a complete `postgresql_embedded::Settings` object.
249    ///
250    /// Applies version, connection, path, and locale settings from the current configuration.
251    /// Returns an error if the version requirement is invalid.
252    ///
253    /// # Returns
254    /// A fully configured `Settings` instance on success, or an error if configuration fails.
255    ///
256    /// # Errors
257    /// Returns an error when the semantic version requirement cannot be parsed.
258    pub fn to_settings(&self) -> Result<Settings> {
259        let mut s = Settings::default();
260
261        self.apply_version(&mut s)?;
262        self.apply_connection(&mut s);
263        self.apply_paths(&mut s);
264        self.apply_locale(&mut s);
265
266        Ok(s)
267    }
268
269    fn apply_version(&self, settings: &mut Settings) -> ConfigResult<()> {
270        if let Some(ref vr) = self.version_req {
271            settings.version =
272                VersionReq::parse(vr).context("PG_VERSION_REQ invalid semver spec")?;
273        }
274        Ok(())
275    }
276
277    fn apply_connection(&self, settings: &mut Settings) {
278        if let Some(p) = self.port {
279            settings.port = p;
280        }
281        if let Some(ref u) = self.superuser {
282            settings.username.clone_from(u);
283        }
284        if let Some(ref pw) = self.password {
285            settings.password.clone_from(pw);
286        }
287    }
288
289    fn apply_paths(&self, settings: &mut Settings) {
290        if let Some(ref dir) = self.data_dir {
291            settings.data_dir = dir.clone().into_std_path_buf();
292        }
293        if let Some(ref dir) = self.runtime_dir {
294            settings.installation_dir = dir.clone().into_std_path_buf();
295        }
296    }
297
298    /// Applies locale and encoding settings to the `PostgreSQL` configuration if specified
299    /// in the environment.
300    ///
301    /// Inserts the `locale` and `encoding` values into the settings configuration map when
302    /// present in the environment configuration.
303    fn apply_locale(&self, settings: &mut Settings) {
304        if let Some(ref loc) = self.locale {
305            settings.configuration.insert("locale".into(), loc.clone());
306        }
307        if let Some(ref enc) = self.encoding {
308            settings
309                .configuration
310                .insert("encoding".into(), enc.clone());
311        }
312    }
313}