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