Skip to main content

reddb_server/
service_cli.rs

1use std::net::{SocketAddr, TcpListener, TcpStream, ToSocketAddrs};
2use std::path::PathBuf;
3use std::process::Command;
4use std::sync::Arc;
5use std::thread;
6use std::time::Duration;
7
8use crate::auth::store::AuthStore;
9use crate::replication::ReplicationConfig;
10use crate::runtime::RedDBRuntime;
11use crate::service_router::{serve_tcp_router, InProcessRouterConfig};
12use crate::storage::StorageProfileSelection;
13pub use crate::transport::{TransportListenerFailure, TransportListenerState, TransportReadiness};
14use crate::{
15    GrpcServerOptions, RedDBGrpcServer, RedDBOptions, RedDBServer, ServerOptions, StorageMode,
16};
17
18mod server_bootstrap;
19pub use server_bootstrap::{bootstrap_server, run_server, transport_set, BootedNode};
20
21pub const DEFAULT_ROUTER_BIND_ADDR: &str = "127.0.0.1:5050";
22
23fn suggested_worker_count(cpus: usize) -> usize {
24    match cpus {
25        0 | 1 => 1,
26        2 => 2,
27        _ => cpus - 1,
28    }
29}
30
31/// Detect available CPU cores and suggest worker thread count.
32pub fn detect_runtime_config() -> RuntimeConfig {
33    let cpus = thread::available_parallelism()
34        .map(|n| n.get())
35        .unwrap_or(1);
36
37    // Two-core instances need both workers to avoid a single blocked query
38    // serialising all engine work. Larger hosts still reserve one core.
39    let suggested_workers = suggested_worker_count(cpus);
40
41    RuntimeConfig {
42        available_cpus: cpus,
43        suggested_workers,
44        stack_size: 8 * 1024 * 1024, // 16MB default
45    }
46}
47
48#[derive(Debug, Clone)]
49pub struct RuntimeConfig {
50    pub available_cpus: usize,
51    pub suggested_workers: usize,
52    pub stack_size: usize,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum ServerTransport {
57    Grpc,
58    Http,
59    Wire,
60}
61
62impl ServerTransport {
63    pub const fn as_str(self) -> &'static str {
64        match self {
65            Self::Grpc => "gRPC",
66            Self::Http => "HTTP",
67            Self::Wire => "wire",
68        }
69    }
70
71    pub const fn default_bind_addr(self) -> &'static str {
72        match self {
73            Self::Grpc => "127.0.0.1:55055",
74            Self::Http => "127.0.0.1:5000",
75            Self::Wire => "127.0.0.1:5050",
76        }
77    }
78}
79
80#[derive(Debug, Clone)]
81pub struct ServerCommandConfig {
82    pub path: Option<PathBuf>,
83    pub router_bind_addr: Option<String>,
84    pub router_bind_explicit: bool,
85    pub grpc_bind_addr: Option<String>,
86    pub grpc_bind_explicit: bool,
87    /// TLS-encrypted gRPC bind address. Can run side-by-side with
88    /// `grpc_bind_addr` (e.g. `:55055` plain + `:55555` TLS) or
89    /// stand alone for TLS-only deploys. Defaults to `None`.
90    pub grpc_tls_bind_addr: Option<String>,
91    /// Path to PEM-encoded gRPC server certificate. Resolved through
92    /// `REDDB_GRPC_TLS_CERT` (with `_FILE` companion for k8s secret
93    /// mounts). When `None` and dev-mode is enabled
94    /// (`RED_GRPC_TLS_DEV=1`) the server auto-generates a self-signed
95    /// cert and prints its SHA-256 fingerprint to stderr.
96    pub grpc_tls_cert: Option<PathBuf>,
97    /// Path to PEM-encoded gRPC server private key. Same env-var
98    /// pattern as `grpc_tls_cert`.
99    pub grpc_tls_key: Option<PathBuf>,
100    /// Optional path to a PEM bundle of trust anchors used to verify
101    /// client certificates. When set, the gRPC listener requires
102    /// every client to present a cert that chains to this CA — i.e.
103    /// mutual TLS. When unset, one-way TLS only.
104    pub grpc_tls_client_ca: Option<PathBuf>,
105    pub http_bind_addr: Option<String>,
106    pub http_bind_explicit: bool,
107    /// HTTPS bind address. When set, the HTTP server also serves a
108    /// TLS-terminated listener on this addr. Plain HTTP and HTTPS can
109    /// run side by side (e.g. 5000 plain + 55555 TLS).
110    pub http_tls_bind_addr: Option<String>,
111    /// PEM cert for HTTPS. Reads `REDDB_HTTP_TLS_CERT` / its `_FILE`
112    /// companion when not set explicitly.
113    pub http_tls_cert: Option<PathBuf>,
114    /// PEM key for HTTPS. Reads `REDDB_HTTP_TLS_KEY` / its `_FILE`
115    /// companion when not set explicitly.
116    pub http_tls_key: Option<PathBuf>,
117    /// Optional PEM CA bundle. When set, the HTTPS listener requires
118    /// every client to present a cert that chains to a CA in this
119    /// bundle (mTLS). When unset, plain server-side TLS only.
120    pub http_tls_client_ca: Option<PathBuf>,
121    pub wire_bind_addr: Option<String>,
122    pub wire_bind_explicit: bool,
123    /// TLS-encrypted wire protocol bind address
124    pub wire_tls_bind_addr: Option<String>,
125    /// Path to TLS cert PEM (if None + wire_tls_bind, auto-generate)
126    pub wire_tls_cert: Option<PathBuf>,
127    /// Path to TLS key PEM
128    pub wire_tls_key: Option<PathBuf>,
129    /// PostgreSQL wire protocol bind address (Phase 3.1 PG parity).
130    /// When set the server accepts psql / JDBC / DBeaver clients on
131    /// this port via the v3 protocol. Defaults to None (disabled).
132    pub pg_bind_addr: Option<String>,
133    pub create_if_missing: bool,
134    pub bounded_disk_residency: bool,
135    pub read_only: bool,
136    pub role: String,
137    pub primary_addr: Option<String>,
138    pub storage_profile: StorageProfileSelection,
139    /// Explicit auth enable switch from CLI/env. `--no-auth` remains
140    /// the final override and forces this off at option resolution time.
141    pub auth: bool,
142    /// Require authenticated requests. Implies `auth = true` when set.
143    pub require_auth: bool,
144    pub vault: bool,
145    /// Issue #663 — explicit `--no-auth` / `--dev` flag for local
146    /// no-password mode. When `true`, the boot pipeline force-disables
147    /// auth (`auth.enabled = false`, `auth.require_auth = false`,
148    /// `auth.vault_enabled = false`) and skips
149    /// `AuthStore::bootstrap_from_env`, so an explicit
150    /// `REDDB_USERNAME` + `REDDB_PASSWORD` pair cannot accidentally
151    /// turn anonymous access into a logged-in admin. An unmissable
152    /// warning is emitted at startup. Default `false` — the existing
153    /// implicit no-auth behaviour (just don't set the envs) is
154    /// unchanged.
155    pub no_auth: bool,
156    /// Override worker thread count (None = auto-detect from CPUs)
157    pub workers: Option<usize>,
158    /// Telemetry config (Phase 6 logging). `None` falls back to the
159    /// built-in default derived from `path` + stderr-only.
160    pub telemetry: Option<crate::telemetry::TelemetryConfig>,
161    /// HTTP handler-pool knobs from the CLI layer (issue #574 slice 5).
162    /// Carries flag and env values; red_config and built-in defaults
163    /// are applied later by [`crate::server::http_limits::resolve_http_limits`]
164    /// once the runtime is open.
165    pub http_limits_cli: crate::server::HttpLimitsCliInput,
166    /// `red server --ui` (issue #1047, ADR 0051). When `true`, the HTTP
167    /// listener also serves the pinned red-ui bundle as static assets.
168    /// The bundle directory is resolved at boot from the local cache
169    /// (downloading on first use, ADR 0050) unless `ui_dir` overrides it.
170    pub ui: bool,
171    /// Explicit bundle directory for `--ui` (`--ui-dir <DIR>`). When set,
172    /// it is served as-is with no download — the seam tests and an
173    /// air-gapped deploy use this. `None` resolves the pinned bundle.
174    pub ui_dir: Option<PathBuf>,
175    /// First-boot bootstrap settings resolved from CLI flags and, when
176    /// unset here, from environment variables by the bootstrap layer.
177    pub bootstrap: BootstrapConfig,
178}
179
180#[derive(Debug, Clone, Default)]
181pub struct BootstrapConfig {
182    pub preset: Option<String>,
183    pub manifest: Option<PathBuf>,
184    pub admin_username: Option<String>,
185    pub admin_password: Option<String>,
186    pub cloud_head_admin: Option<String>,
187    pub cloud_head_admin_password: Option<String>,
188    pub customer_admin: Option<String>,
189    pub customer_admin_password: Option<String>,
190    /// `--bootstrap-cert-out <path>` (issue #1589). When set and the
191    /// first-boot self-bootstrap mints an unseal certificate, that
192    /// certificate hex is written only to this file so a distroless
193    /// single-machine init can
194    /// read it back for the subsequent unseal via `REDDB_CERTIFICATE_FILE`
195    /// without scraping machine logs. Written only on the
196    /// bootstrap-creating boot; a re-boot against an existing vault does
197    /// not rewrite it.
198    pub cert_out: Option<PathBuf>,
199}
200
201#[derive(Debug, Clone)]
202pub struct SystemdServiceConfig {
203    pub service_name: String,
204    pub binary_path: PathBuf,
205    pub run_user: String,
206    pub run_group: String,
207    pub data_path: PathBuf,
208    pub router_bind_addr: Option<String>,
209    pub grpc_bind_addr: Option<String>,
210    pub http_bind_addr: Option<String>,
211}
212
213impl SystemdServiceConfig {
214    pub fn data_dir(&self) -> PathBuf {
215        self.data_path
216            .parent()
217            .map(PathBuf::from)
218            .unwrap_or_else(|| PathBuf::from("."))
219    }
220
221    pub fn unit_path(&self) -> PathBuf {
222        PathBuf::from(format!("/etc/systemd/system/{}.service", self.service_name))
223    }
224}
225
226/// Build a sane default `TelemetryConfig` from a server path when the
227/// caller didn't set one explicitly. Writes rotating logs into the
228/// parent directory of the DB file (or `./logs` for in-memory /
229/// pathless runs). Level defaults to `info`, pretty stderr format.
230pub fn default_telemetry_for_path(
231    path: Option<&std::path::Path>,
232) -> crate::telemetry::TelemetryConfig {
233    let log_dir = match path {
234        Some(p) => p
235            .parent()
236            .map(|parent| parent.join("logs"))
237            .or_else(|| Some(std::path::PathBuf::from("./logs"))),
238        None => None, // in-memory — no file, stderr-only
239    };
240    crate::telemetry::TelemetryConfig {
241        log_dir,
242        file_prefix: "reddb.log".to_string(),
243        level_filter: std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()),
244        format: if std::io::IsTerminal::is_terminal(&std::io::stderr()) {
245            crate::telemetry::LogFormat::Pretty
246        } else {
247            crate::telemetry::LogFormat::Json
248        },
249        rotation_keep_days: 14,
250        service_name: "reddb",
251        // Implicit defaults — no CLI flag has claimed these values yet.
252        level_explicit: false,
253        format_explicit: false,
254        rotation_keep_days_explicit: false,
255        file_prefix_explicit: false,
256        log_dir_explicit: false,
257        log_file_disabled: false,
258    }
259}
260
261/// Metadata key used to thread the parsed backup config from
262/// `to_db_options` down to runner threads. The runner reads it back
263/// (via `runner_backup_intervals`) to spawn the periodic checkpointer
264/// and WAL-flush tasks. Threading through `metadata` avoids extending
265/// `RedDBOptions` with a public field that has no meaning for
266/// library consumers.
267const BACKUP_INTERVAL_META_CHECKPOINT: &str = "red.boot.backup.checkpoint_interval_secs";
268const BACKUP_INTERVAL_META_WAL_FLUSH: &str = "red.boot.backup.wal_flush_interval_secs";
269const BACKUP_KIND_META: &str = "red.boot.backup.backend_kind";
270/// Issue #519 — threaded through `metadata` like the existing interval
271/// values. `0` (default) means "feature disabled" and the runner skips
272/// the lag-monitor wiring entirely.
273const BACKUP_PAUSE_ON_LAG_META: &str = "red.boot.backup.pause_on_lag_secs";
274
275/// Issue #663 — metadata key set by `to_db_options` when the operator
276/// passes `--no-auth` / `--dev`. Read in `build_runtime_with_telemetry`
277/// to (a) skip `AuthStore::bootstrap_from_env` (so a stray
278/// `REDDB_USERNAME`/`REDDB_PASSWORD` cannot auto-create an admin) and
279/// (b) emit the loud "auth disabled" warning. Threaded via `metadata`
280/// — rather than a public `RedDBOptions` field — to keep the flag a
281/// CLI/boot concern with no meaning for library consumers.
282pub(crate) const NO_AUTH_META: &str = "red.boot.no_auth";
283
284/// Returns `true` when `--no-auth` / `--dev` was active for this boot,
285/// i.e. when `to_db_options` stamped [`NO_AUTH_META`] on `options.metadata`.
286pub(crate) fn no_auth_active(options: &RedDBOptions) -> bool {
287    options
288        .metadata
289        .get(NO_AUTH_META)
290        .map(|v| v == "true")
291        .unwrap_or(false)
292}
293
294/// Loud, unmissable warning printed when the operator opts into
295/// anonymous access via `--no-auth` / `--dev`. Goes to stderr (always
296/// visible at startup) **and** the tracing layer (captured by file
297/// logs once telemetry is wired).
298const NO_AUTH_WARNING: &str =
299    "⚠ auth disabled (--no-auth) — anonymous access, do NOT use in production";
300
301impl ServerCommandConfig {
302    fn to_db_options(&self) -> Result<RedDBOptions, String> {
303        let mut options = match &self.path {
304            Some(path) => RedDBOptions::persistent(path),
305            None => RedDBOptions::in_memory(),
306        };
307
308        options.mode = StorageMode::Persistent;
309        options.create_if_missing = self.create_if_missing;
310        options.bounded_disk_residency = self.bounded_disk_residency;
311        // PLAN.md Phase 4.3 — read_only resolution priority:
312        //   1. CLI flag (`--readonly`) — explicit operator intent.
313        //   2. `RED_READONLY=true` env — orchestrator override.
314        //   3. Persisted `<data>/.runtime-state.json` from a prior
315        //      `POST /admin/readonly` — survives restart.
316        //   4. Default `false`.
317        options.read_only = self.read_only
318            || env_nonempty("RED_READONLY")
319                .or_else(|| env_nonempty("REDDB_READONLY"))
320                .map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on"))
321                .unwrap_or(false)
322            || self.path.as_ref().is_some_and(|data_path| {
323                crate::server::handlers_admin::load_runtime_readonly(std::path::Path::new(
324                    data_path,
325                ))
326                .unwrap_or(false)
327            });
328
329        options.replication = match self.role.as_str() {
330            "primary" => ReplicationConfig::primary(),
331            "replica" => {
332                let primary_addr = self
333                    .primary_addr
334                    .clone()
335                    .unwrap_or_else(|| "http://127.0.0.1:55055".to_string());
336                // Public-mutation rejection on replicas is enforced by
337                // `WriteGate` at the runtime/RPC boundary (PLAN.md W1).
338                // Leaving `options.read_only = false` keeps the pager
339                // writable so the internal logical-WAL apply path can
340                // ingest records from the primary; WriteGate ensures no
341                // client request reaches storage.
342                ReplicationConfig::replica(primary_addr)
343            }
344            _ => ReplicationConfig::standalone(),
345        };
346        options.storage_profile = self.storage_profile.validate()?;
347
348        let no_auth = self.no_auth || env_truthy("REDDB_NO_AUTH") || env_truthy("REDDB_DEV");
349        let preset = self.bootstrap.resolved_preset_for_options()?;
350        let preset_requires_auth = matches!(preset.as_str(), PRESET_PRODUCTION | PRESET_CLOUD);
351
352        if self.auth || env_truthy("REDDB_AUTH") || preset_requires_auth {
353            options.auth.enabled = true;
354        }
355        if self.require_auth || env_truthy("REDDB_REQUIRE_AUTH") || preset_requires_auth {
356            options.auth.enabled = true;
357            options.auth.require_auth = true;
358        }
359        if self.vault || env_truthy("REDDB_VAULT") || preset_requires_auth {
360            options.auth.vault_enabled = true;
361        }
362
363        // Issue #663 — `--no-auth` / `--dev` is the last word on auth
364        // for this boot: force every auth knob off, regardless of any
365        // env-derived config (`--vault`, `REDDB_USERNAME`/`PASSWORD`,
366        // OAuth, cert) the operator may also have
367        // set. We *also* stamp [`NO_AUTH_META`] so the auth-store
368        // builder downstream knows to skip `bootstrap_from_env`
369        // (which would otherwise auto-create an admin from
370        // `REDDB_USERNAME`/`REDDB_PASSWORD` even with auth disabled,
371        // a footgun for the local-dev workflow this flag exists to
372        // support).
373        if no_auth {
374            options.auth.enabled = false;
375            options.auth.require_auth = false;
376            options.auth.vault_enabled = false;
377            options
378                .metadata
379                .insert(NO_AUTH_META.to_string(), "true".to_string());
380        }
381
382        // Issue #1587 — first-boot self-bootstrap. A single distroless
383        // `red server` must be able to create the paged vault in place and
384        // serve in one process. The default embedded single-file packaging
385        // routes storage through `UnifiedStore::with_config`, which exposes no
386        // pager, so the vault gate in `build_runtime_with_bootstrap` aborts
387        // with "vault requires a paged database (persistent mode)". The
388        // separate `red bootstrap` subcommand escapes this by opening with the
389        // operational-directory packaging; compose the same storage selection
390        // here so the fresh database is paged, the vault pages exist, and the
391        // existing preset machinery (`ProceedLocal` → `apply_preset_from_config`)
392        // can mint the admins + certificate before listeners come up.
393        //
394        // Gated on (path absent AND a bootstrap intent is present AND the vault
395        // is enabled): an existing database keeps whatever packaging is already
396        // on disk (so a re-boot just serves — no re-bootstrap, no cert churn),
397        // and a fresh path with no intent keeps today's behaviour (a clear
398        // "vault requires a paged database" error rather than a silently
399        // created empty vault). `--no-auth` / `--dev` cleared `vault_enabled`
400        // above, so the development carveout never triggers first-boot either.
401        let bootstrap_intent_present = !matches!(
402            self.bootstrap.auth_bootstrap_input(),
403            crate::cluster::AuthBootstrapInput::None
404        );
405        let path_absent = self.path.as_ref().is_some_and(|path| !path.exists());
406        // Mirrors `embedded_single_file_selected`: only this profile lacks a
407        // pager, so it is the only one that needs promotion to create a vault.
408        let embedded_single_file = options.storage_profile.deploy_profile
409            == crate::storage::DeployProfile::Embedded
410            && options.storage_profile.packaging == crate::storage::StoragePackaging::SingleFile;
411        if options.auth.vault_enabled
412            && bootstrap_intent_present
413            && path_absent
414            && embedded_single_file
415        {
416            // Explicit disk residency already creates a native pager. Keep its
417            // single-file selection instead of routing it through the legacy
418            // snapshot factory's operational-directory bootstrap workaround.
419            if !options.bounded_disk_residency {
420                options.storage_profile.packaging =
421                    crate::storage::StoragePackaging::OperationalDirectory;
422            }
423            options.create_if_missing = true;
424            tracing::info!(
425                target: "reddb::bootstrap",
426                "first-boot self-bootstrap: creating paged vault in place for a fresh database with a bootstrap intent"
427            );
428        }
429
430        // Issue #652 — Control Event Ledger Compliance Mode.
431        // `REDDB_COMPLIANCE_MODE=true|1|yes|on` makes the producer
432        // slices (652b/c/d) fail-closed on ledger persistence
433        // failures. Default `false` — log-and-continue on emit error.
434        if let Some(raw) = env_nonempty("REDDB_COMPLIANCE_MODE") {
435            options.control_events.compliance_mode = matches!(
436                raw.to_ascii_lowercase().as_str(),
437                "1" | "true" | "yes" | "on"
438            );
439        }
440        if preset == PRESET_REGULATED {
441            options.control_events.compliance_mode = true;
442            options.query_audit = crate::runtime::query_audit::QueryAuditConfig::regulated();
443        }
444
445        // Issue #517 — canonical `REDDB_BACKUP_*` contract takes
446        // precedence. On Err, surface the partial-config message so
447        // boot exits non-zero with a clear operator message. On
448        // Ok(None), fall through to the legacy backend-from-env path.
449        match crate::backup_bootstrap::from_env(|k| std::env::var(k).ok()) {
450            Err(msg) => {
451                return Err(format!("backup bootstrap: {msg}"));
452            }
453            Ok(Some(cfg)) => {
454                apply_backup_config(&mut options, &cfg);
455            }
456            Ok(None) => {
457                configure_remote_backend_from_env(&mut options);
458            }
459        }
460
461        if options.remote_backend.is_some()
462            || options
463                .metadata
464                .contains_key(BACKUP_INTERVAL_META_CHECKPOINT)
465        {
466            let mut selection = options.storage_profile;
467            selection.managed_backup = true;
468            options.storage_profile = selection.validate()?;
469        }
470
471        Ok(options)
472    }
473
474    pub fn enabled_transports(&self) -> Vec<ServerTransport> {
475        let mut transports = Vec::with_capacity(3);
476        if self.router_bind_addr.is_some() || self.grpc_bind_addr.is_some() {
477            transports.push(ServerTransport::Grpc);
478        }
479        if self.router_bind_addr.is_some() || self.http_bind_addr.is_some() {
480            transports.push(ServerTransport::Http);
481        }
482        if self.router_bind_addr.is_some() || self.wire_bind_addr.is_some() {
483            transports.push(ServerTransport::Wire);
484        }
485        transports
486    }
487}
488
489/// Read an env var, treating empty / whitespace-only as `None`.
490/// Honors the `<NAME>_FILE` convention. Re-exports the shared
491/// `crate::utils::env_with_file_fallback` helper so call sites in
492/// this module can keep their short local name.
493/// A non-empty env value, also honouring the `<NAME>_FILE` companion for
494/// mounted secrets. Reads the file on demand instead of copying its
495/// contents into the process environment, so the secret never appears in
496/// `/proc/<pid>/environ` or in a child's inherited env.
497fn env_nonempty(name: &str) -> Option<String> {
498    if let Some(value) = crate::utils::env_with_file_fallback(name) {
499        let trimmed = value.trim();
500        if !trimmed.is_empty() {
501            return Some(trimmed.to_string());
502        }
503        return None;
504    }
505    env_nonempty_plain(name)
506}
507
508fn env_nonempty_plain(name: &str) -> Option<String> {
509    crate::utils::env_with_file_fallback(name)
510}
511
512fn env_truthy(name: &str) -> bool {
513    env_nonempty(name)
514        .map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on"))
515        .unwrap_or(false)
516}
517
518impl BootstrapConfig {
519    fn resolved_preset(&self) -> String {
520        self.preset
521            .as_deref()
522            .map(str::trim)
523            .filter(|s| !s.is_empty())
524            .map(str::to_string)
525            .or_else(|| env_nonempty(BOOTSTRAP_PRESET_ENV).or_else(|| env_nonempty(PRESET_ENV)))
526            .unwrap_or_else(|| PRESET_SIMPLE.to_string())
527    }
528
529    fn resolved_preset_for_options(&self) -> Result<String, String> {
530        if let Some(path) = self.selected_manifest() {
531            if let Some(manifest_bootstrap) =
532                crate::cli::bootstrap_manifest::bootstrap_config_from_manifest_file(&path)?
533            {
534                return Ok(manifest_bootstrap.resolved_preset());
535            }
536        }
537        Ok(self.resolved_preset())
538    }
539
540    fn selected_manifest(&self) -> Option<PathBuf> {
541        self.manifest.clone().or_else(|| {
542            env_nonempty(crate::cli::bootstrap_manifest::MANIFEST_ENV).map(PathBuf::from)
543        })
544    }
545
546    /// Resolve the `--bootstrap-cert-out` target (issue #1589): the CLI
547    /// flag wins, else the `REDDB_BOOTSTRAP_CERT_OUT` env (with its
548    /// `_FILE` companion) — matching the flag/env pattern used by the
549    /// other bootstrap inputs.
550    fn selected_cert_out(&self) -> Option<PathBuf> {
551        self.cert_out
552            .clone()
553            .or_else(|| env_nonempty(BOOTSTRAP_CERT_OUT_ENV).map(PathBuf::from))
554    }
555
556    /// Classify the auth bootstrap this config would perform, for the
557    /// cluster bootstrap authority seam (ADR 0058). A manifest wins over
558    /// preset/credential env because it is the broadest authority input.
559    fn auth_bootstrap_input(&self) -> crate::cluster::AuthBootstrapInput {
560        use crate::cluster::AuthBootstrapInput;
561        if self.selected_manifest().is_some() {
562            return AuthBootstrapInput::Manifest;
563        }
564        let preset = self.resolved_preset();
565        let preset_creates_auth = matches!(
566            preset.as_str(),
567            PRESET_PRODUCTION | PRESET_CLOUD | PRESET_REGULATED
568        );
569        // Even the `simple` preset auto-creates an admin when
570        // `REDDB_USERNAME` + `REDDB_PASSWORD` are present (see
571        // `AuthStore::bootstrap_from_env`), so treat credential env as
572        // auth bootstrap input too.
573        let credentials_present =
574            self.production_username().is_some() && self.production_password().is_some();
575        if preset_creates_auth || credentials_present {
576            AuthBootstrapInput::Env
577        } else {
578            AuthBootstrapInput::None
579        }
580    }
581
582    fn explicit_value(value: &Option<String>) -> Option<String> {
583        value
584            .as_deref()
585            .map(str::trim)
586            .filter(|s| !s.is_empty())
587            .map(str::to_string)
588    }
589
590    fn value_or_env(value: &Option<String>, env: &str) -> Option<String> {
591        Self::explicit_value(value).or_else(|| env_nonempty(env))
592    }
593
594    fn production_username(&self) -> Option<String> {
595        Self::value_or_env(&self.admin_username, "REDDB_USERNAME")
596    }
597
598    fn production_password(&self) -> Option<String> {
599        Self::value_or_env(&self.admin_password, "REDDB_PASSWORD")
600    }
601
602    fn cloud_head_username(&self) -> Option<String> {
603        Self::explicit_value(&self.cloud_head_admin)
604            .or_else(|| Self::explicit_value(&self.admin_username))
605            .or_else(|| env_nonempty("REDDB_CLOUD_HEAD_ADMIN"))
606            .or_else(|| env_nonempty("REDDB_USERNAME"))
607    }
608
609    fn cloud_head_password(&self) -> Option<String> {
610        Self::explicit_value(&self.cloud_head_admin_password)
611            .or_else(|| Self::explicit_value(&self.admin_password))
612            .or_else(|| env_nonempty("REDDB_CLOUD_HEAD_ADMIN_PASSWORD"))
613            .or_else(|| env_nonempty("REDDB_PASSWORD"))
614    }
615
616    fn customer_username(&self) -> Option<String> {
617        Self::value_or_env(&self.customer_admin, "REDDB_CUSTOMER_ADMIN")
618    }
619
620    fn customer_password(&self) -> Option<String> {
621        Self::value_or_env(
622            &self.customer_admin_password,
623            "REDDB_CUSTOMER_ADMIN_PASSWORD",
624        )
625    }
626}
627
628/// Apply a parsed [`BackupConfig`] to `options`. Wires the S3
629/// backend via `with_remote_backend` + `with_atomic_remote_backend`
630/// when the `backend-s3` feature is on, stashes intervals + backend
631/// kind in `metadata` so the runner can spawn the periodic tasks,
632/// and emits the startup INFO log required by #517.
633fn apply_backup_config(options: &mut RedDBOptions, cfg: &crate::backup_bootstrap::BackupConfig) {
634    let endpoint_host = endpoint_host(&cfg.endpoint);
635
636    options.metadata.insert(
637        BACKUP_INTERVAL_META_CHECKPOINT.to_string(),
638        cfg.checkpoint_interval_secs.to_string(),
639    );
640    options.metadata.insert(
641        BACKUP_INTERVAL_META_WAL_FLUSH.to_string(),
642        cfg.wal_flush_interval_secs.to_string(),
643    );
644    options
645        .metadata
646        .insert(BACKUP_KIND_META.to_string(), "s3".to_string());
647    options.metadata.insert(
648        BACKUP_PAUSE_ON_LAG_META.to_string(),
649        cfg.pause_on_lag_secs.to_string(),
650    );
651
652    #[cfg(feature = "backend-s3")]
653    {
654        let s3_cfg = crate::storage::backend::S3Config {
655            endpoint: cfg.endpoint.clone(),
656            bucket: cfg.bucket.clone(),
657            key_prefix: cfg.prefix.clone(),
658            access_key: cfg.access_key_id.clone(),
659            secret_key: cfg.secret_access_key.clone(),
660            region: cfg.region.clone(),
661            path_style: true,
662        };
663        let mut backend = crate::storage::backend::S3Backend::new(s3_cfg);
664        if let Some(path) = &cfg.credentials_file {
665            backend = backend.with_credentials_file(path);
666        }
667        let backend = Arc::new(backend);
668        options.remote_backend = Some(backend.clone());
669        options.remote_backend_atomic = Some(backend);
670        // Use the operator-supplied prefix as the snapshot key root.
671        // The existing helpers (`default_snapshot_prefix`,
672        // `default_wal_archive_prefix`) derive sub-prefixes from the
673        // parent of `remote_key`.
674        let trimmed = cfg.prefix.trim_end_matches('/');
675        options.remote_key = Some(reddb_file::remote_database_key(trimmed));
676
677        tracing::info!(
678            backend = "s3",
679            endpoint = %endpoint_host,
680            bucket = %cfg.bucket,
681            prefix = %cfg.prefix,
682            checkpoint_interval_secs = cfg.checkpoint_interval_secs,
683            wal_flush_interval_secs = cfg.wal_flush_interval_secs,
684            "backup backend configured from REDDB_BACKUP_* env"
685        );
686    }
687
688    #[cfg(not(feature = "backend-s3"))]
689    {
690        tracing::warn!(
691            backend = "s3",
692            endpoint = %endpoint_host,
693            bucket = %cfg.bucket,
694            prefix = %cfg.prefix,
695            "REDDB_BACKUP_S3_* configured but binary built without `backend-s3` feature; \
696             backend wiring skipped (archiver/checkpointer also disabled)"
697        );
698    }
699}
700
701fn endpoint_host(endpoint: &str) -> &str {
702    let after_scheme = endpoint
703        .split_once("://")
704        .map(|(_, r)| r)
705        .unwrap_or(endpoint);
706    after_scheme.split('/').next().unwrap_or(after_scheme)
707}
708
709/// If `options` carry backup-task intervals threaded in via
710/// [`apply_backup_config`], spawn periodic checkpointer + WAL-flush
711/// tasks against `runtime`. Returns a `BackupTasksHandle` that
712/// stops the tasks when dropped; runners keep it alive for the
713/// server lifetime.
714fn spawn_backup_tasks_if_configured(
715    options: &RedDBOptions,
716    runtime: &RedDBRuntime,
717) -> Option<BackupTasksHandle> {
718    let checkpoint_secs: u64 = options
719        .metadata
720        .get(BACKUP_INTERVAL_META_CHECKPOINT)?
721        .parse()
722        .ok()?;
723    let wal_secs: u64 = options
724        .metadata
725        .get(BACKUP_INTERVAL_META_WAL_FLUSH)?
726        .parse()
727        .ok()?;
728    // Issue #519 — opt-in graceful read-only when remote archive lag
729    // exceeds the threshold. `0` (default) keeps legacy behaviour.
730    let pause_on_lag_secs: u64 = options
731        .metadata
732        .get(BACKUP_PAUSE_ON_LAG_META)
733        .and_then(|raw| raw.parse().ok())
734        .unwrap_or(0);
735    options.remote_backend.as_ref()?;
736
737    let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
738
739    // Stamp the gate with the threshold + a "now" baseline so the
740    // first auto-pause can only fire after `pause_on_lag_secs` of
741    // archive silence. The poller below re-evaluates on the same
742    // clock the archive-task wrapper uses.
743    if pause_on_lag_secs > 0 {
744        let now_ms = std::time::SystemTime::now()
745            .duration_since(std::time::UNIX_EPOCH)
746            .map(|d| d.as_millis() as u64)
747            .unwrap_or(0);
748        runtime
749            .write_gate()
750            .configure_archive_lag_pause(pause_on_lag_secs, now_ms);
751        tracing::info!(
752            pause_on_lag_secs,
753            "archive-lag pause enabled — engine will transition to read-only after threshold seconds of archiver silence"
754        );
755    }
756
757    let checkpoint_handle = {
758        let stop = Arc::clone(&stop);
759        let runtime = runtime.clone();
760        let interval = Duration::from_secs(checkpoint_secs);
761        thread::Builder::new()
762            .name("red-checkpointer".into())
763            .spawn(move || {
764                periodic_loop(stop, interval, move || {
765                    if let Err(err) = runtime.checkpoint() {
766                        tracing::warn!(error = %err, "periodic checkpoint failed");
767                    }
768                })
769            })
770            .ok()
771    };
772
773    let archiver_handle = {
774        let stop = Arc::clone(&stop);
775        let runtime = runtime.clone();
776        let interval = Duration::from_secs(wal_secs);
777        let lag_enabled = pause_on_lag_secs > 0;
778        thread::Builder::new()
779            .name("red-wal-archiver".into())
780            .spawn(move || {
781                periodic_loop(stop, interval, move || match runtime.trigger_backup() {
782                    Ok(_) if lag_enabled => {
783                        let now_ms = std::time::SystemTime::now()
784                            .duration_since(std::time::UNIX_EPOCH)
785                            .map(|d| d.as_millis() as u64)
786                            .unwrap_or(0);
787                        runtime.write_gate().record_archive_success(now_ms);
788                        // Same-tick re-evaluation: catching up while
789                        // already auto-paused must auto-resume without
790                        // waiting for the poller's cadence.
791                        runtime.write_gate().evaluate_archive_lag(now_ms);
792                    }
793                    Ok(_) => {}
794                    Err(err) => {
795                        tracing::warn!(error = %err, "periodic WAL archive/backup failed");
796                    }
797                })
798            })
799            .ok()
800    };
801
802    // Issue #519 — lag poller. Wakes on a short cadence so a frozen
803    // archiver (the worst case) still flips the gate within ~5s of
804    // crossing the threshold, instead of waiting up to a full
805    // `wal_secs` for the next archive attempt that may never come.
806    let lag_monitor_handle = if pause_on_lag_secs > 0 {
807        let stop = Arc::clone(&stop);
808        let runtime = runtime.clone();
809        // 5s is short enough that the threshold is honoured tightly
810        // and long enough that the atomic loads stay invisible at the
811        // process level.
812        let interval = Duration::from_secs(5);
813        thread::Builder::new()
814            .name("red-archive-lag-monitor".into())
815            .spawn(move || {
816                periodic_loop(stop, interval, move || {
817                    let now_ms = std::time::SystemTime::now()
818                        .duration_since(std::time::UNIX_EPOCH)
819                        .map(|d| d.as_millis() as u64)
820                        .unwrap_or(0);
821                    let was_paused = runtime.write_gate().is_auto_paused();
822                    let now_paused = runtime.write_gate().evaluate_archive_lag(now_ms);
823                    if now_paused && !was_paused {
824                        tracing::warn!(
825                            pause_on_lag_secs,
826                            last_archive_at_ms = runtime.write_gate().last_archive_at_ms(),
827                            "WAL archive lag exceeded threshold — entering graceful read-only mode (issue #519)"
828                        );
829                    } else if !now_paused && was_paused {
830                        tracing::info!(
831                            "WAL archive caught up — exiting graceful read-only mode (issue #519)"
832                        );
833                    }
834                })
835            })
836            .ok()
837    } else {
838        None
839    };
840
841    tracing::info!(
842        checkpoint_interval_secs = checkpoint_secs,
843        wal_flush_interval_secs = wal_secs,
844        "backup tasks spawned (checkpointer + WAL archiver)"
845    );
846
847    Some(BackupTasksHandle {
848        stop,
849        _checkpoint_handle: checkpoint_handle,
850        _archiver_handle: archiver_handle,
851        _lag_monitor_handle: lag_monitor_handle,
852    })
853}
854
855/// Shutdown handle for the periodic checkpointer + archiver tasks.
856/// Drop signals both loops to exit on their next wake.
857pub struct BackupTasksHandle {
858    stop: Arc<std::sync::atomic::AtomicBool>,
859    _checkpoint_handle: Option<thread::JoinHandle<()>>,
860    _archiver_handle: Option<thread::JoinHandle<()>>,
861    /// Issue #519 — periodic archive-lag poller, only spawned when
862    /// `REDDB_BACKUP_PAUSE_ON_LAG_SECS > 0`.
863    _lag_monitor_handle: Option<thread::JoinHandle<()>>,
864}
865
866impl Drop for BackupTasksHandle {
867    fn drop(&mut self) {
868        self.stop.store(true, std::sync::atomic::Ordering::Release);
869    }
870}
871
872fn periodic_loop<F: FnMut()>(
873    stop: Arc<std::sync::atomic::AtomicBool>,
874    interval: Duration,
875    mut tick: F,
876) {
877    // Wake on a short cadence so shutdown is responsive even when the
878    // operator-configured interval is large (e.g. 1h checkpoint).
879    let wake = Duration::from_secs(1);
880    let mut elapsed = Duration::ZERO;
881    while !stop.load(std::sync::atomic::Ordering::Acquire) {
882        thread::sleep(wake);
883        elapsed += wake;
884        if elapsed >= interval {
885            tick();
886            elapsed = Duration::ZERO;
887        }
888    }
889}
890
891fn configure_remote_backend_from_env(options: &mut RedDBOptions) {
892    // PLAN.md (cloud-agnostic) — prefer the new spelling
893    // `RED_BACKEND`; accept the legacy `REDDB_REMOTE_BACKEND` for
894    // existing dev installs. `none` (default) means standalone — no
895    // remote backend, valid for development and on-prem without
896    // remote.
897    let backend = env_nonempty("RED_BACKEND")
898        .or_else(|| env_nonempty("REDDB_REMOTE_BACKEND"))
899        .unwrap_or_else(|| "none".to_string())
900        .to_ascii_lowercase();
901
902    match backend.as_str() {
903        // Universal S3-compatible — covers AWS, R2, MinIO, Ceph,
904        // GCS-interop, B2, DO Spaces, Wasabi, Garage, SeaweedFS,
905        // IDrive, Storj. The `path_style` toggle in S3Config picks
906        // the right addressing for self-hosted vs hosted.
907        "s3" | "minio" | "r2" => {
908            #[cfg(feature = "backend-s3")]
909            {
910                if let Some(config) = s3_config_from_env() {
911                    let remote_key = env_nonempty("RED_REMOTE_KEY")
912                        .or_else(|| env_nonempty("REDDB_REMOTE_KEY"))
913                        .unwrap_or_else(|| reddb_file::remote_database_key("clusters/dev"));
914                    let backend = Arc::new(crate::storage::backend::S3Backend::new(config));
915                    options.remote_backend = Some(backend.clone());
916                    options.remote_backend_atomic = Some(backend);
917                    options.remote_key = Some(remote_key);
918                }
919            }
920            #[cfg(not(feature = "backend-s3"))]
921            {
922                tracing::warn!(
923                    backend = %backend,
924                    "RED_BACKEND={backend} requested but binary was built without `backend-s3` feature"
925                );
926            }
927        }
928        // Filesystem backend (NFS/EFS/SMB/local-disk). PLAN.md spec
929        // calls it `fs`; legacy code shipped it as `local`. Both
930        // names map to LocalBackend, with the remote_key derived
931        // from `RED_FS_PATH` + a per-database suffix when provided.
932        "fs" | "local" => {
933            let base_path = env_nonempty("RED_FS_PATH").or_else(|| env_nonempty("REDDB_FS_PATH"));
934            let remote_key = match (
935                base_path,
936                env_nonempty("RED_REMOTE_KEY").or_else(|| env_nonempty("REDDB_REMOTE_KEY")),
937            ) {
938                (Some(base), Some(rel)) => Some(format!(
939                    "{}/{}",
940                    base.trim_end_matches('/'),
941                    rel.trim_start_matches('/')
942                )),
943                (Some(base), None) => Some(reddb_file::remote_database_key(&format!(
944                    "{}/clusters/dev",
945                    base.trim_end_matches('/')
946                ))),
947                (None, Some(rel)) => Some(rel),
948                (None, None) => None,
949            };
950            if let Some(key) = remote_key {
951                let backend = Arc::new(crate::storage::backend::LocalBackend);
952                options.remote_backend = Some(backend.clone());
953                options.remote_backend_atomic = Some(backend);
954                options.remote_key = Some(key);
955            }
956        }
957        // Generic HTTP backend (PLAN.md Phase 2.3). Maximum
958        // portability: any service exposing PUT/GET/DELETE serves
959        // as a backup target. Optional auth via *_FILE secret
960        // path keeps the token out of the env.
961        "http" => {
962            let base_url = match env_nonempty("RED_HTTP_BACKEND_URL")
963                .or_else(|| env_nonempty("REDDB_HTTP_BACKEND_URL"))
964            {
965                Some(u) => u,
966                None => {
967                    tracing::warn!(
968                        "RED_BACKEND=http requires RED_HTTP_BACKEND_URL — backend disabled"
969                    );
970                    return;
971                }
972            };
973            let prefix = env_nonempty("RED_HTTP_BACKEND_PREFIX")
974                .or_else(|| env_nonempty("REDDB_HTTP_BACKEND_PREFIX"))
975                .unwrap_or_default();
976            let auth_header = if let Some(path) = env_nonempty("RED_HTTP_BACKEND_AUTH_HEADER_FILE")
977                .or_else(|| env_nonempty("REDDB_HTTP_BACKEND_AUTH_HEADER_FILE"))
978            {
979                std::fs::read_to_string(&path)
980                    .ok()
981                    .map(|s| s.trim().to_string())
982                    .filter(|s| !s.is_empty())
983            } else {
984                env_nonempty("RED_HTTP_BACKEND_AUTH_HEADER")
985                    .or_else(|| env_nonempty("REDDB_HTTP_BACKEND_AUTH_HEADER"))
986            };
987
988            let mut config =
989                crate::storage::backend::HttpBackendConfig::new(base_url).with_prefix(prefix);
990            if let Some(auth) = auth_header {
991                config = config.with_auth_header(auth);
992            }
993            let conditional_writes = env_truthy("RED_HTTP_CONDITIONAL_WRITES")
994                || env_truthy("RED_HTTP_BACKEND_CONDITIONAL_WRITES")
995                || env_truthy("REDDB_HTTP_BACKEND_CONDITIONAL_WRITES");
996            config = config.with_conditional_writes(conditional_writes);
997            // Always populate the snapshot-transport handle. When the
998            // operator confirmed CAS support, also populate the atomic
999            // handle via AtomicHttpBackend — without that flag,
1000            // LeaseStore must remain unreachable on this backend.
1001            if conditional_writes {
1002                match crate::storage::backend::AtomicHttpBackend::try_new(config.clone()) {
1003                    Ok(atomic) => {
1004                        let atomic_arc = Arc::new(atomic);
1005                        options.remote_backend = Some(atomic_arc.clone());
1006                        options.remote_backend_atomic = Some(atomic_arc);
1007                    }
1008                    Err(err) => {
1009                        tracing::warn!(error = %err, "AtomicHttpBackend init failed; falling back to plain HTTP (no CAS)");
1010                        options.remote_backend =
1011                            Some(Arc::new(crate::storage::backend::HttpBackend::new(config)));
1012                    }
1013                }
1014            } else {
1015                options.remote_backend =
1016                    Some(Arc::new(crate::storage::backend::HttpBackend::new(config)));
1017            }
1018            options.remote_key = env_nonempty("RED_REMOTE_KEY")
1019                .or_else(|| env_nonempty("REDDB_REMOTE_KEY"))
1020                .or_else(|| Some(reddb_file::remote_database_key("clusters/dev")));
1021        }
1022        // `none` is the explicit standalone — no remote, no backup
1023        // pipeline. Boot never blocks on network reachability.
1024        "none" | "" => {}
1025        other => {
1026            tracing::warn!(
1027                backend = %other,
1028                "unknown RED_BACKEND value — supported: s3 | fs | http | none"
1029            );
1030        }
1031    }
1032}
1033
1034/// Resolve a value from env, accepting both the cloud-agnostic
1035/// `RED_S3_*` family (PLAN.md spec) and the legacy `REDDB_S3_*` form
1036/// kept for existing dev installs. The new spelling wins; the
1037/// legacy spelling is read with a warning hint in callers' logs.
1038#[cfg(feature = "backend-s3")]
1039fn env_s3(suffix: &str) -> Option<String> {
1040    env_nonempty(&format!("RED_S3_{suffix}"))
1041        .or_else(|| env_nonempty(&format!("REDDB_S3_{suffix}")))
1042}
1043
1044/// Read a secret value from either the literal env var or a file
1045/// path supplied via `*_FILE` (PLAN.md spec — compatible with
1046/// Kubernetes / Docker Secrets, HashiCorp Vault Agent, sealed-
1047/// secrets). The `_FILE` variant wins so an operator can set it to
1048/// override the inline value without touching the inline env.
1049#[cfg(feature = "backend-s3")]
1050fn env_s3_secret(suffix: &str) -> Option<String> {
1051    let file_key_red = format!("RED_S3_{suffix}_FILE");
1052    let file_key_legacy = format!("REDDB_S3_{suffix}_FILE");
1053    if let Some(path) = env_nonempty(&file_key_red).or_else(|| env_nonempty(&file_key_legacy)) {
1054        return std::fs::read_to_string(&path)
1055            .ok()
1056            .map(|s| s.trim().to_string())
1057            .filter(|s| !s.is_empty());
1058    }
1059    env_s3(suffix)
1060}
1061
1062#[cfg(feature = "backend-s3")]
1063fn s3_config_from_env() -> Option<crate::storage::backend::S3Config> {
1064    let endpoint = env_s3("ENDPOINT")?;
1065    let bucket = env_s3("BUCKET")?;
1066    let access_key = env_s3_secret("ACCESS_KEY")?;
1067    let secret_key = env_s3_secret("SECRET_KEY")?;
1068    let region = env_s3("REGION").unwrap_or_else(|| "us-east-1".to_string());
1069    let key_prefix = env_s3("KEY_PREFIX")
1070        .or_else(|| env_s3("PREFIX"))
1071        .unwrap_or_default();
1072    let path_style = env_s3("PATH_STYLE")
1073        .map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on"))
1074        .unwrap_or(true);
1075    Some(crate::storage::backend::S3Config {
1076        endpoint,
1077        bucket,
1078        key_prefix,
1079        access_key,
1080        secret_key,
1081        region,
1082        path_style,
1083    })
1084}
1085
1086pub fn render_systemd_unit(config: &SystemdServiceConfig) -> String {
1087    let data_dir = config.data_dir();
1088    let exec_start = render_systemd_exec_start(config);
1089    format!(
1090        "[Unit]\n\
1091Description=RedDB unified database service\n\
1092After=network-online.target\n\
1093Wants=network-online.target\n\
1094\n\
1095[Service]\n\
1096Type=simple\n\
1097User={user}\n\
1098Group={group}\n\
1099WorkingDirectory={workdir}\n\
1100ExecStart={exec_start}\n\
1101Restart=always\n\
1102RestartSec=2\n\
1103LimitSTACK=16M\n\
1104NoNewPrivileges=true\n\
1105PrivateTmp=true\n\
1106ProtectSystem=strict\n\
1107ProtectHome=true\n\
1108ProtectControlGroups=true\n\
1109ProtectKernelTunables=true\n\
1110ProtectKernelModules=true\n\
1111RestrictNamespaces=true\n\
1112LockPersonality=true\n\
1113MemoryDenyWriteExecute=true\n\
1114ReadWritePaths={workdir}\n\
1115\n\
1116[Install]\n\
1117WantedBy=multi-user.target\n",
1118        user = config.run_user,
1119        group = config.run_group,
1120        workdir = data_dir.display(),
1121        exec_start = exec_start,
1122    )
1123}
1124
1125/// Install a systemd unit + start the service.
1126///
1127/// Linux-only. The helper shells out to `systemctl`, `useradd`,
1128/// `groupadd`, `install`, `getent`, and `id` — none of which exist on
1129/// Windows or macOS. The Windows/macOS branch returns a hard error so
1130/// callers (the CLI) surface a clear message instead of a confusing
1131/// syscall failure. A proper Windows-service equivalent (sc.exe /
1132/// NSSM) is a Phase 3.6 follow-up.
1133#[cfg(target_os = "linux")]
1134pub fn install_systemd_service(config: &SystemdServiceConfig) -> Result<(), String> {
1135    ensure_root()?;
1136    ensure_command_available("systemctl")?;
1137    ensure_command_available("getent")?;
1138    ensure_command_available("groupadd")?;
1139    ensure_command_available("useradd")?;
1140    ensure_command_available("install")?;
1141    ensure_executable(&config.binary_path)?;
1142
1143    if !command_success("getent", ["group", config.run_group.as_str()])? {
1144        run_command("groupadd", ["--system", config.run_group.as_str()])?;
1145    }
1146
1147    if !command_success("id", ["-u", config.run_user.as_str()])? {
1148        let data_dir = config.data_dir();
1149        run_command(
1150            "useradd",
1151            [
1152                "--system",
1153                "--gid",
1154                config.run_group.as_str(),
1155                "--home-dir",
1156                data_dir.to_string_lossy().as_ref(),
1157                "--shell",
1158                "/usr/sbin/nologin",
1159                config.run_user.as_str(),
1160            ],
1161        )?;
1162    }
1163
1164    let data_dir = config.data_dir();
1165    run_command(
1166        "install",
1167        [
1168            "-d",
1169            "-o",
1170            config.run_user.as_str(),
1171            "-g",
1172            config.run_group.as_str(),
1173            "-m",
1174            "0750",
1175            data_dir.to_string_lossy().as_ref(),
1176        ],
1177    )?;
1178
1179    std::fs::write(config.unit_path(), render_systemd_unit(config))
1180        .map_err(|err| format!("failed to write systemd unit: {err}"))?;
1181
1182    run_command("systemctl", ["daemon-reload"])?;
1183    run_command(
1184        "systemctl",
1185        [
1186            "enable",
1187            "--now",
1188            format!("{}.service", config.service_name).as_str(),
1189        ],
1190    )?;
1191
1192    Ok(())
1193}
1194
1195/// Non-Linux fallback — systemd is Linux-specific. Keep the signature
1196/// identical so callers compile on every platform; surface a clear
1197/// error at runtime. Windows/macOS service-manager integration is a
1198/// Phase 3.6 follow-up (sc.exe + NSSM on Windows, launchd on macOS).
1199#[cfg(not(target_os = "linux"))]
1200pub fn install_systemd_service(_config: &SystemdServiceConfig) -> Result<(), String> {
1201    Err("systemd install is Linux-only — use sc.exe (Windows) or \
1202         launchd (macOS) to install the service manually using the \
1203         unit printed by `red service print-unit`"
1204        .to_string())
1205}
1206
1207#[cfg(target_os = "linux")]
1208fn ensure_root() -> Result<(), String> {
1209    let output = Command::new("id")
1210        .arg("-u")
1211        .output()
1212        .map_err(|err| format!("failed to determine current uid: {err}"))?;
1213    if !output.status.success() {
1214        return Err("failed to determine current uid".to_string());
1215    }
1216    let uid = String::from_utf8_lossy(&output.stdout);
1217    if uid.trim() != "0" {
1218        return Err("run this command as root (sudo)".to_string());
1219    }
1220    Ok(())
1221}
1222
1223#[cfg(target_os = "linux")]
1224fn ensure_command_available(command: &str) -> Result<(), String> {
1225    let status = Command::new("sh")
1226        .args(["-lc", &format!("command -v {command} >/dev/null 2>&1")])
1227        .status()
1228        .map_err(|err| format!("failed to check command '{command}': {err}"))?;
1229    if status.success() {
1230        Ok(())
1231    } else {
1232        Err(format!("required command not found: {command}"))
1233    }
1234}
1235
1236#[cfg(target_os = "linux")]
1237fn ensure_executable(path: &std::path::Path) -> Result<(), String> {
1238    let metadata = std::fs::metadata(path)
1239        .map_err(|err| format!("binary not found '{}': {err}", path.display()))?;
1240    #[cfg(unix)]
1241    {
1242        use std::os::unix::fs::PermissionsExt;
1243        if metadata.permissions().mode() & 0o111 == 0 {
1244            return Err(format!("binary is not executable: {}", path.display()));
1245        }
1246    }
1247    #[cfg(not(unix))]
1248    {
1249        if !metadata.is_file() {
1250            return Err(format!("binary is not a file: {}", path.display()));
1251        }
1252    }
1253    Ok(())
1254}
1255
1256#[cfg(target_os = "linux")]
1257fn command_success<const N: usize>(program: &str, args: [&str; N]) -> Result<bool, String> {
1258    Command::new(program)
1259        .args(args)
1260        .status()
1261        .map(|status| status.success())
1262        .map_err(|err| format!("failed to run {program}: {err}"))
1263}
1264
1265#[cfg(target_os = "linux")]
1266fn run_command<const N: usize>(program: &str, args: [&str; N]) -> Result<(), String> {
1267    let output = Command::new(program)
1268        .args(args)
1269        .output()
1270        .map_err(|err| format!("failed to run {program}: {err}"))?;
1271    if output.status.success() {
1272        return Ok(());
1273    }
1274
1275    let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
1276    let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
1277    let detail = if !stderr.is_empty() {
1278        stderr
1279    } else if !stdout.is_empty() {
1280        stdout
1281    } else {
1282        format!("exit status {}", output.status)
1283    };
1284    Err(format!("{program} failed: {detail}"))
1285}
1286
1287fn render_systemd_exec_start(config: &SystemdServiceConfig) -> String {
1288    let mut parts = vec![
1289        config.binary_path.display().to_string(),
1290        "server".to_string(),
1291        "--path".to_string(),
1292        config.data_path.display().to_string(),
1293    ];
1294
1295    if let Some(bind_addr) = &config.router_bind_addr {
1296        parts.push("--bind".to_string());
1297        parts.push(bind_addr.clone());
1298    } else if let Some(bind_addr) = &config.grpc_bind_addr {
1299        parts.push("--grpc-bind".to_string());
1300        parts.push(bind_addr.clone());
1301    }
1302    if let Some(bind_addr) = &config.http_bind_addr {
1303        parts.push("--http-bind".to_string());
1304        parts.push(bind_addr.clone());
1305    }
1306
1307    parts.join(" ")
1308}
1309
1310pub fn probe_listener(target: &str, timeout: Duration) -> bool {
1311    let addresses: Vec<SocketAddr> = match target.to_socket_addrs() {
1312        Ok(addresses) => addresses.collect(),
1313        Err(_) => return false,
1314    };
1315
1316    addresses
1317        .into_iter()
1318        .any(|address| TcpStream::connect_timeout(&address, timeout).is_ok())
1319}
1320
1321/// Bind a TCP listener for a transport at startup and record the
1322/// outcome in the shared [`TransportReadiness`] state.
1323///
1324/// The split between *explicit* and *implicit/default* binds is the
1325/// contract from issue #545:
1326///
1327/// * `explicit == true` — the operator named this transport on the
1328///   CLI / env / config. A failed bind is fatal: this returns `Err`
1329///   and the boot path must propagate the error so the process exits
1330///   non-zero with the recorded `reason`.
1331/// * `explicit == false` — this is a default listener the server
1332///   would have spun up regardless. A failed bind degrades: this
1333///   returns `Ok(None)` (no listener) but the failure is still
1334///   recorded in `readiness.failed`, so the server keeps running and
1335///   the next `/health` probe enumerates the degraded listener.
1336///
1337/// On success the bound listener lands in `readiness.active`.
1338pub fn bind_listener_for_startup(
1339    readiness: &mut TransportReadiness,
1340    transport: &str,
1341    bind_addr: &str,
1342    explicit: bool,
1343) -> Result<Option<TcpListener>, String> {
1344    match TcpListener::bind(bind_addr) {
1345        Ok(listener) => {
1346            readiness.active(transport, bind_addr, explicit);
1347            Ok(Some(listener))
1348        }
1349        Err(err) => {
1350            let reason = format!("{transport} listener bind {bind_addr}: {err}");
1351            readiness.failed(transport, bind_addr, explicit, reason.clone());
1352            if explicit {
1353                tracing::error!(
1354                    transport,
1355                    bind = %bind_addr,
1356                    error = %err,
1357                    "fatal explicit bind failure"
1358                );
1359                Err(format!("explicit {reason}"))
1360            } else {
1361                tracing::warn!(
1362                    transport,
1363                    bind = %bind_addr,
1364                    error = %err,
1365                    "non-fatal implicit bind failure; listener degraded"
1366                );
1367                Ok(None)
1368            }
1369        }
1370    }
1371}
1372
1373/// Wire SIGTERM and SIGINT to `RedDBRuntime::graceful_shutdown`.
1374///
1375/// PLAN.md Phase 1.1 — orchestrators (K8s preStop, Fly autostop, ECS
1376/// drain, systemd, plain `docker stop`) all rely on SIGTERM with a
1377/// grace window. SIGKILL after that grace window is the OS's
1378/// fallback; we are responsible for finishing in time.
1379///
1380/// Spawns a tokio task on the caller's runtime that:
1381///   1. Awaits the first of SIGTERM / SIGINT.
1382///   2. Calls `runtime.graceful_shutdown(backup_on_shutdown)`. The
1383///      runtime moves to `Stopped` on its own; this just runs the
1384///      flush + checkpoint pipeline and (optionally) a final backup.
1385///   3. Calls `std::process::exit(0)` so the orchestrator sees a
1386///      clean exit code.
1387///
1388/// `RED_BACKUP_ON_SHUTDOWN` (default `true` if a remote backend is
1389/// configured) toggles step 3's backup branch. The flush + checkpoint
1390/// always run.
1391///
1392/// Idempotent across signal storms — `graceful_shutdown` returns the
1393/// cached report on second call, but we exit on the first one
1394/// regardless, so the second SIGTERM never reaches the handler.
1395async fn spawn_lifecycle_signal_handler(runtime: RedDBRuntime) {
1396    let backup_on_shutdown = std::env::var("RED_BACKUP_ON_SHUTDOWN")
1397        .ok()
1398        .map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on"))
1399        .unwrap_or(true);
1400
1401    #[cfg(unix)]
1402    {
1403        use tokio::signal::unix::{signal, SignalKind};
1404
1405        let mut sigterm = match signal(SignalKind::terminate()) {
1406            Ok(s) => s,
1407            Err(err) => {
1408                tracing::warn!(
1409                    error = %err,
1410                    "could not install SIGTERM handler; orchestrator graceful shutdown will fall back to SIGKILL"
1411                );
1412                return;
1413            }
1414        };
1415        let mut sigint = match signal(SignalKind::interrupt()) {
1416            Ok(s) => s,
1417            Err(err) => {
1418                tracing::warn!(error = %err, "could not install SIGINT handler");
1419                return;
1420            }
1421        };
1422        // PLAN.md Phase 6.4 — SIGHUP triggers a reload of secrets from
1423        // their `_FILE` companions without restarting the process.
1424        // Useful for credential rotation pipelines (kubectl create
1425        // secret + kubectl rollout restart, but for systemd / Nomad /
1426        // bare-metal where rolling the process is heavier).
1427        let mut sighup = match signal(SignalKind::hangup()) {
1428            Ok(s) => Some(s),
1429            Err(err) => {
1430                tracing::warn!(error = %err, "could not install SIGHUP handler; secret reload via signal disabled");
1431                None
1432            }
1433        };
1434
1435        let reload_runtime = runtime.clone();
1436        tokio::spawn(async move {
1437            loop {
1438                let signal_name = match &mut sighup {
1439                    Some(hup) => tokio::select! {
1440                        _ = sigterm.recv() => "SIGTERM",
1441                        _ = sigint.recv() => "SIGINT",
1442                        _ = hup.recv() => "SIGHUP",
1443                    },
1444                    None => tokio::select! {
1445                        _ = sigterm.recv() => "SIGTERM",
1446                        _ = sigint.recv() => "SIGINT",
1447                    },
1448                };
1449
1450                if signal_name == "SIGHUP" {
1451                    handle_sighup_reload(&reload_runtime);
1452                    continue; // stay alive; SIGHUP isn't a shutdown
1453                }
1454
1455                tracing::info!(
1456                    signal = signal_name,
1457                    "lifecycle signal received; shutting down"
1458                );
1459                match runtime.graceful_shutdown(backup_on_shutdown) {
1460                    Ok(report) => {
1461                        tracing::info!(
1462                            duration_ms = report.duration_ms,
1463                            flushed_wal = report.flushed_wal,
1464                            final_checkpoint = report.final_checkpoint,
1465                            backup_uploaded = report.backup_uploaded,
1466                            "graceful shutdown complete"
1467                        );
1468                    }
1469                    Err(err) => {
1470                        tracing::error!(error = %err, "graceful shutdown failed");
1471                        // Issue #205 — graceful shutdown returning Err
1472                        // means the runtime is exiting without a clean
1473                        // flush/checkpoint. Operator-grade event so the
1474                        // operator notices the dirty exit even when the
1475                        // process restarts before they read tracing logs.
1476                        crate::telemetry::operator_event::OperatorEvent::ShutdownForced {
1477                            reason: format!("graceful shutdown failed: {err}"),
1478                        }
1479                        .emit_global();
1480                    }
1481                }
1482                std::process::exit(0);
1483            }
1484        });
1485    }
1486
1487    #[cfg(not(unix))]
1488    {
1489        tokio::spawn(async move {
1490            let interrupted = tokio::signal::ctrl_c().await;
1491            if let Err(err) = interrupted {
1492                tracing::warn!(error = %err, "could not install Ctrl+C handler");
1493                return;
1494            }
1495
1496            tracing::info!(
1497                signal = "Ctrl+C",
1498                "lifecycle signal received; shutting down"
1499            );
1500            match runtime.graceful_shutdown(backup_on_shutdown) {
1501                Ok(report) => {
1502                    tracing::info!(
1503                        duration_ms = report.duration_ms,
1504                        flushed_wal = report.flushed_wal,
1505                        final_checkpoint = report.final_checkpoint,
1506                        backup_uploaded = report.backup_uploaded,
1507                        "graceful shutdown complete"
1508                    );
1509                }
1510                Err(err) => {
1511                    tracing::error!(error = %err, "graceful shutdown failed");
1512                }
1513            }
1514            std::process::exit(0);
1515        });
1516    }
1517}
1518
1519/// PLAN.md Phase 6.4 — re-read secrets from `*_FILE` companion env
1520/// vars. Today this only refreshes the audit log + records the
1521/// reload event; the runtime modules that hold cached secret
1522/// material (S3 backend credentials, admin token, JWT keys) read
1523/// the env on each request so the next call after SIGHUP picks up
1524/// the new file contents automatically. A future extension can
1525/// punch through to the LeaseStore / AuthStore for in-memory
1526/// caches that don't re-read on each call.
1527fn handle_sighup_reload(runtime: &RedDBRuntime) {
1528    let now_ms = std::time::SystemTime::now()
1529        .duration_since(std::time::UNIX_EPOCH)
1530        .map(|d| d.as_millis() as u64)
1531        .unwrap_or(0);
1532    tracing::info!(
1533        target: "reddb::secrets",
1534        ts_unix_ms = now_ms,
1535        "SIGHUP received; secrets will be re-read from *_FILE on next access"
1536    );
1537    // Routed through AuditFieldEscaper (ADR 0010 / issue #177) so
1538    // every emission goes through the typed-field guard. The
1539    // arguments here are static, but using the typed entry point
1540    // keeps the discipline uniform across call sites.
1541    use crate::runtime::audit_log::{AuditAuthSource, AuditEvent, AuditFieldEscaper, Outcome};
1542    runtime.audit_log().record_event(
1543        AuditEvent::builder("config/sighup_reload")
1544            .source(AuditAuthSource::System)
1545            .resource("secrets")
1546            .outcome(Outcome::Success)
1547            .field(AuditFieldEscaper::field("ts_unix_ms", now_ms))
1548            .build(),
1549    );
1550}
1551
1552/// Resolve the TLS material the PG-Wire listener shares with the native
1553/// wire transport, if any.
1554///
1555/// Policy: when the operator has enabled wire TLS — an explicit cert/key
1556/// pair or a `--wire-tls-bind` that auto-generates a self-signed dev cert
1557/// — the PG-Wire listener answers `SSLRequest` with `'S'` over the *same*
1558/// cert config (`resolve_wire_tls_config`) rather than inventing a
1559/// parallel surface. With no wire TLS configured the listener stays
1560/// plaintext and declines `SSLRequest` with `'N'`. A config error degrades
1561/// to plaintext rather than killing the listener.
1562fn resolve_pg_wire_tls(config: &ServerCommandConfig) -> Option<crate::wire::WireTlsConfig> {
1563    let tls_configured = config.wire_tls_bind_addr.is_some()
1564        || (config.wire_tls_cert.is_some() && config.wire_tls_key.is_some());
1565    if !tls_configured {
1566        return None;
1567    }
1568    match resolve_wire_tls_config(config) {
1569        Ok(cfg) => Some(cfg),
1570        Err(e) => {
1571            tracing::error!(
1572                err = %e,
1573                "pg wire TLS config error; listener will run plaintext"
1574            );
1575            None
1576        }
1577    }
1578}
1579
1580/// Resolve gRPC TLS material into PEM bytes.
1581///
1582/// Lookup order, in priority:
1583///   1. Explicit `config.grpc_tls_cert` / `config.grpc_tls_key` (paths
1584///      passed via CLI/env). Both must be present together.
1585///   2. `RED_GRPC_TLS_DEV=1` — auto-generate a self-signed cert next
1586///      to the data dir. Refuses to run without the env flag so an
1587///      operator can't accidentally ship a dev cert in prod.
1588///
1589/// `client_ca` is loaded when `config.grpc_tls_client_ca` is set,
1590/// turning the listener into a mutual-TLS endpoint that requires
1591/// every client to present a chain that anchors at one of the CAs
1592/// in the bundle.
1593fn resolve_grpc_tls_options(config: &ServerCommandConfig) -> Result<crate::GrpcTlsOptions, String> {
1594    let (cert_pem, key_pem) = match (&config.grpc_tls_cert, &config.grpc_tls_key) {
1595        (Some(cert), Some(key)) => {
1596            let cert_pem = std::fs::read(cert)
1597                .map_err(|e| format!("read grpc cert {}: {e}", cert.display()))?;
1598            let key_pem =
1599                std::fs::read(key).map_err(|e| format!("read grpc key {}: {e}", key.display()))?;
1600            (cert_pem, key_pem)
1601        }
1602        _ => {
1603            // No explicit material → only proceed when dev-mode is on.
1604            let dev = std::env::var("RED_GRPC_TLS_DEV")
1605                .ok()
1606                .map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on"))
1607                .unwrap_or(false);
1608            if !dev {
1609                return Err("gRPC TLS configured but no cert/key supplied — set \
1610                     REDDB_GRPC_TLS_CERT / REDDB_GRPC_TLS_KEY (or \
1611                     RED_GRPC_TLS_DEV=1 to auto-generate a self-signed cert)"
1612                    .to_string());
1613            }
1614            let dir = config
1615                .path
1616                .as_ref()
1617                .and_then(|p| p.parent())
1618                .map(PathBuf::from)
1619                .unwrap_or_else(|| PathBuf::from("."));
1620            let (cert_pem_str, key_pem_str) =
1621                crate::wire::tls::generate_self_signed_cert("localhost")
1622                    .map_err(|e| format!("auto-generate dev grpc cert: {e}"))?;
1623
1624            // Constant-time-friendly fingerprint to stderr so the
1625            // operator can pin a client trust store. We log via
1626            // `tracing::warn!` so it stands out next to ordinary
1627            // listener-online events.
1628            let fp = sha256_pem_fingerprint(cert_pem_str.as_bytes());
1629            tracing::warn!(
1630                target: "reddb::security",
1631                transport = "grpc",
1632                cert_sha256 = %fp,
1633                "RED_GRPC_TLS_DEV=1: using auto-generated self-signed cert; \
1634                 DO NOT use in production"
1635            );
1636            // Persist so that restarts reuse the same identity.
1637            let cert_path = dir.join("grpc-tls-cert.pem");
1638            let key_path = dir.join("grpc-tls-key.pem");
1639            if !cert_path.exists() || !key_path.exists() {
1640                let _ = std::fs::create_dir_all(&dir);
1641                std::fs::write(&cert_path, cert_pem_str.as_bytes())
1642                    .map_err(|e| format!("write grpc dev cert: {e}"))?;
1643                std::fs::write(&key_path, key_pem_str.as_bytes())
1644                    .map_err(|e| format!("write grpc dev key: {e}"))?;
1645                #[cfg(unix)]
1646                {
1647                    use std::os::unix::fs::PermissionsExt;
1648                    let _ =
1649                        std::fs::set_permissions(&key_path, std::fs::Permissions::from_mode(0o600));
1650                }
1651            }
1652            (cert_pem_str.into_bytes(), key_pem_str.into_bytes())
1653        }
1654    };
1655
1656    let client_ca_pem = match &config.grpc_tls_client_ca {
1657        Some(path) => Some(
1658            std::fs::read(path)
1659                .map_err(|e| format!("read grpc client CA {}: {e}", path.display()))?,
1660        ),
1661        None => None,
1662    };
1663
1664    Ok(crate::GrpcTlsOptions {
1665        cert_pem,
1666        key_pem,
1667        client_ca_pem,
1668    })
1669}
1670
1671/// Hex-encoded SHA-256 of a PEM blob, used for cert-pin operator log
1672/// lines. Constant-time hash; no token contents leave this fn.
1673fn sha256_pem_fingerprint(pem: &[u8]) -> String {
1674    use sha2::{Digest, Sha256};
1675    let mut h = Sha256::new();
1676    h.update(pem);
1677    let d = h.finalize();
1678    let mut buf = String::with_capacity(64);
1679    for b in d.iter() {
1680        buf.push_str(&format!("{b:02x}"));
1681    }
1682    buf
1683}
1684
1685/// Resolve TLS config: use provided cert/key or auto-generate.
1686fn resolve_wire_tls_config(
1687    config: &ServerCommandConfig,
1688) -> Result<crate::wire::WireTlsConfig, String> {
1689    match (&config.wire_tls_cert, &config.wire_tls_key) {
1690        (Some(cert), Some(key)) => Ok(crate::wire::WireTlsConfig {
1691            cert_path: cert.clone(),
1692            key_path: key.clone(),
1693        }),
1694        _ => {
1695            // Auto-generate self-signed cert
1696            let dir = config
1697                .path
1698                .as_ref()
1699                .and_then(|p| p.parent())
1700                .map(PathBuf::from)
1701                .unwrap_or_else(|| PathBuf::from("."));
1702            crate::wire::tls::auto_generate_cert(&dir).map_err(|e| e.to_string())
1703        }
1704    }
1705}
1706
1707#[inline(never)]
1708fn build_runtime_and_auth_store(
1709    config: &ServerCommandConfig,
1710    db_options: RedDBOptions,
1711) -> Result<
1712    (
1713        RedDBRuntime,
1714        Arc<AuthStore>,
1715        Option<crate::telemetry::TelemetryGuard>,
1716    ),
1717    String,
1718> {
1719    // Return the TelemetryGuard so server runners can bind it for
1720    // their full lifetime. Dropping the guard tears down the
1721    // non-blocking log writer thread and, because that writer is
1722    // built with `.lossy(true)`, any subsequent log event routed to
1723    // the file sink is silently dropped — so callers MUST keep the
1724    // returned `Option<TelemetryGuard>` alive until shutdown.
1725    build_runtime_with_bootstrap(db_options, config.telemetry.clone(), &config.bootstrap)
1726}
1727
1728/// Open the runtime, initialise structured logging from merged CLI +
1729/// `red_config` settings, and return a guard the caller must keep
1730/// alive for the server lifetime (drop flushes pending log writes).
1731///
1732/// Merge priority: CLI flag (explicit `Some`) beats `red.logging.*`
1733/// in red_config, beats the built-in default. A CLI-flag value of
1734/// `None` / empty means "inherit from config or default" — never
1735/// "disable". The one exception is `--no-log-file` which forces
1736/// `log_dir = None` regardless of config.
1737pub(crate) fn build_runtime_with_telemetry(
1738    db_options: RedDBOptions,
1739    cli_telemetry: Option<crate::telemetry::TelemetryConfig>,
1740) -> Result<
1741    (
1742        RedDBRuntime,
1743        Arc<AuthStore>,
1744        Option<crate::telemetry::TelemetryGuard>,
1745    ),
1746    String,
1747> {
1748    let bootstrap = BootstrapConfig::default();
1749    build_runtime_with_bootstrap(db_options, cli_telemetry, &bootstrap)
1750}
1751
1752fn build_runtime_with_bootstrap(
1753    db_options: RedDBOptions,
1754    cli_telemetry: Option<crate::telemetry::TelemetryConfig>,
1755    bootstrap: &BootstrapConfig,
1756) -> Result<
1757    (
1758        RedDBRuntime,
1759        Arc<AuthStore>,
1760        Option<crate::telemetry::TelemetryGuard>,
1761    ),
1762    String,
1763> {
1764    let runtime = RedDBRuntime::with_options(db_options.clone()).map_err(|err| {
1765        // Issue #205 — runtime construction failure is the canonical
1766        // StartupFailed phase. The audit sink isn't installed yet
1767        // (it would have been registered inside `with_options`), so
1768        // the emit falls through to tracing+eprintln only — operator
1769        // still sees it on stderr.
1770        let msg = err.to_string();
1771        crate::telemetry::operator_event::OperatorEvent::StartupFailed {
1772            phase: "runtime_construction".to_string(),
1773            error: msg.clone(),
1774        }
1775        .emit_global();
1776        msg
1777    })?;
1778
1779    // PLAN.md Phase 5 / W6 — opt into serverless writer-lease fencing
1780    // when `RED_LEASE_REQUIRED=true`. Failure here aborts boot: the
1781    // operator asked for a fence; running unfenced would silently
1782    // expose split-brain risk.
1783    crate::runtime::lease_loop::start_lease_loop_if_required(&runtime).map_err(|err| {
1784        let msg = err.to_string();
1785        crate::telemetry::operator_event::OperatorEvent::StartupFailed {
1786            phase: "lease_loop".to_string(),
1787            error: msg.clone(),
1788        }
1789        .emit_global();
1790        msg
1791    })?;
1792
1793    // #213 — edge-triggered disk-space watchdog. Watches the data
1794    // directory; falls back to polling when fanotify is unavailable
1795    // (non-Linux or unprivileged container).
1796    if let Some(data_path) = db_options.data_path.as_deref() {
1797        let watch_dir = data_path.parent().unwrap_or(data_path);
1798        crate::runtime::disk_space_monitor::DiskSpaceMonitor::new(watch_dir, 90).spawn();
1799    }
1800
1801    // #214 — inotify config hot-reload watcher. Watches the config file
1802    // (REDDB_CONFIG_FILE or /etc/reddb/config.json) for changes and
1803    // applies hot-reloadable keys without restart.
1804    {
1805        let config_path = crate::runtime::config_overlay::config_file_path();
1806        let store = runtime.db().store();
1807        crate::runtime::config_watcher::ConfigWatcher::new(config_path, store).spawn();
1808    }
1809
1810    // Phase 6 logging: merge red_config overrides onto the CLI-built
1811    // telemetry config, then install the global subscriber.
1812    let merged = merge_telemetry_with_config(
1813        cli_telemetry
1814            .unwrap_or_else(|| default_telemetry_for_path(db_options.data_path.as_deref())),
1815        &runtime,
1816    );
1817    let telemetry_guard = crate::telemetry::init(merged);
1818    if db_options.bounded_disk_residency {
1819        tracing::info!(
1820            target: "reddb::storage::residency",
1821            resident_memory_bytes_max = runtime.db().store().config().resident_memory_bytes_max,
1822            "bounded disk residency enabled"
1823        );
1824    }
1825
1826    let no_auth = no_auth_active(&db_options);
1827    let auth_store =
1828        if db_options.auth.vault_enabled {
1829            let pager =
1830                runtime.db().store().pager().cloned().ok_or_else(|| {
1831                    "vault requires a paged database (persistent mode)".to_string()
1832                })?;
1833            let store = AuthStore::with_vault(db_options.auth.clone(), pager)
1834                .map_err(|err| err.to_string())?;
1835            Arc::new(store)
1836        } else {
1837            Arc::new(AuthStore::new(db_options.auth.clone()))
1838        };
1839    auth_store.configure_control_events(
1840        runtime.control_event_ledger(),
1841        runtime.control_event_config(),
1842    );
1843    // ADR 0058 — cluster bootstrap authority fail-closed seam. A
1844    // cluster-shaped boot may only run auth bootstrap when a concrete
1845    // reserved-range owner can be proven; none is implemented yet, so the
1846    // seam rejects credentialled cluster boots and allows only the explicit
1847    // `--no-auth` / `--dev` development carveout. Issue #663 — when
1848    // `--no-auth` is active, deliberately skip the preset machinery so a
1849    // stray `REDDB_USERNAME`+`REDDB_PASSWORD` pair cannot silently create an
1850    // admin, defeating the point of anonymous mode.
1851    //
1852    // Issue #1230 — the durable bootstrap completion marker
1853    // (`system.bootstrap.completed`) is the authority path's record that
1854    // first boot already produced global auth state. Observing it makes a
1855    // restart or duplicate bootstrap attempt idempotent: the seam returns
1856    // `AlreadyComplete` and the caller rehydrates read-only state only,
1857    // recreating no users, reissuing no vault certificate, and reapplying no
1858    // mutable config over operator changes.
1859    let bootstrap_already_completed = runtime
1860        .db()
1861        .store()
1862        .try_get_config(BOOTSTRAP_COMPLETED_KEY)
1863        .map_err(|error| error.to_string())?
1864        .is_some();
1865    ensure_persisted_auth_state(
1866        bootstrap_already_completed,
1867        db_options.auth.require_auth,
1868        auth_store.needs_bootstrap(),
1869    )?;
1870    let disposition = crate::cluster::authorize_cluster_bootstrap(
1871        db_options.storage_profile.deploy_profile,
1872        no_auth,
1873        bootstrap.auth_bootstrap_input(),
1874        bootstrap_already_completed,
1875    )?;
1876    // Issue #1231 — wire cluster vault first boot through the bootstrap
1877    // authority. The authorized disposition selects the vault plan: the owner
1878    // path (`ProceedLocal`) opens the vault against the real cluster-global
1879    // auth store (`auth_store`, backed by the runtime pager) and consumes the
1880    // env/`_FILE` secret inputs to mint the certificate; a restart after
1881    // completion (`AlreadyComplete`) reopens and unseals that same store
1882    // without consuming secret inputs. A non-owner cluster boot already failed
1883    // closed above, so no scratch or per-member vault is minted.
1884    let vault_plan = crate::cluster::plan_vault_bootstrap(disposition);
1885    match disposition {
1886        crate::cluster::BootstrapDisposition::SkipDevBypass => {
1887            debug_assert_eq!(vault_plan, crate::cluster::VaultBootstrapPlan::SkipNoVault);
1888            eprintln!("{NO_AUTH_WARNING}");
1889            tracing::warn!("{NO_AUTH_WARNING}");
1890        }
1891        crate::cluster::BootstrapDisposition::AlreadyComplete => {
1892            // First boot is over. Rehydrate the read-only manifest registry
1893            // so config-derived state is observable, but create no users,
1894            // reissue no vault material, and reapply no mutable config
1895            // (issue #1230). `apply_preset_from_config` does exactly this
1896            // when the completion marker is present — matching the unseal-only
1897            // vault plan (no secret inputs consumed).
1898            debug_assert_eq!(
1899                vault_plan,
1900                crate::cluster::VaultBootstrapPlan::OpenClusterGlobalStore {
1901                    consume_secret_inputs: false,
1902                }
1903            );
1904            apply_preset_from_config(&runtime, &auth_store, bootstrap)?;
1905            tracing::info!("bootstrap already completed, unsealing existing cluster auth store");
1906        }
1907        crate::cluster::BootstrapDisposition::ProceedLocal => {
1908            // Owner first boot: open the vault against the real cluster-global
1909            // auth store and consume the secret inputs to mint the certificate.
1910            debug_assert_eq!(
1911                vault_plan,
1912                crate::cluster::VaultBootstrapPlan::OpenClusterGlobalStore {
1913                    consume_secret_inputs: true,
1914                }
1915            );
1916            apply_preset_from_config(&runtime, &auth_store, bootstrap)?;
1917            maybe_apply_policy_break_glass(&auth_store);
1918        }
1919    }
1920
1921    // Background session purge (every 5 minutes)
1922    {
1923        let store = Arc::clone(&auth_store);
1924        std::thread::Builder::new()
1925            .name("reddb-session-purge".into())
1926            .spawn(move || loop {
1927                std::thread::sleep(std::time::Duration::from_secs(300));
1928                store.purge_expired_sessions();
1929            })
1930            .ok();
1931    }
1932
1933    // Native wire listeners obtain their authentication authority from the
1934    // runtime, including deployments without an HTTP or gRPC server object.
1935    runtime.set_auth_store(Arc::clone(&auth_store));
1936    Ok((runtime, auth_store, telemetry_guard))
1937}
1938
1939/// Honour `REDDB_POLICY_BREAK_GLASS=1` at boot — see issue #713 and
1940/// the [`crate::auth::self_lock_guard`] module. Anything other than
1941/// `1`/`true`/`yes` (case-insensitive) is treated as not set.
1942fn maybe_apply_policy_break_glass(auth_store: &Arc<AuthStore>) {
1943    use crate::auth::self_lock_guard::BREAK_GLASS_ENV;
1944
1945    let enabled = std::env::var(BREAK_GLASS_ENV)
1946        .ok()
1947        .map(|v| {
1948            let trimmed = v.trim().to_ascii_lowercase();
1949            matches!(trimmed.as_str(), "1" | "true" | "yes")
1950        })
1951        .unwrap_or(false);
1952    if !enabled {
1953        return;
1954    }
1955    let now = crate::utils::now_unix_millis() as u128;
1956    match auth_store.apply_policy_break_glass(now) {
1957        Ok(()) => {
1958            tracing::warn!(env = BREAK_GLASS_ENV, "policy break-glass recovery applied");
1959        }
1960        Err(err) => {
1961            tracing::error!(env = BREAK_GLASS_ENV, %err, "policy break-glass recovery failed");
1962        }
1963    }
1964}
1965
1966/// Reserved config keys describing first-boot bootstrap state (issue #650).
1967/// Presence of [`BOOTSTRAP_COMPLETED_KEY`] is the idempotency hinge: when
1968/// it is set, [`apply_preset`] silently no-ops on subsequent boots so a
1969/// container restart with the same env is a no-op.
1970pub(crate) const BOOTSTRAP_COMPLETED_KEY: &str = "system.bootstrap.completed";
1971pub(crate) const BOOTSTRAP_PRESET_KEY: &str = "system.bootstrap.preset";
1972pub(crate) const BOOTSTRAP_FIRST_ADMIN_KEY: &str = "system.bootstrap.first_admin_id";
1973
1974/// Env vars selecting the bootstrap preset. `REDDB_BOOTSTRAP_PRESET`
1975/// is canonical; `REDDB_PRESET` remains accepted for compatibility.
1976pub(crate) const BOOTSTRAP_PRESET_ENV: &str = "REDDB_BOOTSTRAP_PRESET";
1977pub(crate) const PRESET_ENV: &str = "REDDB_PRESET";
1978/// Env equivalent of `--bootstrap-cert-out` (issue #1589). Honours the
1979/// `_FILE` companion via [`env_nonempty`].
1980pub(crate) const BOOTSTRAP_CERT_OUT_ENV: &str = "REDDB_BOOTSTRAP_CERT_OUT";
1981pub(crate) const PRESET_SIMPLE: &str = "simple";
1982pub(crate) const PRESET_PRODUCTION: &str = "production";
1983pub(crate) const PRESET_REGULATED: &str = "regulated";
1984pub(crate) const PRESET_CLOUD: &str = "cloud";
1985
1986/// Policy id installed by the `production` preset and attached to the
1987/// first admin. Grants `"*"` on `"*"` so the admin has policy-derived
1988/// broad authority (acceptance #3) — not an authorization bypass.
1989pub(crate) const FIRST_ADMIN_ALLOW_ALL_POLICY: &str = "system.bootstrap.first-admin-allow-all";
1990pub(crate) const REGULATED_PROTECT_MANAGED_POLICY: &str = "system.regulated.protect-managed";
1991pub(crate) const CLOUD_PROTECT_MANAGED_POLICY: &str = "system.cloud.protect-managed";
1992pub(crate) const CLOUD_CONFIG_NAMESPACE: &str = "red.config.cloud";
1993pub(crate) const REGULATED_AUDIT_CONFIG_NAMESPACE: &str = "red.config.audit";
1994pub(crate) const REGULATED_EVIDENCE_CONFIG_NAMESPACE: &str = "red.config.evidence";
1995pub(crate) const REGULATED_QUERY_AUDIT_CONFIG_NAMESPACE: &str = "red.config.query_audit";
1996
1997/// Apply the bootstrap preset selected by `REDDB_BOOTSTRAP_PRESET` (or
1998/// legacy `REDDB_PRESET`; default `simple`). Idempotent — if
1999/// `system.bootstrap.completed` is already set, this is a one-line
2000/// `tracing::info!` no-op and the server proceeds (issue #650
2001/// acceptance #5).
2002///
2003/// Caller must have already short-circuited the `--no-auth` / `--dev`
2004/// path (issue #663): when that flag is set, the preset must be skipped
2005/// entirely — no admin created, no bootstrap state written.
2006pub(crate) fn apply_preset(
2007    runtime: &RedDBRuntime,
2008    auth_store: &Arc<AuthStore>,
2009) -> Result<(), String> {
2010    let bootstrap = BootstrapConfig::default();
2011    apply_preset_from_config(runtime, auth_store, &bootstrap)
2012}
2013
2014fn apply_preset_from_config(
2015    runtime: &RedDBRuntime,
2016    auth_store: &Arc<AuthStore>,
2017    bootstrap: &BootstrapConfig,
2018) -> Result<(), String> {
2019    let store = runtime.db().store();
2020
2021    if store
2022        .try_get_config(BOOTSTRAP_COMPLETED_KEY)
2023        .map_err(|error| error.to_string())?
2024        .is_some()
2025    {
2026        crate::cli::bootstrap_manifest::rehydrate_manifest_registry(
2027            runtime,
2028            &runtime.config_registry(),
2029        )?;
2030        tracing::info!("bootstrap state present, skipping preset application");
2031        return Ok(());
2032    }
2033
2034    let preset = bootstrap.resolved_preset();
2035
2036    if let Some(path) = bootstrap.selected_manifest() {
2037        if let Some(manifest_bootstrap) =
2038            crate::cli::bootstrap_manifest::bootstrap_config_from_manifest_file(&path)?
2039        {
2040            apply_preset_from_config(runtime, auth_store, &manifest_bootstrap)?;
2041            return Ok(());
2042        }
2043        let first_admin_id = crate::cli::bootstrap_manifest::apply_manifest_file(
2044            runtime,
2045            auth_store,
2046            &runtime.config_registry(),
2047            &path,
2048        )?;
2049        persist_bootstrap_state(runtime, "manifest", Some(&first_admin_id))?;
2050        tracing::info!("bootstrap manifest applied");
2051        return Ok(());
2052    }
2053
2054    // The encrypted auth state is independently durable. If it already says
2055    // bootstrap completed, never reopen preset bootstrap merely because the
2056    // adjacent config marker was not flushed before an abrupt process exit.
2057    if !auth_store.needs_bootstrap() {
2058        persist_bootstrap_state(runtime, &preset, None)?;
2059        tracing::info!(preset = %preset, "durable auth state present, skipping preset bootstrap");
2060        return Ok(());
2061    }
2062
2063    // The simple preset does not create an administrator. With required auth,
2064    // leave the completion marker absent until a durable vault user exists so
2065    // a crash before the first HTTP/gRPC bootstrap does not turn into a
2066    // permanent lockout. Older volatile-auth installs that already carry the
2067    // marker still fail closed in `ensure_persisted_auth_state` above.
2068    if preset == PRESET_SIMPLE && auth_store.config().require_auth && auth_store.needs_bootstrap() {
2069        tracing::info!("required-auth simple preset is awaiting durable first-admin bootstrap");
2070        return Ok(());
2071    }
2072
2073    let mut certificate_output = if matches!(preset.as_str(), PRESET_PRODUCTION | PRESET_CLOUD)
2074        && auth_store.bootstrap_requires_certificate_delivery()
2075    {
2076        require_bootstrap_certificate_output(bootstrap, &preset)?;
2077        Some(open_bootstrap_certificate_output(bootstrap)?)
2078    } else {
2079        None
2080    };
2081    let first_admin_id = match preset.as_str() {
2082        PRESET_SIMPLE => {
2083            // `simple` is the default low-friction preset. Auth knobs
2084            // remain whatever the CLI/env set; we only persist the
2085            // bootstrap state so subsequent boots are idempotent.
2086            None
2087        }
2088        PRESET_PRODUCTION => Some(apply_production_preset_from_config(
2089            auth_store,
2090            bootstrap,
2091            &mut certificate_output,
2092        )?),
2093        PRESET_CLOUD => Some(apply_cloud_preset(
2094            runtime,
2095            auth_store,
2096            bootstrap,
2097            &mut certificate_output,
2098        )?),
2099        PRESET_REGULATED => {
2100            apply_regulated_preset(runtime, auth_store)?;
2101            None
2102        }
2103        other => {
2104            return Err(format!(
2105                "bootstrap preset {other:?} is not recognised (expected `simple`, `production`, `regulated`, or `cloud`)"
2106            ));
2107        }
2108    };
2109
2110    // Every preset mutation must be durable before the adjacent completion
2111    // marker can make bootstrap permanently ineligible on a later restart.
2112    auth_store
2113        .persist_to_vault_result()
2114        .map_err(|err| format!("persist bootstrap auth state: {err}"))?;
2115    persist_bootstrap_state(runtime, &preset, first_admin_id.as_deref())?;
2116    tracing::info!(preset = %preset, "bootstrap preset applied");
2117    Ok(())
2118}
2119
2120struct BootstrapCertificateOutput {
2121    path: PathBuf,
2122    file: Option<std::fs::File>,
2123    committed: bool,
2124}
2125
2126impl Drop for BootstrapCertificateOutput {
2127    fn drop(&mut self) {
2128        if self.committed {
2129            return;
2130        }
2131        self.file.take();
2132        let _ = std::fs::remove_file(&self.path);
2133    }
2134}
2135
2136/// Reserve the secure certificate destination before mutating bootstrap state.
2137/// A failed preset application removes the empty reservation on drop.
2138fn open_bootstrap_certificate_output(
2139    bootstrap: &BootstrapConfig,
2140) -> Result<BootstrapCertificateOutput, String> {
2141    let Some(path) = bootstrap.selected_cert_out() else {
2142        return Err("bootstrap certificate output is not configured".to_string());
2143    };
2144    let mut options = std::fs::OpenOptions::new();
2145    options.write(true).create_new(true);
2146    #[cfg(unix)]
2147    {
2148        use std::os::unix::fs::OpenOptionsExt;
2149        options.mode(0o600);
2150    }
2151    let file = options
2152        .open(&path)
2153        .map_err(|err| format!("create --bootstrap-cert-out {}: {err}", path.display()))?;
2154    Ok(BootstrapCertificateOutput {
2155        path,
2156        file: Some(file),
2157        committed: false,
2158    })
2159}
2160
2161fn stage_bootstrap_certificate(
2162    output: &mut BootstrapCertificateOutput,
2163    certificate_hex: &str,
2164) -> Result<(), String> {
2165    use std::io::Write;
2166    let path = output.path.clone();
2167    let file = output
2168        .file
2169        .as_mut()
2170        .ok_or_else(|| "bootstrap certificate output is closed".to_string())?;
2171    writeln!(file, "{certificate_hex}")
2172        .map_err(|err| format!("write --bootstrap-cert-out {}: {err}", path.display()))?;
2173    file.sync_all()
2174        .map_err(|err| format!("sync --bootstrap-cert-out {}: {err}", path.display()))?;
2175    Ok(())
2176}
2177
2178fn commit_bootstrap_certificate(mut output: BootstrapCertificateOutput) {
2179    let path = output.path.clone();
2180    output.committed = true;
2181    tracing::info!(
2182        target: "reddb::bootstrap",
2183        path = %path.display(),
2184        "first-boot self-bootstrap: wrote unseal certificate to --bootstrap-cert-out (consume via REDDB_CERTIFICATE_FILE on the next boot)"
2185    );
2186}
2187
2188fn bootstrap_with_certificate_output(
2189    auth_store: &Arc<AuthStore>,
2190    username: &str,
2191    password: &str,
2192    certificate_output: &mut Option<BootstrapCertificateOutput>,
2193) -> Result<crate::auth::store::BootstrapResult, String> {
2194    let mut certificate_staged = false;
2195    let result = auth_store
2196        .bootstrap_with_certificate_delivery(username, password, |certificate| {
2197            let output = certificate_output.as_mut().ok_or_else(|| {
2198                crate::auth::AuthError::Internal(
2199                    "bootstrap certificate output was not prepared".to_string(),
2200                )
2201            })?;
2202            stage_bootstrap_certificate(output, certificate)
2203                .map_err(crate::auth::AuthError::Internal)?;
2204            certificate_staged = true;
2205            Ok(())
2206        })
2207        .map_err(|err| format!("bootstrap first admin: {err}"))?;
2208
2209    // When the store was opened with an operator-supplied certificate there
2210    // is no newly generated seal to deliver before activation. Mirror that
2211    // existing credential into the requested file after bootstrap.
2212    if !certificate_staged {
2213        if let (Some(certificate), Some(output)) =
2214            (result.certificate.as_deref(), certificate_output.as_mut())
2215        {
2216            stage_bootstrap_certificate(output, certificate)?;
2217            certificate_staged = true;
2218        }
2219    }
2220    if certificate_staged {
2221        let output = certificate_output
2222            .take()
2223            .ok_or_else(|| "bootstrap certificate output was not prepared".to_string())?;
2224        commit_bootstrap_certificate(output);
2225    }
2226    Ok(result)
2227}
2228
2229fn ensure_persisted_auth_state(
2230    bootstrap_completed: bool,
2231    require_auth: bool,
2232    needs_bootstrap: bool,
2233) -> Result<(), String> {
2234    if bootstrap_completed && require_auth && needs_bootstrap {
2235        return Err(
2236            "persistent database is marked bootstrapped but its authentication state is empty; refusing to reopen public bootstrap"
2237                .to_string(),
2238        );
2239    }
2240    Ok(())
2241}
2242
2243fn require_bootstrap_certificate_output(
2244    bootstrap: &BootstrapConfig,
2245    preset: &str,
2246) -> Result<(), String> {
2247    if bootstrap.selected_cert_out().is_some() {
2248        return Ok(());
2249    }
2250    Err(format!(
2251        "bootstrap preset `{preset}` requires --bootstrap-cert-out (or REDDB_BOOTSTRAP_CERT_OUT); vault certificates are never written to logs"
2252    ))
2253}
2254
2255fn apply_production_preset_from_config(
2256    auth_store: &Arc<AuthStore>,
2257    bootstrap: &BootstrapConfig,
2258    certificate_output: &mut Option<BootstrapCertificateOutput>,
2259) -> Result<String, String> {
2260    use crate::auth::store::PrincipalRef;
2261    use crate::auth::UserId;
2262
2263    let username = bootstrap.production_username().ok_or_else(|| {
2264        "production preset requires --bootstrap-admin or REDDB_USERNAME (or REDDB_USERNAME_FILE)"
2265            .to_string()
2266    })?;
2267    let password = bootstrap.production_password().ok_or_else(|| {
2268        "production preset requires --bootstrap-admin-password or REDDB_PASSWORD (or REDDB_PASSWORD_FILE)"
2269            .to_string()
2270    })?;
2271
2272    // (1) Create the first admin as an ordinary platform-scoped user.
2273    // Managed guardrails are policy-derived; bootstrap does not stamp a
2274    // parallel ownership flag.
2275    let result =
2276        bootstrap_with_certificate_output(auth_store, &username, &password, certificate_output)?;
2277    if result.certificate.is_some() {
2278        tracing::warn!(
2279            "vault certificate issued by REDDB_PRESET=production and written to the configured certificate output"
2280        );
2281    }
2282    let first_admin = UserId::platform(result.user.username.clone());
2283
2284    // (2) Install the allow-all policy as an ordinary policy row.
2285    install_allow_all_policy(auth_store)?;
2286
2287    // (3) Attach the policy to the first admin.
2288    auth_store
2289        .attach_policy(
2290            PrincipalRef::User(first_admin.clone()),
2291            FIRST_ADMIN_ALLOW_ALL_POLICY,
2292        )
2293        .map_err(|err| format!("attach allow-all policy: {err}"))?;
2294
2295    Ok(first_admin.to_string())
2296}
2297
2298fn apply_cloud_preset(
2299    runtime: &RedDBRuntime,
2300    auth_store: &Arc<AuthStore>,
2301    bootstrap: &BootstrapConfig,
2302    certificate_output: &mut Option<BootstrapCertificateOutput>,
2303) -> Result<String, String> {
2304    use crate::auth::store::PrincipalRef;
2305    use crate::auth::{Role, UserId};
2306
2307    let head_username = bootstrap.cloud_head_username().ok_or_else(|| {
2308        "cloud preset requires --cloud-head-admin or REDDB_CLOUD_HEAD_ADMIN (or REDDB_USERNAME)"
2309            .to_string()
2310    })?;
2311    let head_password = bootstrap.cloud_head_password().ok_or_else(|| {
2312        "cloud preset requires --cloud-head-admin-password or REDDB_CLOUD_HEAD_ADMIN_PASSWORD (or REDDB_PASSWORD)"
2313            .to_string()
2314    })?;
2315    let customer_username = bootstrap.customer_username().ok_or_else(|| {
2316        "cloud preset requires --customer-admin or REDDB_CUSTOMER_ADMIN".to_string()
2317    })?;
2318    let customer_password = bootstrap.customer_password().ok_or_else(|| {
2319        "cloud preset requires --customer-admin-password or REDDB_CUSTOMER_ADMIN_PASSWORD"
2320            .to_string()
2321    })?;
2322    if head_username == customer_username {
2323        return Err("cloud preset requires distinct head and customer admin usernames".to_string());
2324    }
2325
2326    let head = bootstrap_with_certificate_output(
2327        auth_store,
2328        &head_username,
2329        &head_password,
2330        certificate_output,
2331    )
2332    .map_err(|err| format!("bootstrap cloud head admin: {err}"))?;
2333    let head_id = UserId::platform(head.user.username.clone());
2334
2335    auth_store
2336        .create_user(&customer_username, &customer_password, Role::Admin)
2337        .map_err(|err| format!("create cloud customer admin: {err}"))?;
2338    let customer_id = UserId::platform(customer_username.clone());
2339
2340    install_allow_all_policy(auth_store)?;
2341    for user_id in [&head_id, &customer_id] {
2342        auth_store
2343            .attach_policy(
2344                PrincipalRef::User(user_id.clone()),
2345                FIRST_ADMIN_ALLOW_ALL_POLICY,
2346            )
2347            .map_err(|err| format!("attach allow-all policy: {err}"))?;
2348    }
2349    install_cloud_guardrails(runtime, auth_store, &head_id)?;
2350    auth_store
2351        .attach_policy(
2352            PrincipalRef::User(customer_id),
2353            CLOUD_PROTECT_MANAGED_POLICY,
2354        )
2355        .map_err(|err| format!("attach cloud guardrail policy: {err}"))?;
2356
2357    Ok(head_id.to_string())
2358}
2359
2360pub(crate) fn install_allow_all_policy(auth_store: &AuthStore) -> Result<(), String> {
2361    use crate::auth::policies::Policy;
2362
2363    let policy = Policy::from_json_str(&format!(
2364        r#"{{
2365            "id": "{id}",
2366            "version": 1,
2367            "statements": [{{
2368                "effect": "allow",
2369                "actions": ["*"],
2370                "resources": ["*"]
2371            }}]
2372        }}"#,
2373        id = FIRST_ADMIN_ALLOW_ALL_POLICY
2374    ))
2375    .map_err(|err| format!("compile allow-all policy: {err}"))?;
2376    auth_store
2377        .put_policy(policy)
2378        .map_err(|err| format!("install allow-all policy: {err}"))
2379}
2380
2381fn install_cloud_guardrails(
2382    runtime: &RedDBRuntime,
2383    auth_store: &Arc<AuthStore>,
2384    head_admin: &crate::auth::UserId,
2385) -> Result<(), String> {
2386    use crate::auth::policies::Policy;
2387    use crate::auth::registry::EvidenceRequirement;
2388
2389    let policy = Policy::from_json_str(&format!(
2390        r#"{{
2391            "id": "{id}",
2392            "version": 1,
2393            "statements": [
2394                {{
2395                    "effect": "deny",
2396                    "actions": ["policy:put", "policy:drop", "policy:attach", "policy:detach"],
2397                    "resources": ["policy:{id}"]
2398                }},
2399                {{
2400                    "effect": "deny",
2401                    "actions": [
2402                        "user:delete",
2403                        "user:disable",
2404                        "user:password:change",
2405                        "user:role:update"
2406                    ],
2407                    "resources": ["user:{head_admin}"]
2408                }},
2409                {{
2410                    "effect": "deny",
2411                    "actions": ["config:write"],
2412                    "resources": ["config:{cloud}.*"]
2413                }}
2414            ]
2415        }}"#,
2416        id = CLOUD_PROTECT_MANAGED_POLICY,
2417        head_admin = head_admin,
2418        cloud = CLOUD_CONFIG_NAMESPACE,
2419    ))
2420    .map_err(|err| format!("compile cloud guardrail policy: {err}"))?;
2421    auth_store
2422        .put_policy(policy)
2423        .map_err(|err| format!("install cloud guardrail policy: {err}"))?;
2424
2425    let now_ms = crate::utils::now_unix_millis() as u128;
2426    let entries = vec![
2427        regulated_registry_entry(
2428            CLOUD_PROTECT_MANAGED_POLICY,
2429            crate::auth::managed_policy::RESOURCE_TYPE_POLICY,
2430            "policy",
2431            "policy:*",
2432            &format!("policy:{CLOUD_PROTECT_MANAGED_POLICY}"),
2433            EvidenceRequirement::Metadata,
2434            now_ms,
2435        ),
2436        regulated_registry_entry(
2437            CLOUD_CONFIG_NAMESPACE,
2438            crate::auth::managed_config::RESOURCE_TYPE_CONFIG_NAMESPACE,
2439            "config_namespace",
2440            "config:write",
2441            &format!("config:{CLOUD_CONFIG_NAMESPACE}.*"),
2442            EvidenceRequirement::Metadata,
2443            now_ms,
2444        ),
2445    ];
2446    for entry in entries.iter().cloned() {
2447        runtime
2448            .config_registry()
2449            .restore_bootstrap_entry(entry)
2450            .map_err(|err| format!("install cloud registry entry: {err}"))?;
2451    }
2452    crate::cli::bootstrap_manifest::persist_registry_state(runtime, &entries)?;
2453    Ok(())
2454}
2455
2456fn apply_regulated_preset(
2457    runtime: &RedDBRuntime,
2458    auth_store: &Arc<AuthStore>,
2459) -> Result<(), String> {
2460    use crate::auth::policies::Policy;
2461    use crate::auth::registry::EvidenceRequirement;
2462    use crate::auth::store::{PrincipalRef, PUBLIC_IAM_GROUP};
2463
2464    runtime.query_audit().enable_infrastructure();
2465
2466    let policy = Policy::from_json_str(&format!(
2467        r#"{{
2468            "id": "{id}",
2469            "version": 1,
2470            "statements": [
2471                {{
2472                    "effect": "deny",
2473                    "actions": ["policy:put", "policy:drop", "policy:attach", "policy:detach"],
2474                    "resources": ["policy:{id}"]
2475                }},
2476                {{
2477                    "effect": "deny",
2478                    "actions": ["config:write"],
2479                    "resources": [
2480                        "config:{audit}.*",
2481                        "config:{evidence}.*",
2482                        "config:{query_audit}.*"
2483                    ]
2484                }}
2485            ]
2486        }}"#,
2487        id = REGULATED_PROTECT_MANAGED_POLICY,
2488        audit = REGULATED_AUDIT_CONFIG_NAMESPACE,
2489        evidence = REGULATED_EVIDENCE_CONFIG_NAMESPACE,
2490        query_audit = REGULATED_QUERY_AUDIT_CONFIG_NAMESPACE,
2491    ))
2492    .map_err(|err| format!("compile regulated guardrail policy: {err}"))?;
2493    auth_store
2494        .put_policy(policy)
2495        .map_err(|err| format!("install regulated guardrail policy: {err}"))?;
2496    auth_store
2497        .attach_policy(
2498            PrincipalRef::Group(PUBLIC_IAM_GROUP.to_string()),
2499            REGULATED_PROTECT_MANAGED_POLICY,
2500        )
2501        .map_err(|err| format!("attach regulated guardrail policy: {err}"))?;
2502
2503    let now_ms = crate::utils::now_unix_millis() as u128;
2504    let entries = vec![
2505        regulated_registry_entry(
2506            REGULATED_PROTECT_MANAGED_POLICY,
2507            crate::auth::managed_policy::RESOURCE_TYPE_POLICY,
2508            "iam_policy",
2509            "policy:*",
2510            &format!("policy:{REGULATED_PROTECT_MANAGED_POLICY}"),
2511            EvidenceRequirement::Metadata,
2512            now_ms,
2513        ),
2514        regulated_registry_entry(
2515            REGULATED_AUDIT_CONFIG_NAMESPACE,
2516            crate::auth::managed_config::RESOURCE_TYPE_CONFIG_NAMESPACE,
2517            "config_namespace",
2518            "config:write",
2519            &format!("config:{REGULATED_AUDIT_CONFIG_NAMESPACE}.*"),
2520            EvidenceRequirement::Metadata,
2521            now_ms,
2522        ),
2523        regulated_registry_entry(
2524            REGULATED_EVIDENCE_CONFIG_NAMESPACE,
2525            crate::auth::managed_config::RESOURCE_TYPE_CONFIG_NAMESPACE,
2526            "config_namespace",
2527            "config:write",
2528            &format!("config:{REGULATED_EVIDENCE_CONFIG_NAMESPACE}.*"),
2529            EvidenceRequirement::Metadata,
2530            now_ms,
2531        ),
2532        regulated_registry_entry(
2533            REGULATED_QUERY_AUDIT_CONFIG_NAMESPACE,
2534            crate::auth::managed_config::RESOURCE_TYPE_CONFIG_NAMESPACE,
2535            "config_namespace",
2536            "config:write",
2537            &format!("config:{REGULATED_QUERY_AUDIT_CONFIG_NAMESPACE}.*"),
2538            EvidenceRequirement::Metadata,
2539            now_ms,
2540        ),
2541    ];
2542
2543    for entry in entries.iter().cloned() {
2544        runtime
2545            .config_registry()
2546            .restore_bootstrap_entry(entry)
2547            .map_err(|err| format!("install regulated registry entry: {err}"))?;
2548    }
2549    crate::cli::bootstrap_manifest::persist_registry_state(runtime, &entries)?;
2550    Ok(())
2551}
2552
2553fn regulated_registry_entry(
2554    id: &str,
2555    resource_type: &str,
2556    schema: &str,
2557    required_action: &str,
2558    required_resource: &str,
2559    evidence_requirement: crate::auth::registry::EvidenceRequirement,
2560    updated_at_ms: u128,
2561) -> crate::auth::registry::ConfigRegistryEntry {
2562    crate::auth::registry::ConfigRegistryEntry {
2563        id: id.to_string(),
2564        version: 1,
2565        resource_type: resource_type.to_string(),
2566        schema: schema.to_string(),
2567        mutability: crate::auth::registry::Mutability::Immutable,
2568        sensitivity: crate::auth::registry::Sensitivity::Internal,
2569        managed: true,
2570        required_action: required_action.to_string(),
2571        required_resource: required_resource.to_string(),
2572        evidence_requirement,
2573        updated_by: "system:regulated-preset".to_string(),
2574        updated_at_ms,
2575    }
2576}
2577
2578fn persist_bootstrap_state(
2579    runtime: &RedDBRuntime,
2580    preset: &str,
2581    first_admin_id: Option<&str>,
2582) -> Result<(), String> {
2583    let store = runtime.db().store();
2584    let mut tree = crate::serde_json::Map::new();
2585    tree.insert(
2586        BOOTSTRAP_COMPLETED_KEY.to_string(),
2587        crate::serde_json::Value::Bool(true),
2588    );
2589    tree.insert(
2590        BOOTSTRAP_PRESET_KEY.to_string(),
2591        crate::serde_json::Value::String(preset.to_string()),
2592    );
2593    if let Some(id) = first_admin_id {
2594        tree.insert(
2595            BOOTSTRAP_FIRST_ADMIN_KEY.to_string(),
2596            crate::serde_json::Value::String(id.to_string()),
2597        );
2598    }
2599    let json = crate::serde_json::Value::Object(tree);
2600    store
2601        .try_set_config_tree("", &json)
2602        .map_err(|error| error.to_string())?;
2603    Ok(())
2604}
2605
2606/// Read `red.logging.*` keys from the persistent config store and
2607/// merge them into the CLI-built `TelemetryConfig`. Merge priority:
2608/// explicit CLI flag > red_config > built-in default.
2609///
2610/// The "was a flag passed" signal comes from the `*_explicit` bools
2611/// on `TelemetryConfig`, populated by the CLI parser. This replaces
2612/// an earlier equality-to-default heuristic that silently dropped
2613/// config whenever the CLI-derived default diverged from
2614/// `TelemetryConfig::default()` (e.g. path-derived `log_dir`,
2615/// non-TTY `format`) and that silently overrode `--no-log-file`.
2616fn merge_telemetry_with_config(
2617    mut cli: crate::telemetry::TelemetryConfig,
2618    runtime: &RedDBRuntime,
2619) -> crate::telemetry::TelemetryConfig {
2620    use reddb_types::Value;
2621
2622    let store = runtime.db().store();
2623
2624    if !cli.level_explicit {
2625        if let Some(Value::Text(v)) = store.get_config("red.logging.level") {
2626            cli.level_filter = v.to_string();
2627        }
2628    }
2629    if !cli.format_explicit {
2630        if let Some(Value::Text(v)) = store.get_config("red.logging.format") {
2631            if let Some(parsed) = crate::telemetry::LogFormat::parse(&v) {
2632                cli.format = parsed;
2633            }
2634        }
2635    }
2636    if !cli.rotation_keep_days_explicit {
2637        match store.get_config("red.logging.keep_days") {
2638            Some(Value::Integer(n)) if n >= 0 && n <= u16::MAX as i64 => {
2639                cli.rotation_keep_days = n as u16
2640            }
2641            Some(Value::UnsignedInteger(n)) if n <= u16::MAX as u64 => {
2642                cli.rotation_keep_days = n as u16
2643            }
2644            Some(Value::Text(v)) => {
2645                if let Ok(n) = v.parse::<u16>() {
2646                    cli.rotation_keep_days = n;
2647                }
2648            }
2649            _ => {}
2650        }
2651    }
2652    if !cli.file_prefix_explicit {
2653        if let Some(Value::Text(v)) = store.get_config("red.logging.file_prefix") {
2654            if !v.is_empty() {
2655                cli.file_prefix = v.to_string();
2656            }
2657        }
2658    }
2659    // --no-log-file is a kill-switch: config cannot resurrect the
2660    // file sink. Explicit --log-dir also wins.
2661    if !cli.log_dir_explicit && !cli.log_file_disabled {
2662        if let Some(Value::Text(v)) = store.get_config("red.logging.dir") {
2663            if !v.is_empty() {
2664                cli.log_dir = Some(std::path::PathBuf::from(v.as_ref()));
2665            }
2666        }
2667    }
2668
2669    cli
2670}
2671
2672#[cfg(test)]
2673mod telemetry_merge_tests {
2674    use super::*;
2675    use crate::telemetry::{LogFormat, TelemetryConfig};
2676
2677    fn fresh_runtime() -> RedDBRuntime {
2678        RedDBRuntime::with_options(RedDBOptions::in_memory()).expect("runtime")
2679    }
2680
2681    fn set_str(runtime: &RedDBRuntime, key: &str, value: &str) {
2682        runtime
2683            .db()
2684            .store()
2685            .set_config_tree(key, &crate::serde_json::Value::String(value.to_string()));
2686    }
2687
2688    fn cli_base() -> TelemetryConfig {
2689        // Emulate default_telemetry_for_path(Some(path)) on a non-TTY host:
2690        // log_dir = Some(...), format = Json. Nothing marked explicit.
2691        TelemetryConfig {
2692            log_dir: Some(std::path::PathBuf::from("/tmp/reddb-default/logs")),
2693            format: LogFormat::Json,
2694            ..Default::default()
2695        }
2696    }
2697
2698    #[test]
2699    fn config_log_dir_promoted_when_flag_absent() {
2700        let runtime = fresh_runtime();
2701        set_str(&runtime, "red.logging.dir", "/var/log/reddb");
2702        let merged = merge_telemetry_with_config(cli_base(), &runtime);
2703        assert_eq!(
2704            merged.log_dir.as_deref(),
2705            Some(std::path::Path::new("/var/log/reddb"))
2706        );
2707    }
2708
2709    #[test]
2710    fn explicit_log_dir_wins_over_config() {
2711        let runtime = fresh_runtime();
2712        set_str(&runtime, "red.logging.dir", "/var/log/reddb");
2713        let mut cli = cli_base();
2714        cli.log_dir = Some(std::path::PathBuf::from("/custom/dir"));
2715        cli.log_dir_explicit = true;
2716        let merged = merge_telemetry_with_config(cli, &runtime);
2717        assert_eq!(
2718            merged.log_dir.as_deref(),
2719            Some(std::path::Path::new("/custom/dir"))
2720        );
2721    }
2722
2723    #[test]
2724    fn no_log_file_beats_config_log_dir() {
2725        let runtime = fresh_runtime();
2726        set_str(&runtime, "red.logging.dir", "/var/log/reddb");
2727        let mut cli = cli_base();
2728        cli.log_dir = None;
2729        cli.log_file_disabled = true;
2730        let merged = merge_telemetry_with_config(cli, &runtime);
2731        assert!(
2732            merged.log_dir.is_none(),
2733            "--no-log-file must veto config dir"
2734        );
2735    }
2736
2737    #[test]
2738    fn config_format_promoted_on_non_tty_default() {
2739        // On non-TTY, default_telemetry_for_path yields format=Json even
2740        // though TelemetryConfig::default() is Pretty. The old equality
2741        // check silently dropped config here.
2742        let runtime = fresh_runtime();
2743        set_str(&runtime, "red.logging.format", "pretty");
2744        let merged = merge_telemetry_with_config(cli_base(), &runtime);
2745        assert_eq!(merged.format, LogFormat::Pretty);
2746    }
2747
2748    #[test]
2749    fn explicit_format_wins_over_config() {
2750        let runtime = fresh_runtime();
2751        set_str(&runtime, "red.logging.format", "pretty");
2752        let mut cli = cli_base();
2753        cli.format = LogFormat::Json;
2754        cli.format_explicit = true;
2755        let merged = merge_telemetry_with_config(cli, &runtime);
2756        assert_eq!(merged.format, LogFormat::Json);
2757    }
2758}
2759
2760#[inline(never)]
2761fn build_http_server(
2762    runtime: RedDBRuntime,
2763    auth_store: Arc<AuthStore>,
2764    bind_addr: String,
2765) -> RedDBServer {
2766    build_http_server_with_transport_readiness(
2767        runtime,
2768        auth_store,
2769        bind_addr,
2770        TransportReadiness::default(),
2771    )
2772}
2773
2774/// Apply the resolved HTTP limits to a freshly-built `RedDBServer`.
2775///
2776/// Centralised here so every `run_*` path goes through the same
2777/// resolver and the structured startup log line carries the same
2778/// `http_limits.*` fields regardless of transport combination.
2779fn apply_http_limits(
2780    server: RedDBServer,
2781    config: &ServerCommandConfig,
2782    runtime: &RedDBRuntime,
2783) -> RedDBServer {
2784    let store = runtime.db().store();
2785    let resolved =
2786        crate::server::http_limits::resolve_http_limits(&config.http_limits_cli, |key| match store
2787            .get_config(key)
2788        {
2789            Some(reddb_types::Value::Text(v)) => Some(v.to_string()),
2790            Some(reddb_types::Value::Integer(n)) if n >= 0 => Some(n.to_string()),
2791            Some(reddb_types::Value::UnsignedInteger(n)) => Some(n.to_string()),
2792            _ => None,
2793        });
2794    tracing::info!(
2795        target: "reddb::http_limits",
2796        max_handlers = resolved.max_handlers,
2797        execution_workers = resolved.execution_workers,
2798        handler_timeout_ms = resolved.handler_timeout_ms,
2799        retry_after_secs = resolved.retry_after_secs,
2800        max_inflight_per_principal = resolved.max_inflight_per_principal,
2801        "http_limits resolved"
2802    );
2803    server.with_http_limits(resolved)
2804}
2805
2806/// `red server --ui` (issue #1047, ADR 0051): attach the resolved red-ui
2807/// bundle directory to an HTTP server so it also serves the bundle as
2808/// static assets. No-op when `--ui` is off.
2809///
2810/// An explicit `--ui-dir <DIR>` is served as-is (no download); otherwise
2811/// the pinned bundle is resolved from the local cache, downloading on
2812/// first use (ADR 0050). A resolution failure is fatal: the operator
2813/// explicitly asked for the UI, so silently serving API-only would be
2814/// surprising and would mask an offline/checksum problem.
2815fn apply_ui_bundle(
2816    server: RedDBServer,
2817    config: &ServerCommandConfig,
2818) -> Result<RedDBServer, String> {
2819    if !config.ui {
2820        return Ok(server);
2821    }
2822    let ui_dir = match &config.ui_dir {
2823        Some(dir) => dir.clone(),
2824        None => {
2825            let cache_root = crate::server::ui_bundle_resolver::reddb_user_cache_root()
2826                .unwrap_or_else(|_| std::env::temp_dir().join("reddb"));
2827            crate::server::ui_bundle_resolver::resolve_ui_bundle(
2828                &cache_root,
2829                &crate::server::ui_bundle_resolver::HttpFetcher,
2830            )
2831            .map_err(|err| format!("resolve red-ui bundle for --ui: {err}"))?
2832        }
2833    };
2834    tracing::info!(target: "reddb::ui", dir = %ui_dir.display(), "serving red-ui bundle on HTTP surface");
2835    Ok(server.with_ui_dir(ui_dir))
2836}
2837
2838#[inline(never)]
2839fn build_http_server_with_transport_readiness(
2840    runtime: RedDBRuntime,
2841    auth_store: Arc<AuthStore>,
2842    bind_addr: String,
2843    transport_readiness: TransportReadiness,
2844) -> RedDBServer {
2845    RedDBServer::with_options(
2846        runtime,
2847        ServerOptions {
2848            bind_addr,
2849            transport_readiness,
2850            ..ServerOptions::default()
2851        },
2852    )
2853    .with_auth(auth_store)
2854}
2855
2856/// PLAN.md Phase 6.2 — build a listener that only serves
2857/// `/admin/*` + `/metrics` + `/health/*`. Defaults to `127.0.0.1`
2858/// when the env var has no host (loopback-only by default per spec).
2859#[inline(never)]
2860fn build_admin_only_server(
2861    runtime: RedDBRuntime,
2862    auth_store: Arc<AuthStore>,
2863    bind_addr: String,
2864) -> RedDBServer {
2865    RedDBServer::with_options(
2866        runtime,
2867        ServerOptions {
2868            bind_addr,
2869            surface: crate::server::ServerSurface::AdminOnly,
2870            ..ServerOptions::default()
2871        },
2872    )
2873    .with_auth(auth_store)
2874}
2875
2876/// PLAN.md Phase 6.2 — build a listener that only serves `/metrics`
2877/// + `/health/*`. Suitable for Prometheus scrape ports that may be
2878///   exposed wider than the admin port.
2879#[inline(never)]
2880fn build_metrics_only_server(
2881    runtime: RedDBRuntime,
2882    auth_store: Arc<AuthStore>,
2883    bind_addr: String,
2884) -> RedDBServer {
2885    RedDBServer::with_options(
2886        runtime,
2887        ServerOptions {
2888            bind_addr,
2889            surface: crate::server::ServerSurface::MetricsOnly,
2890            ..ServerOptions::default()
2891        },
2892    )
2893    .with_auth(auth_store)
2894}
2895
2896/// Spawn dedicated admin / metrics listeners when the operator set
2897/// `RED_ADMIN_BIND` / `RED_METRICS_BIND`. Both are optional; when
2898/// unset the existing listener keeps serving everything (back-compat).
2899fn spawn_admin_metrics_listeners(runtime: &RedDBRuntime, auth_store: &Arc<AuthStore>) {
2900    if let Some(addr) = env_nonempty("RED_ADMIN_BIND") {
2901        let server = build_admin_only_server(runtime.clone(), auth_store.clone(), addr.clone());
2902        let _ = server.serve_in_background();
2903        tracing::info!(transport = "http", surface = "admin", bind = %addr, "listener online");
2904    }
2905    if let Some(addr) = env_nonempty("RED_METRICS_BIND") {
2906        let server = build_metrics_only_server(runtime.clone(), auth_store.clone(), addr.clone());
2907        let _ = server.serve_in_background();
2908        tracing::info!(transport = "http", surface = "metrics", bind = %addr, "listener online");
2909    }
2910}
2911
2912/// Resolve the HTTP TLS config from CLI / env / dev defaults.
2913fn resolve_http_tls_config(
2914    config: &ServerCommandConfig,
2915) -> Result<crate::server::tls::HttpTlsConfig, String> {
2916    match (&config.http_tls_cert, &config.http_tls_key) {
2917        (Some(cert), Some(key)) => Ok(crate::server::tls::HttpTlsConfig {
2918            cert_path: cert.clone(),
2919            key_path: key.clone(),
2920            client_ca_path: config.http_tls_client_ca.clone(),
2921        }),
2922        (None, None) => {
2923            // Dev-mode auto-generate next to the data directory.
2924            let dir = config
2925                .path
2926                .as_ref()
2927                .and_then(|p| p.parent().map(std::path::PathBuf::from))
2928                .unwrap_or_else(|| std::path::PathBuf::from("."));
2929            let auto = crate::server::tls::auto_generate_dev_cert(&dir)
2930                .map_err(|err| format!("HTTP TLS dev: {err}"))?;
2931            Ok(crate::server::tls::HttpTlsConfig {
2932                cert_path: auto.cert_path,
2933                key_path: auto.key_path,
2934                client_ca_path: config.http_tls_client_ca.clone(),
2935            })
2936        }
2937        _ => Err("HTTP TLS requires both --http-tls-cert and --http-tls-key (or neither, with RED_HTTP_TLS_DEV=1)".to_string()),
2938    }
2939}
2940
2941#[cfg(test)]
2942mod tests {
2943    use super::*;
2944
2945    #[test]
2946    fn server_runtime_receives_the_bootstrapped_auth_authority() {
2947        let mut options = RedDBOptions::in_memory();
2948        options.auth.enabled = true;
2949        let (runtime, store, _telemetry) =
2950            build_runtime_with_telemetry(options, None).expect("server runtime");
2951        let installed = runtime.auth_store().expect("runtime auth authority");
2952        assert!(Arc::ptr_eq(&store, &installed));
2953    }
2954
2955    #[test]
2956    fn runtime_workers_use_both_cores_on_small_instances() {
2957        assert_eq!(suggested_worker_count(1), 1);
2958        assert_eq!(suggested_worker_count(2), 2);
2959        assert_eq!(suggested_worker_count(4), 3);
2960    }
2961
2962    #[test]
2963    fn render_systemd_unit_contains_expected_execstart() {
2964        let config = SystemdServiceConfig {
2965            service_name: "reddb".to_string(),
2966            binary_path: PathBuf::from("/usr/local/bin/red"),
2967            run_user: "reddb".to_string(),
2968            run_group: "reddb".to_string(),
2969            data_path: reddb_file::default_service_database_path(),
2970            router_bind_addr: None,
2971            grpc_bind_addr: Some("0.0.0.0:55055".to_string()),
2972            http_bind_addr: None,
2973        };
2974
2975        let unit = render_systemd_unit(&config);
2976        assert!(unit.contains("ExecStart=/usr/local/bin/red server --path /var/lib/reddb/data.rdb --grpc-bind 0.0.0.0:55055"));
2977        assert!(unit.contains("ReadWritePaths=/var/lib/reddb"));
2978    }
2979
2980    #[test]
2981    fn systemd_service_config_derives_paths() {
2982        let config = SystemdServiceConfig {
2983            service_name: "reddb-api".to_string(),
2984            binary_path: PathBuf::from("/usr/local/bin/red"),
2985            run_user: "reddb".to_string(),
2986            run_group: "reddb".to_string(),
2987            data_path: PathBuf::from("/srv/reddb/live/data.rdb"),
2988            router_bind_addr: None,
2989            grpc_bind_addr: None,
2990            http_bind_addr: Some("127.0.0.1:5000".to_string()),
2991        };
2992
2993        assert_eq!(config.data_dir(), PathBuf::from("/srv/reddb/live"));
2994        assert_eq!(
2995            config.unit_path(),
2996            PathBuf::from("/etc/systemd/system/reddb-api.service")
2997        );
2998    }
2999
3000    #[test]
3001    fn render_systemd_unit_supports_dual_transport() {
3002        let config = SystemdServiceConfig {
3003            service_name: "reddb".to_string(),
3004            binary_path: PathBuf::from("/usr/local/bin/red"),
3005            run_user: "reddb".to_string(),
3006            run_group: "reddb".to_string(),
3007            data_path: reddb_file::default_service_database_path(),
3008            router_bind_addr: None,
3009            grpc_bind_addr: Some("0.0.0.0:55055".to_string()),
3010            http_bind_addr: Some("0.0.0.0:5000".to_string()),
3011        };
3012
3013        let unit = render_systemd_unit(&config);
3014        assert!(unit.contains("--grpc-bind 0.0.0.0:55055"));
3015        assert!(unit.contains("--http-bind 0.0.0.0:5000"));
3016    }
3017
3018    #[test]
3019    fn render_systemd_unit_supports_router_mode() {
3020        let config = SystemdServiceConfig {
3021            service_name: "reddb".to_string(),
3022            binary_path: PathBuf::from("/usr/local/bin/red"),
3023            run_user: "reddb".to_string(),
3024            run_group: "reddb".to_string(),
3025            data_path: reddb_file::default_service_database_path(),
3026            router_bind_addr: Some(DEFAULT_ROUTER_BIND_ADDR.to_string()),
3027            grpc_bind_addr: None,
3028            http_bind_addr: None,
3029        };
3030
3031        let unit = render_systemd_unit(&config);
3032        assert!(unit.contains("--bind 127.0.0.1:5050"));
3033        assert!(!unit.contains("--grpc-bind"));
3034        assert!(!unit.contains("--http-bind"));
3035    }
3036
3037    #[test]
3038    fn explicit_bind_collision_is_fatal() {
3039        let held = TcpListener::bind("127.0.0.1:0").expect("hold test port");
3040        let addr = held.local_addr().expect("held addr").to_string();
3041        let mut readiness = TransportReadiness::default();
3042
3043        let error = bind_listener_for_startup(&mut readiness, "http", &addr, true).unwrap_err();
3044
3045        assert!(error.contains("explicit http listener bind"));
3046        assert_eq!(readiness.active.len(), 0);
3047        assert_eq!(readiness.failed.len(), 1);
3048        assert!(readiness.failed[0].explicit);
3049        assert_eq!(readiness.failed[0].bind_addr, addr);
3050    }
3051
3052    // ---------- Issue #663 — `--no-auth` / `--dev` ----------
3053
3054    // Env access in tests is process-global; serialise the two
3055    // `--no-auth` tests so the REDDB_USERNAME / REDDB_PASSWORD pair
3056    // one of them sets cannot leak into the other under cargo's
3057    // default parallel runner.
3058    fn no_auth_env_lock() -> &'static std::sync::Mutex<()> {
3059        static LOCK: std::sync::OnceLock<std::sync::Mutex<()>> = std::sync::OnceLock::new();
3060        LOCK.get_or_init(|| std::sync::Mutex::new(()))
3061    }
3062
3063    fn bootstrap_cert_path(label: &str) -> PathBuf {
3064        std::env::temp_dir().join(format!(
3065            "reddb-{label}-{}-{}.cert",
3066            std::process::id(),
3067            std::time::SystemTime::now()
3068                .duration_since(std::time::UNIX_EPOCH)
3069                .unwrap_or_default()
3070                .as_nanos()
3071        ))
3072    }
3073
3074    #[test]
3075    fn secure_presets_require_certificate_file_delivery() {
3076        let _guard = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3077        unsafe {
3078            std::env::remove_var(BOOTSTRAP_CERT_OUT_ENV);
3079        }
3080        let missing = BootstrapConfig::default();
3081        assert!(require_bootstrap_certificate_output(&missing, PRESET_PRODUCTION).is_err());
3082
3083        let configured = BootstrapConfig {
3084            cert_out: Some(bootstrap_cert_path("required-output")),
3085            ..BootstrapConfig::default()
3086        };
3087        assert!(require_bootstrap_certificate_output(&configured, PRESET_PRODUCTION).is_ok());
3088    }
3089
3090    #[test]
3091    fn official_images_enable_persistent_vault_auth() {
3092        for dockerfile in [
3093            include_str!("../../../Dockerfile"),
3094            include_str!("../../../docker/Dockerfile.release"),
3095            include_str!("../../../docker/Dockerfile.musl"),
3096        ] {
3097            assert!(dockerfile.contains("REDDB_AUTH=true"));
3098            assert!(dockerfile.contains("REDDB_REQUIRE_AUTH=true"));
3099            assert!(dockerfile.contains("REDDB_VAULT=true"));
3100            assert!(!dockerfile.contains("REDDB_VAULT=false"));
3101        }
3102    }
3103
3104    #[test]
3105    fn completed_required_auth_database_cannot_reenter_bootstrap() {
3106        let error = ensure_persisted_auth_state(true, true, true)
3107            .expect_err("empty auth state after durable completion must fail closed");
3108        assert!(error.contains("refusing to reopen public bootstrap"));
3109
3110        assert!(ensure_persisted_auth_state(false, true, true).is_ok());
3111        assert!(ensure_persisted_auth_state(true, false, true).is_ok());
3112        assert!(ensure_persisted_auth_state(true, true, false).is_ok());
3113    }
3114
3115    #[test]
3116    fn failed_certificate_delivery_reservation_is_removed() {
3117        let path = bootstrap_cert_path("failed-reservation");
3118        let bootstrap = BootstrapConfig {
3119            cert_out: Some(path.clone()),
3120            ..BootstrapConfig::default()
3121        };
3122
3123        let output = open_bootstrap_certificate_output(&bootstrap).expect("reserve output");
3124        assert!(path.exists());
3125        #[cfg(unix)]
3126        {
3127            use std::os::unix::fs::PermissionsExt;
3128            let mode = std::fs::metadata(&path)
3129                .expect("certificate metadata")
3130                .permissions()
3131                .mode()
3132                & 0o777;
3133            assert_eq!(mode, 0o600);
3134        }
3135        drop(output);
3136        assert!(!path.exists());
3137    }
3138
3139    #[test]
3140    fn configured_vault_credentials_do_not_require_certificate_output() {
3141        use crate::storage::engine::pager::{Pager, PagerConfig};
3142
3143        let config = crate::auth::AuthConfig {
3144            enabled: true,
3145            require_auth: true,
3146            vault_enabled: true,
3147            ..crate::auth::AuthConfig::default()
3148        };
3149
3150        let passphrase_dir = bootstrap_cert_path("passphrase-vault");
3151        std::fs::create_dir_all(&passphrase_dir).expect("passphrase temp directory");
3152        let passphrase_pager = Arc::new(
3153            Pager::open(passphrase_dir.join("test.rdb"), PagerConfig::default())
3154                .expect("passphrase pager"),
3155        );
3156        let passphrase_store = Arc::new(
3157            AuthStore::with_vault_passphrase(
3158                config.clone(),
3159                Arc::clone(&passphrase_pager),
3160                "test-passphrase",
3161            )
3162            .expect("passphrase vault"),
3163        );
3164        assert!(!passphrase_store.bootstrap_requires_certificate_delivery());
3165        bootstrap_with_certificate_output(&passphrase_store, "admin", "secret", &mut None)
3166            .expect("passphrase bootstrap without certificate output");
3167        drop(passphrase_store);
3168        drop(passphrase_pager);
3169        let _ = std::fs::remove_dir_all(passphrase_dir);
3170
3171        let certificate_dir = bootstrap_cert_path("certificate-vault");
3172        std::fs::create_dir_all(&certificate_dir).expect("certificate temp directory");
3173        let certificate_pager = Arc::new(
3174            Pager::open(certificate_dir.join("test.rdb"), PagerConfig::default())
3175                .expect("certificate pager"),
3176        );
3177        let certificate = "11".repeat(32);
3178        let certificate_store = Arc::new(
3179            AuthStore::with_vault_certificate(config, Arc::clone(&certificate_pager), &certificate)
3180                .expect("certificate vault"),
3181        );
3182        assert!(!certificate_store.bootstrap_requires_certificate_delivery());
3183        bootstrap_with_certificate_output(&certificate_store, "admin", "secret", &mut None)
3184            .expect("certificate bootstrap without duplicate output");
3185        drop(certificate_store);
3186        drop(certificate_pager);
3187        let _ = std::fs::remove_dir_all(certificate_dir);
3188    }
3189
3190    #[test]
3191    fn required_auth_simple_preset_waits_for_durable_admin() {
3192        let _guard = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3193        let runtime = RedDBRuntime::with_options(RedDBOptions::in_memory()).expect("runtime");
3194        let auth_store = Arc::new(AuthStore::new(crate::auth::AuthConfig {
3195            enabled: true,
3196            require_auth: true,
3197            vault_enabled: true,
3198            ..crate::auth::AuthConfig::default()
3199        }));
3200
3201        apply_preset(&runtime, &auth_store).expect("pending simple preset");
3202        assert!(runtime
3203            .db()
3204            .store()
3205            .try_get_config(BOOTSTRAP_COMPLETED_KEY)
3206            .expect("completion lookup")
3207            .is_none());
3208    }
3209
3210    #[test]
3211    fn server_options_preserve_explicit_disk_residency() {
3212        let _guard = no_auth_env_lock().lock().unwrap();
3213        let mut config = no_auth_test_config(true);
3214        assert!(!config.to_db_options().unwrap().bounded_disk_residency);
3215        config.bounded_disk_residency = true;
3216        assert!(config.to_db_options().unwrap().bounded_disk_residency);
3217    }
3218
3219    fn no_auth_test_config(no_auth: bool) -> ServerCommandConfig {
3220        ServerCommandConfig {
3221            path: None,
3222            router_bind_addr: Some(DEFAULT_ROUTER_BIND_ADDR.to_string()),
3223            router_bind_explicit: false,
3224            grpc_bind_addr: None,
3225            grpc_bind_explicit: false,
3226            grpc_tls_bind_addr: None,
3227            grpc_tls_cert: None,
3228            grpc_tls_key: None,
3229            grpc_tls_client_ca: None,
3230            http_bind_addr: None,
3231            http_bind_explicit: false,
3232            http_tls_bind_addr: None,
3233            http_tls_cert: None,
3234            http_tls_key: None,
3235            http_tls_client_ca: None,
3236            wire_bind_addr: None,
3237            wire_bind_explicit: false,
3238            wire_tls_bind_addr: None,
3239            wire_tls_cert: None,
3240            wire_tls_key: None,
3241            pg_bind_addr: None,
3242            create_if_missing: true,
3243            bounded_disk_residency: false,
3244            read_only: false,
3245            role: "standalone".to_string(),
3246            primary_addr: None,
3247            storage_profile: StorageProfileSelection::embedded_single_file(),
3248            auth: false,
3249            require_auth: false,
3250            // Operator-set `--vault`: `--no-auth` must override this
3251            // alongside REDDB_USERNAME/PASSWORD.
3252            vault: true,
3253            no_auth,
3254            workers: None,
3255            telemetry: None,
3256            http_limits_cli: crate::server::HttpLimitsCliInput::default(),
3257            ui: false,
3258            ui_dir: None,
3259            bootstrap: BootstrapConfig::default(),
3260        }
3261    }
3262
3263    #[test]
3264    fn no_auth_flag_disables_every_auth_knob_and_stamps_metadata() {
3265        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3266        // Pre-existing env that *would* turn auth on if `--no-auth`
3267        // weren't the last word. The acceptance criterion is that
3268        // the flag wins over env.
3269        // SAFETY: serialised by `no_auth_env_lock` above.
3270        unsafe {
3271            std::env::set_var("REDDB_USERNAME", "admin");
3272            std::env::set_var("REDDB_PASSWORD", "hunter2");
3273        }
3274        let config = no_auth_test_config(true);
3275        let options = config.to_db_options().expect("to_db_options");
3276
3277        assert!(no_auth_active(&options), "metadata should be stamped");
3278        assert!(!options.auth.enabled, "auth.enabled must be forced off");
3279        assert!(
3280            !options.auth.require_auth,
3281            "require_auth must be forced off"
3282        );
3283        assert!(
3284            !options.auth.vault_enabled,
3285            "vault_enabled must be forced off (overrides --vault)"
3286        );
3287        assert_eq!(
3288            options.metadata.get(NO_AUTH_META).map(String::as_str),
3289            Some("true"),
3290        );
3291
3292        // SAFETY: serialised by `no_auth_env_lock` above.
3293        unsafe {
3294            std::env::remove_var("REDDB_USERNAME");
3295            std::env::remove_var("REDDB_PASSWORD");
3296        }
3297    }
3298
3299    #[test]
3300    fn default_behaviour_without_no_auth_flag_is_unchanged() {
3301        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3302        let config = no_auth_test_config(false);
3303        let options = config.to_db_options().expect("to_db_options");
3304
3305        assert!(
3306            !no_auth_active(&options),
3307            "default boot must not be marked no-auth"
3308        );
3309        assert!(
3310            options.metadata.get(NO_AUTH_META).is_none(),
3311            "metadata key must be absent when flag is off"
3312        );
3313        // `--vault` should still take effect when `--no-auth` is not set.
3314        assert!(options.auth.vault_enabled);
3315    }
3316
3317    #[test]
3318    fn no_auth_active_blocks_bootstrap_from_env() {
3319        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3320        // SAFETY: serialised by `no_auth_env_lock` above. The pair
3321        // would normally cause `AuthStore::bootstrap_from_env` to
3322        // create an admin; the boot pipeline must suppress that call
3323        // whenever `no_auth_active` is true.
3324        unsafe {
3325            std::env::set_var("REDDB_USERNAME", "admin");
3326            std::env::set_var("REDDB_PASSWORD", "hunter2");
3327        }
3328
3329        let options = no_auth_test_config(true)
3330            .to_db_options()
3331            .expect("to_db_options");
3332
3333        // Mirror the exact branch in `build_runtime_with_telemetry`:
3334        // build a non-vault AuthStore from `options.auth`, then call
3335        // `bootstrap_from_env` *only* when the no-auth gate is off.
3336        let auth_store = AuthStore::new(options.auth.clone());
3337        if !no_auth_active(&options) {
3338            auth_store.bootstrap_from_env();
3339        }
3340
3341        assert!(
3342            auth_store.needs_bootstrap(),
3343            "no admin user must be bootstrapped under --no-auth even with REDDB_USERNAME/PASSWORD set"
3344        );
3345
3346        // SAFETY: serialised by `no_auth_env_lock` above.
3347        unsafe {
3348            std::env::remove_var("REDDB_USERNAME");
3349            std::env::remove_var("REDDB_PASSWORD");
3350        }
3351    }
3352
3353    // ---------- Issue #650 — bootstrap presets ----------
3354
3355    // Preset tests mutate process-global env (`REDDB_PRESET`,
3356    // `REDDB_USERNAME`, `REDDB_PASSWORD`) and the global tracing
3357    // subscriber. Share the no_auth lock so they don't race with each
3358    // other or with the --no-auth tests above.
3359    fn clear_preset_env() {
3360        // SAFETY: callers hold `no_auth_env_lock()`.
3361        unsafe {
3362            std::env::remove_var(BOOTSTRAP_PRESET_ENV);
3363            std::env::remove_var(PRESET_ENV);
3364            std::env::remove_var("REDDB_BOOTSTRAP_MANIFEST");
3365            std::env::remove_var("REDDB_AUTH");
3366            std::env::remove_var("REDDB_REQUIRE_AUTH");
3367            std::env::remove_var("REDDB_NO_AUTH");
3368            std::env::remove_var("REDDB_DEV");
3369            std::env::remove_var("REDDB_VAULT");
3370            std::env::remove_var("REDDB_USERNAME");
3371            std::env::remove_var("REDDB_PASSWORD");
3372            std::env::remove_var("REDDB_USERNAME_FILE");
3373            std::env::remove_var("REDDB_PASSWORD_FILE");
3374            std::env::remove_var(BOOTSTRAP_CERT_OUT_ENV);
3375            std::env::remove_var("REDDB_CLOUD_HEAD_ADMIN");
3376            std::env::remove_var("REDDB_CLOUD_HEAD_ADMIN_PASSWORD");
3377            std::env::remove_var("REDDB_CLOUD_HEAD_ADMIN_PASSWORD_FILE");
3378            std::env::remove_var("REDDB_CUSTOMER_ADMIN");
3379            std::env::remove_var("REDDB_CUSTOMER_ADMIN_PASSWORD");
3380            std::env::remove_var("REDDB_CUSTOMER_ADMIN_PASSWORD_FILE");
3381        }
3382    }
3383
3384    fn clear_backup_env() {
3385        // SAFETY: callers hold `no_auth_env_lock()`.
3386        unsafe {
3387            std::env::remove_var("REDDB_BACKUP_S3_ENDPOINT");
3388            std::env::remove_var("REDDB_BACKUP_S3_BUCKET");
3389            std::env::remove_var("REDDB_BACKUP_S3_PREFIX");
3390            std::env::remove_var("REDDB_BACKUP_S3_ACCESS_KEY_ID");
3391            std::env::remove_var("REDDB_BACKUP_S3_SECRET_ACCESS_KEY");
3392            std::env::remove_var("REDDB_BACKUP_S3_REGION");
3393            std::env::remove_var("REDDB_BACKUP_CHECKPOINT_INTERVAL_SECS");
3394            std::env::remove_var("REDDB_BACKUP_WAL_FLUSH_INTERVAL_SECS");
3395            std::env::remove_var("REDDB_BACKUP_PAUSE_ON_LAG_SECS");
3396        }
3397    }
3398
3399    fn fresh_runtime_and_store() -> (RedDBRuntime, Arc<AuthStore>) {
3400        let runtime = RedDBRuntime::with_options(RedDBOptions::in_memory()).expect("runtime");
3401        let auth_store = Arc::new(AuthStore::new(crate::auth::AuthConfig::default()));
3402        (runtime, auth_store)
3403    }
3404
3405    #[test]
3406    fn auth_env_knobs_enable_auth_require_auth_and_vault() {
3407        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3408        clear_preset_env();
3409        // SAFETY: env serialised by `no_auth_env_lock`.
3410        unsafe {
3411            std::env::set_var("REDDB_AUTH", "true");
3412            std::env::set_var("REDDB_REQUIRE_AUTH", "true");
3413            std::env::set_var("REDDB_VAULT", "true");
3414        }
3415
3416        let mut config = no_auth_test_config(false);
3417        config.vault = false;
3418        let options = config.to_db_options().expect("to_db_options");
3419
3420        assert!(options.auth.enabled);
3421        assert!(options.auth.require_auth);
3422        assert!(options.auth.vault_enabled);
3423
3424        clear_preset_env();
3425    }
3426
3427    #[test]
3428    fn production_and_cloud_presets_force_auth_require_auth_and_vault() {
3429        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3430        clear_preset_env();
3431
3432        for preset in [PRESET_PRODUCTION, PRESET_CLOUD] {
3433            let mut config = no_auth_test_config(false);
3434            config.vault = false;
3435            config.bootstrap.preset = Some(preset.to_string());
3436
3437            let options = config.to_db_options().expect("to_db_options");
3438            assert!(options.auth.enabled, "{preset} should enable auth");
3439            assert!(
3440                options.auth.require_auth,
3441                "{preset} should require authenticated requests"
3442            );
3443            assert!(options.auth.vault_enabled, "{preset} should enable vault");
3444            assert!(!no_auth_active(&options));
3445        }
3446
3447        clear_preset_env();
3448    }
3449
3450    #[test]
3451    fn no_auth_env_overrides_preset_forced_auth() {
3452        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3453        clear_preset_env();
3454        // SAFETY: env serialised by `no_auth_env_lock`.
3455        unsafe {
3456            std::env::set_var("REDDB_NO_AUTH", "true");
3457            std::env::set_var(BOOTSTRAP_PRESET_ENV, PRESET_CLOUD);
3458        }
3459
3460        let mut config = no_auth_test_config(false);
3461        config.auth = true;
3462        config.require_auth = true;
3463        let options = config.to_db_options().expect("to_db_options");
3464
3465        assert!(no_auth_active(&options));
3466        assert!(!options.auth.enabled);
3467        assert!(!options.auth.require_auth);
3468        assert!(!options.auth.vault_enabled);
3469
3470        clear_preset_env();
3471    }
3472
3473    #[test]
3474    fn simple_preset_is_default_and_persists_state() {
3475        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3476        clear_preset_env();
3477
3478        let (runtime, auth_store) = fresh_runtime_and_store();
3479        apply_preset(&runtime, &auth_store).expect("simple preset applies cleanly");
3480
3481        // No admin was created — `simple` is anonymous-friendly.
3482        assert!(
3483            auth_store.needs_bootstrap(),
3484            "simple preset must not create an admin"
3485        );
3486
3487        // Bootstrap state persisted so the next boot is a no-op.
3488        let store = runtime.db().store();
3489        let completed = store
3490            .get_config(BOOTSTRAP_COMPLETED_KEY)
3491            .expect("completed key persisted");
3492        assert!(matches!(completed, reddb_types::Value::Boolean(true)));
3493        let preset = store
3494            .get_config(BOOTSTRAP_PRESET_KEY)
3495            .expect("preset key persisted");
3496        match preset {
3497            reddb_types::Value::Text(s) => assert_eq!(s.as_ref(), PRESET_SIMPLE),
3498            other => panic!("expected Text(simple), got {other:?}"),
3499        }
3500        assert!(
3501            store.get_config(BOOTSTRAP_FIRST_ADMIN_KEY).is_none(),
3502            "simple preset must not record a first admin"
3503        );
3504
3505        clear_preset_env();
3506    }
3507
3508    #[test]
3509    fn production_preset_creates_first_admin_with_allow_all_policy() {
3510        use crate::auth::policies::{EvalContext, ResourceRef};
3511        use crate::auth::UserId;
3512
3513        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3514        clear_preset_env();
3515        let cert_out = bootstrap_cert_path("production-admin");
3516        // SAFETY: env serialised by `no_auth_env_lock`.
3517        unsafe {
3518            std::env::set_var(PRESET_ENV, PRESET_PRODUCTION);
3519            std::env::set_var("REDDB_USERNAME", "ops");
3520            std::env::set_var("REDDB_PASSWORD", "hunter2");
3521            std::env::set_var(BOOTSTRAP_CERT_OUT_ENV, &cert_out);
3522        }
3523
3524        let (runtime, auth_store) = fresh_runtime_and_store();
3525        apply_preset(&runtime, &auth_store).expect("production preset applies cleanly");
3526
3527        // Admin exists and the auth store is sealed.
3528        assert!(
3529            !auth_store.needs_bootstrap(),
3530            "production preset must seal bootstrap"
3531        );
3532        let users = auth_store.list_users();
3533        assert_eq!(users.len(), 1);
3534        let admin = &users[0];
3535        assert_eq!(admin.username, "ops");
3536        assert!(
3537            admin.tenant_id.is_none(),
3538            "first admin must be platform-scoped (tenant=None)"
3539        );
3540
3541        // Allow-all policy was installed and attached to the first admin.
3542        let policy = auth_store
3543            .get_policy(FIRST_ADMIN_ALLOW_ALL_POLICY)
3544            .expect("allow-all policy installed");
3545        assert!(!policy.statements.is_empty());
3546
3547        // Verify policy-derived authority via the policy evaluator —
3548        // not a bypass. Any action on any resource must Allow.
3549        let actor = UserId::platform("ops");
3550        let ctx = EvalContext {
3551            principal_tenant: None,
3552            current_tenant: None,
3553            peer_ip: None,
3554            mfa_present: false,
3555            now_ms: 1_700_000_000_000,
3556            principal_is_admin_role: true,
3557            principal_is_platform_scoped: true,
3558        };
3559        let arbitrary_resource = ResourceRef::new("config", "red.config.audit.enabled");
3560        assert!(
3561            auth_store.check_policy_authz(&actor, "config:write", &arbitrary_resource, &ctx),
3562            "allow-all policy must grant arbitrary actions via the evaluator"
3563        );
3564
3565        // Persisted state records the first admin id.
3566        let store = runtime.db().store();
3567        match store
3568            .get_config(BOOTSTRAP_FIRST_ADMIN_KEY)
3569            .expect("first_admin_id persisted")
3570        {
3571            reddb_types::Value::Text(s) => assert_eq!(s.as_ref(), "ops"),
3572            other => panic!("expected Text(ops), got {other:?}"),
3573        }
3574        match store.get_config(BOOTSTRAP_PRESET_KEY).unwrap() {
3575            reddb_types::Value::Text(s) => assert_eq!(s.as_ref(), PRESET_PRODUCTION),
3576            other => panic!("expected Text(production), got {other:?}"),
3577        }
3578
3579        clear_preset_env();
3580        let _ = std::fs::remove_file(cert_out);
3581    }
3582
3583    #[test]
3584    fn bootstrap_preset_env_takes_precedence_over_legacy_preset_env() {
3585        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3586        clear_preset_env();
3587        // SAFETY: env serialised by `no_auth_env_lock`.
3588        unsafe {
3589            std::env::set_var(BOOTSTRAP_PRESET_ENV, PRESET_REGULATED);
3590            std::env::set_var(PRESET_ENV, PRESET_SIMPLE);
3591        }
3592
3593        let options = no_auth_test_config(false)
3594            .to_db_options()
3595            .expect("regulated options");
3596        assert!(
3597            options.control_events.compliance_mode,
3598            "canonical REDDB_BOOTSTRAP_PRESET should win over REDDB_PRESET"
3599        );
3600        assert!(options.query_audit.enabled);
3601
3602        clear_preset_env();
3603    }
3604
3605    #[test]
3606    fn cloud_preset_creates_system_head_and_customer_admins() {
3607        use crate::auth::policies::{EvalContext, ResourceRef};
3608        use crate::auth::UserId;
3609
3610        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3611        clear_preset_env();
3612        let cert_out = bootstrap_cert_path("cloud-admins");
3613        // SAFETY: env serialised by `no_auth_env_lock`.
3614        unsafe {
3615            std::env::set_var(BOOTSTRAP_PRESET_ENV, PRESET_CLOUD);
3616            std::env::set_var("REDDB_CLOUD_HEAD_ADMIN", "head");
3617            std::env::set_var("REDDB_CLOUD_HEAD_ADMIN_PASSWORD", "head-pass");
3618            std::env::set_var("REDDB_CUSTOMER_ADMIN", "customer");
3619            std::env::set_var("REDDB_CUSTOMER_ADMIN_PASSWORD", "customer-pass");
3620            std::env::set_var(BOOTSTRAP_CERT_OUT_ENV, &cert_out);
3621        }
3622
3623        let (runtime, auth_store) = fresh_runtime_and_store();
3624        apply_preset(&runtime, &auth_store).expect("cloud preset applies cleanly");
3625
3626        let head = auth_store
3627            .get_user(None, "head")
3628            .expect("head admin should exist");
3629        assert_eq!(head.tenant_id, None);
3630        let customer = auth_store
3631            .get_user(None, "customer")
3632            .expect("customer admin should exist");
3633        assert_eq!(customer.tenant_id, None);
3634
3635        let ctx = EvalContext {
3636            principal_tenant: None,
3637            current_tenant: None,
3638            peer_ip: None,
3639            mfa_present: false,
3640            now_ms: 1_700_000_000_000,
3641            principal_is_admin_role: true,
3642            principal_is_platform_scoped: true,
3643        };
3644        assert!(auth_store.check_policy_authz(
3645            &UserId::platform("customer"),
3646            "config:write",
3647            &ResourceRef::new("config", "red.config.customer.enabled"),
3648            &ctx,
3649        ));
3650        assert!(
3651            auth_store.check_policy_authz(
3652                &UserId::platform("head"),
3653                "config:write",
3654                &ResourceRef::new("config", "red.config.cloud.enabled"),
3655                &ctx,
3656            ),
3657            "cloud head keeps allow-all authority over managed cloud config"
3658        );
3659        assert!(
3660            !auth_store.check_policy_authz(
3661                &UserId::platform("customer"),
3662                "config:write",
3663                &ResourceRef::new("config", "red.config.cloud.enabled"),
3664                &ctx,
3665            ),
3666            "cloud customer must be denied managed cloud config writes"
3667        );
3668        assert!(
3669            !auth_store.check_policy_authz(
3670                &UserId::platform("customer"),
3671                "policy:drop",
3672                &ResourceRef::new("policy", CLOUD_PROTECT_MANAGED_POLICY),
3673                &ctx,
3674            ),
3675            "cloud customer must be denied mutations of the managed guardrail policy"
3676        );
3677
3678        assert!(auth_store.get_user(None, "head").is_some());
3679        assert!(auth_store
3680            .get_policy(CLOUD_PROTECT_MANAGED_POLICY)
3681            .is_some());
3682        assert!(runtime
3683            .config_registry()
3684            .get_active(CLOUD_CONFIG_NAMESPACE)
3685            .is_some());
3686
3687        let store = runtime.db().store();
3688        match store
3689            .get_config(BOOTSTRAP_FIRST_ADMIN_KEY)
3690            .expect("head admin id persisted")
3691        {
3692            reddb_types::Value::Text(s) => assert_eq!(s.as_ref(), "head"),
3693            other => panic!("expected Text(head), got {other:?}"),
3694        }
3695        match store.get_config(BOOTSTRAP_PRESET_KEY).unwrap() {
3696            reddb_types::Value::Text(s) => assert_eq!(s.as_ref(), PRESET_CLOUD),
3697            other => panic!("expected Text(cloud), got {other:?}"),
3698        }
3699
3700        clear_preset_env();
3701        let _ = std::fs::remove_file(cert_out);
3702    }
3703
3704    #[test]
3705    fn cloud_preset_customer_cannot_disable_head_admin_or_drop_guardrail_policy() {
3706        use crate::auth::Role;
3707        use crate::runtime::mvcc::{clear_current_auth_identity, set_current_auth_identity};
3708
3709        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3710        clear_preset_env();
3711
3712        let bootstrap = BootstrapConfig {
3713            preset: Some(PRESET_CLOUD.to_string()),
3714            cloud_head_admin: Some("red_admin".to_string()),
3715            cloud_head_admin_password: Some("head-pass".to_string()),
3716            customer_admin: Some("admin".to_string()),
3717            customer_admin_password: Some("customer-pass".to_string()),
3718            cert_out: Some(bootstrap_cert_path("cloud-guardrail")),
3719            ..BootstrapConfig::default()
3720        };
3721        let (runtime, auth_store) = fresh_runtime_and_store();
3722        apply_preset_from_config(&runtime, &auth_store, &bootstrap)
3723            .expect("cloud preset applies cleanly");
3724        runtime.set_auth_store(Arc::clone(&auth_store));
3725
3726        set_current_auth_identity("admin".to_string(), Role::Admin);
3727        let disable_head = runtime.execute_query("ALTER USER red_admin DISABLE");
3728        let drop_guardrail =
3729            runtime.execute_query(&format!("DROP POLICY '{}'", CLOUD_PROTECT_MANAGED_POLICY));
3730        let create_user = runtime.execute_query("CREATE USER app_user WITH PASSWORD 'p' ROLE read");
3731        let disable_user = runtime.execute_query("ALTER USER app_user DISABLE");
3732        clear_current_auth_identity();
3733
3734        let disable_head_err = disable_head.expect_err("customer admin must not disable red_admin");
3735        assert!(
3736            disable_head_err.to_string().contains("user:disable"),
3737            "error should name the denied lifecycle action: {disable_head_err}"
3738        );
3739        assert!(
3740            auth_store
3741                .get_user(None, "red_admin")
3742                .expect("red_admin should still exist")
3743                .enabled,
3744            "denied mutation must leave red_admin enabled"
3745        );
3746
3747        let drop_guardrail_err =
3748            drop_guardrail.expect_err("customer admin must not drop cloud guardrail policy");
3749        assert!(
3750            drop_guardrail_err.to_string().contains("managed policy"),
3751            "error should name the managed policy guardrail: {drop_guardrail_err}"
3752        );
3753        assert!(auth_store
3754            .get_policy(CLOUD_PROTECT_MANAGED_POLICY)
3755            .is_some());
3756
3757        create_user.expect("customer admin should retain normal create-user authority");
3758        disable_user.expect("customer admin should retain normal user-disable authority");
3759        assert!(
3760            !auth_store
3761                .get_user(None, "app_user")
3762                .expect("app_user should exist")
3763                .enabled,
3764            "normal capability should still apply outside the reserved head admin"
3765        );
3766
3767        let _ = bootstrap.cert_out.as_ref().map(std::fs::remove_file);
3768        clear_preset_env();
3769    }
3770
3771    #[test]
3772    fn cloud_preset_cli_admin_alias_wins_over_cloud_env_password() {
3773        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3774        clear_preset_env();
3775        // SAFETY: env serialised by `no_auth_env_lock`.
3776        unsafe {
3777            std::env::set_var("REDDB_CLOUD_HEAD_ADMIN_PASSWORD", "env-head-pass");
3778        }
3779
3780        let bootstrap = BootstrapConfig {
3781            preset: Some(PRESET_CLOUD.to_string()),
3782            admin_username: Some("head".to_string()),
3783            admin_password: Some("cli-head-pass".to_string()),
3784            customer_admin: Some("customer".to_string()),
3785            customer_admin_password: Some("customer-pass".to_string()),
3786            cert_out: Some(bootstrap_cert_path("cloud-alias")),
3787            ..BootstrapConfig::default()
3788        };
3789        let (runtime, auth_store) = fresh_runtime_and_store();
3790        apply_preset_from_config(&runtime, &auth_store, &bootstrap)
3791            .expect("cloud preset applies cleanly");
3792
3793        auth_store
3794            .authenticate("head", "cli-head-pass")
3795            .expect("CLI alias password should win");
3796        assert!(
3797            auth_store.authenticate("head", "env-head-pass").is_err(),
3798            "cloud-specific env password must not beat CLI alias"
3799        );
3800
3801        let _ = bootstrap.cert_out.as_ref().map(std::fs::remove_file);
3802        clear_preset_env();
3803    }
3804
3805    #[test]
3806    fn bootstrap_intent_manifest_matches_cloud_flag_path() {
3807        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3808        clear_preset_env();
3809
3810        let manifest_dir = std::env::current_dir()
3811            .expect("current dir")
3812            .join(".red/tmp/bootstrap-manifest-tests");
3813        std::fs::create_dir_all(&manifest_dir).expect("create manifest test dir");
3814        let unique = format!(
3815            "{}-{}",
3816            std::process::id(),
3817            std::time::SystemTime::now()
3818                .duration_since(std::time::UNIX_EPOCH)
3819                .unwrap_or_default()
3820                .as_millis()
3821        );
3822        let head_password = manifest_dir.join(format!("head-{unique}.secret"));
3823        let customer_password = manifest_dir.join(format!("customer-{unique}.secret"));
3824        let manifest_path = manifest_dir.join(format!("intent-{unique}.json"));
3825        std::fs::write(&head_password, "head-pass\n").expect("write head secret");
3826        std::fs::write(&customer_password, "customer-pass\n").expect("write customer secret");
3827        std::fs::write(
3828            &manifest_path,
3829            format!(
3830                r#"{{
3831                    "bootstrap": {{
3832                        "preset": "cloud",
3833                        "cloud_head_admin": {{
3834                            "username": "head",
3835                            "password_file": "{}"
3836                        }},
3837                        "customer_admin": {{
3838                            "username": "customer",
3839                            "password_file": "{}"
3840                        }}
3841                    }}
3842                }}"#,
3843                head_password.display(),
3844                customer_password.display()
3845            ),
3846        )
3847        .expect("write manifest");
3848
3849        let mut config = no_auth_test_config(false);
3850        config.bootstrap.manifest = Some(manifest_path.clone());
3851        let options = config.to_db_options().expect("manifest options");
3852        assert!(options.auth.enabled);
3853        assert!(options.auth.require_auth);
3854        assert!(options.auth.vault_enabled);
3855
3856        let (runtime, auth_store) = fresh_runtime_and_store();
3857        apply_preset_from_config(&runtime, &auth_store, &config.bootstrap)
3858            .expect("manifest intent applies cleanly");
3859
3860        auth_store
3861            .authenticate("head", "head-pass")
3862            .expect("head password comes from file");
3863        auth_store
3864            .authenticate("customer", "customer-pass")
3865            .expect("customer password comes from file");
3866        assert!(auth_store.get_user(None, "head").is_some());
3867        assert!(auth_store.get_user(None, "customer").is_some());
3868        assert!(auth_store
3869            .get_policy(CLOUD_PROTECT_MANAGED_POLICY)
3870            .is_some());
3871        match runtime
3872            .db()
3873            .store()
3874            .get_config(BOOTSTRAP_PRESET_KEY)
3875            .expect("preset key persisted")
3876        {
3877            reddb_types::Value::Text(s) => assert_eq!(s.as_ref(), PRESET_CLOUD),
3878            other => panic!("expected Text(cloud), got {other:?}"),
3879        }
3880
3881        apply_preset_from_config(&runtime, &auth_store, &config.bootstrap)
3882            .expect("second boot is idempotent");
3883
3884        let _ = std::fs::remove_file(&manifest_path);
3885        let _ = std::fs::remove_file(&head_password);
3886        let _ = std::fs::remove_file(&customer_password);
3887        clear_preset_env();
3888    }
3889
3890    #[test]
3891    fn regulated_preset_enables_query_audit_infrastructure_without_rules() {
3892        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3893        clear_preset_env();
3894        // SAFETY: env serialised by `no_auth_env_lock`.
3895        unsafe {
3896            std::env::set_var(PRESET_ENV, PRESET_REGULATED);
3897        }
3898
3899        let (runtime, auth_store) = fresh_runtime_and_store();
3900        apply_preset(&runtime, &auth_store).expect("regulated preset applies cleanly");
3901
3902        assert!(runtime.query_audit().is_enabled());
3903        assert!(runtime.query_audit().rules().is_empty());
3904        assert!(
3905            runtime
3906                .db()
3907                .store()
3908                .get_collection(crate::runtime::query_audit::QUERY_AUDIT_COLLECTION)
3909                .is_some(),
3910            "regulated preset should create the query-audit stream"
3911        );
3912
3913        runtime
3914            .execute_query("CREATE TABLE docs (id INT)")
3915            .expect("create table");
3916        runtime
3917            .execute_query("INSERT INTO docs (id) VALUES (1)")
3918            .expect("insert");
3919        runtime.execute_query("SELECT * FROM docs").expect("select");
3920        let rows = runtime
3921            .db()
3922            .store()
3923            .get_collection(crate::runtime::query_audit::QUERY_AUDIT_COLLECTION)
3924            .expect("query audit collection")
3925            .query_all(|_| true);
3926        assert!(
3927            rows.is_empty(),
3928            "regulated preset must not globally audit every query"
3929        );
3930
3931        clear_preset_env();
3932    }
3933
3934    #[test]
3935    fn managed_backup_env_rejects_primary_replica_single_file_storage() {
3936        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3937        clear_backup_env();
3938        // SAFETY: env serialised by `no_auth_env_lock`.
3939        unsafe {
3940            std::env::set_var("REDDB_BACKUP_S3_ENDPOINT", "https://s3.example.test");
3941            std::env::set_var("REDDB_BACKUP_S3_BUCKET", "reddb");
3942            std::env::set_var("REDDB_BACKUP_S3_PREFIX", "clusters/prod");
3943            std::env::set_var("REDDB_BACKUP_S3_ACCESS_KEY_ID", "AK");
3944            std::env::set_var("REDDB_BACKUP_S3_SECRET_ACCESS_KEY", "SK");
3945        }
3946
3947        let mut config = no_auth_test_config(false);
3948        config.role = "primary".to_string();
3949        config.storage_profile = crate::storage::StorageDeployPreset::PrimaryReplicaDev.selection();
3950
3951        let err = config.to_db_options().unwrap_err();
3952        assert!(err.contains("managed backup"), "got: {err}");
3953        assert!(err.contains("operational-directory"), "got: {err}");
3954
3955        clear_backup_env();
3956    }
3957
3958    #[test]
3959    fn regulated_preset_installs_managed_evidence_guardrails_end_to_end() {
3960        use crate::auth::policies::{EvalContext, Policy, ResourceRef};
3961        use crate::auth::store::PrincipalRef;
3962        use crate::auth::{Role, UserId};
3963        use crate::runtime::mvcc::{clear_current_auth_identity, set_current_auth_identity};
3964        use reddb_types::Value;
3965
3966        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3967        clear_preset_env();
3968        // SAFETY: env serialised by `no_auth_env_lock`.
3969        unsafe {
3970            std::env::set_var(PRESET_ENV, PRESET_REGULATED);
3971        }
3972
3973        let options = no_auth_test_config(false)
3974            .to_db_options()
3975            .expect("regulated options");
3976        assert!(
3977            options.control_events.compliance_mode,
3978            "regulated preset must enable fail-closed control evidence before runtime boot"
3979        );
3980        assert!(
3981            options.query_audit.enabled && options.query_audit.rules.is_empty(),
3982            "regulated preset must enable query-audit infrastructure without global rules"
3983        );
3984
3985        let runtime = RedDBRuntime::with_options(options).expect("runtime");
3986        let auth_store = Arc::new(AuthStore::new(crate::auth::AuthConfig::default()));
3987        apply_preset(&runtime, &auth_store).expect("regulated preset applies cleanly");
3988        runtime.set_auth_store(Arc::clone(&auth_store));
3989
3990        assert!(runtime.control_events_require_persistence());
3991        assert!(runtime.query_audit().is_enabled());
3992        assert!(runtime.query_audit().rules().is_empty());
3993        assert!(auth_store
3994            .get_policy(REGULATED_PROTECT_MANAGED_POLICY)
3995            .is_some());
3996
3997        let managed_policy = runtime
3998            .config_registry()
3999            .get_active(REGULATED_PROTECT_MANAGED_POLICY)
4000            .expect("regulated managed policy registry entry");
4001        assert!(managed_policy.managed);
4002        assert_eq!(managed_policy.resource_type, "policy");
4003        assert!(
4004            runtime
4005                .config_registry()
4006                .get_active(REGULATED_AUDIT_CONFIG_NAMESPACE)
4007                .expect("regulated audit config namespace")
4008                .managed
4009        );
4010
4011        let registry_rows = runtime
4012            .execute_query(&format!(
4013                "SELECT id, managed FROM red.registry WHERE id = '{}'",
4014                REGULATED_PROTECT_MANAGED_POLICY
4015            ))
4016            .expect("red.registry query");
4017        assert_eq!(registry_rows.result.records.len(), 1);
4018        assert_eq!(
4019            registry_rows.result.records[0].get("managed"),
4020            Some(&Value::Boolean(true))
4021        );
4022
4023        let managed_policy_rows = runtime
4024            .execute_query(&format!(
4025                "SELECT policy_id FROM red.managed_policies WHERE policy_id = '{}'",
4026                REGULATED_PROTECT_MANAGED_POLICY
4027            ))
4028            .expect("red.managed_policies query");
4029        assert_eq!(managed_policy_rows.result.records.len(), 1);
4030
4031        let capability_rows = runtime
4032            .execute_query(
4033                "SELECT action FROM red.control_capabilities WHERE action = 'evidence:export'",
4034            )
4035            .expect("red.control_capabilities query");
4036        assert_eq!(capability_rows.result.records.len(), 1);
4037
4038        auth_store
4039            .create_user("alice", "p", Role::Admin)
4040            .expect("create ordinary admin");
4041        let allow_all = Policy::from_json_str(
4042            r#"{
4043                "id": "alice-allow-all",
4044                "version": 1,
4045                "statements": [{
4046                    "effect": "allow",
4047                    "actions": ["*"],
4048                    "resources": ["*"]
4049                }]
4050            }"#,
4051        )
4052        .expect("allow-all policy");
4053        auth_store.put_policy(allow_all).expect("install allow-all");
4054        auth_store
4055            .attach_policy(
4056                PrincipalRef::User(UserId::platform("alice")),
4057                "alice-allow-all",
4058            )
4059            .expect("attach allow-all");
4060        let ctx = EvalContext {
4061            principal_tenant: None,
4062            current_tenant: None,
4063            peer_ip: None,
4064            mfa_present: false,
4065            now_ms: 1_700_000_000_000,
4066            principal_is_admin_role: true,
4067            principal_is_platform_scoped: true,
4068        };
4069        assert!(
4070            !auth_store.check_policy_authz(
4071                &UserId::platform("alice"),
4072                "policy:drop",
4073                &ResourceRef::new("policy", REGULATED_PROTECT_MANAGED_POLICY),
4074                &ctx,
4075            ),
4076            "managed guardrail deny policy must be effective for ordinary admins"
4077        );
4078
4079        set_current_auth_identity("alice".to_string(), Role::Admin);
4080        let denied = runtime.execute_query(&format!(
4081            "DROP POLICY '{}'",
4082            REGULATED_PROTECT_MANAGED_POLICY
4083        ));
4084        clear_current_auth_identity();
4085        let err = denied.expect_err("managed policy guardrail must deny ordinary admin");
4086        assert!(
4087            err.to_string().contains("managed policy"),
4088            "error should name the managed guardrail: {err}"
4089        );
4090        assert!(
4091            auth_store
4092                .get_policy(REGULATED_PROTECT_MANAGED_POLICY)
4093                .is_some(),
4094            "denied mutation must leave managed policy installed"
4095        );
4096
4097        let denied_events = runtime
4098            .execute_query(&format!(
4099                "SELECT action, resource, outcome FROM red.control_events \
4100                 WHERE action = 'policy:drop' AND resource = 'policy:{}'",
4101                REGULATED_PROTECT_MANAGED_POLICY
4102            ))
4103            .expect("red.control_events denied policy drop");
4104        assert_eq!(denied_events.result.records.len(), 1);
4105        assert_eq!(
4106            denied_events.result.records[0].get("outcome"),
4107            Some(&Value::text("denied"))
4108        );
4109
4110        set_current_auth_identity("alice".to_string(), Role::Admin);
4111        let config_denied = runtime.execute_query("SET CONFIG red.config.audit.enabled = true");
4112        clear_current_auth_identity();
4113        let err = config_denied.expect_err("managed config guardrail must deny ordinary admin");
4114        assert!(
4115            err.to_string().contains("managed config"),
4116            "error should name the managed config guardrail: {err}"
4117        );
4118
4119        let denied_config_events = runtime
4120            .execute_query(
4121                "SELECT action, resource, outcome FROM red.control_events \
4122                 WHERE action = 'config:write' AND resource = 'config:red.config.audit.enabled'",
4123            )
4124            .expect("red.control_events denied config write");
4125        assert_eq!(denied_config_events.result.records.len(), 1);
4126        assert_eq!(
4127            denied_config_events.result.records[0].get("outcome"),
4128            Some(&Value::text("denied"))
4129        );
4130
4131        runtime
4132            .execute_query("CREATE TABLE regulated_docs (id INT)")
4133            .expect("create user table");
4134        runtime
4135            .execute_query("SELECT * FROM regulated_docs")
4136            .expect("select user table");
4137        let audit_rows = runtime
4138            .db()
4139            .store()
4140            .get_collection(crate::runtime::query_audit::QUERY_AUDIT_COLLECTION)
4141            .expect("query audit collection")
4142            .query_all(|_| true);
4143        assert!(
4144            audit_rows.is_empty(),
4145            "regulated preset must not globally audit data-plane queries"
4146        );
4147
4148        clear_preset_env();
4149    }
4150
4151    #[test]
4152    fn bootstrap_manifest_installs_initial_users_policies_guardrails_and_config() {
4153        use crate::auth::policies::{EvalContext, ResourceRef};
4154        use crate::auth::UserId;
4155        use reddb_types::Value;
4156
4157        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
4158        clear_preset_env();
4159
4160        let manifest_dir = std::env::current_dir()
4161            .expect("current dir")
4162            .join(".red/tmp/bootstrap-manifest-tests");
4163        std::fs::create_dir_all(&manifest_dir).expect("create manifest test dir");
4164        let manifest_path = manifest_dir.join(format!(
4165            "reddb-bootstrap-manifest-{}-{}.json",
4166            std::process::id(),
4167            std::time::SystemTime::now()
4168                .duration_since(std::time::UNIX_EPOCH)
4169                .unwrap_or_default()
4170                .as_millis()
4171        ));
4172        std::fs::write(
4173            &manifest_path,
4174            r#"{
4175                "users": [
4176                    {
4177                        "username": "ops",
4178                        "password": "hunter2",
4179                        "role": "admin"
4180                    }
4181                ],
4182                "policies": [
4183                    {
4184                        "id": "bootstrap-registry-admin",
4185                        "version": 1,
4186                        "statements": [
4187                            {
4188                                "effect": "allow",
4189                                "actions": ["red.registry:*", "policy:*", "config:write", "select"],
4190                                "resources": ["registry:*", "policy:*", "config:*", "collection:docs"]
4191                            }
4192                        ]
4193                    }
4194                ],
4195                "managed_policies": [
4196                    {
4197                        "id": "managed-deny-drop",
4198                        "version": 1,
4199                        "statements": [
4200                            {
4201                                "effect": "deny",
4202                                "actions": ["policy:drop"],
4203                                "resources": ["policy:managed-deny-drop"]
4204                            }
4205                        ],
4206                        "required_resource": "policy:managed-deny-drop",
4207                        "evidence": "full"
4208                    }
4209                ],
4210                "attachments": [
4211                    {"user": "ops", "policy": "bootstrap-registry-admin"}
4212                ],
4213                "managed_config_namespaces": [
4214                    {
4215                        "id": "red.ai",
4216                        "required_action": "config:write",
4217                        "required_resource": "config:red.ai.*",
4218                        "evidence": "metadata"
4219                    }
4220                ],
4221                "config": [
4222                    {"key": "red.ai.default.provider", "value": "openai"},
4223                    {
4224                        "key": "red.ai.openai.default.secret_ref",
4225                        "secret_ref": {"collection": "red.vault", "key": "openai"}
4226                    }
4227                ],
4228                "actor": "ops"
4229            }"#,
4230        )
4231        .expect("write manifest");
4232        // SAFETY: env serialised by `no_auth_env_lock`.
4233        unsafe {
4234            std::env::set_var("REDDB_BOOTSTRAP_MANIFEST", &manifest_path);
4235        }
4236
4237        let (runtime, auth_store) = fresh_runtime_and_store();
4238        apply_preset(&runtime, &auth_store).expect("manifest applies cleanly");
4239
4240        let users = auth_store.list_users();
4241        assert_eq!(users.len(), 1);
4242        assert_eq!(users[0].username, "ops");
4243        assert!(users[0].tenant_id.is_none());
4244
4245        let actor = UserId::platform("ops");
4246        let ctx = EvalContext {
4247            principal_tenant: None,
4248            current_tenant: None,
4249            peer_ip: None,
4250            mfa_present: false,
4251            now_ms: 1_700_000_000_000,
4252            principal_is_admin_role: true,
4253            principal_is_platform_scoped: true,
4254        };
4255        // Manifest fixture pins a canonical data-plane read action.
4256        assert!(auth_store.check_policy_authz(
4257            &actor,
4258            "select",
4259            &ResourceRef::new("collection", "docs"),
4260            &ctx
4261        ));
4262
4263        let managed_policy = runtime
4264            .config_registry()
4265            .get_active("managed-deny-drop")
4266            .expect("managed policy registry entry");
4267        assert!(managed_policy.managed);
4268        assert_eq!(managed_policy.resource_type, "policy");
4269        let managed_config = runtime
4270            .config_registry()
4271            .get_active("red.ai")
4272            .expect("managed config namespace registry entry");
4273        assert!(managed_config.managed);
4274        assert_eq!(managed_config.resource_type, "config_namespace");
4275
4276        let store = runtime.db().store();
4277        match store
4278            .get_config("red.ai.default.provider")
4279            .expect("plain config persisted")
4280        {
4281            Value::Text(s) => assert_eq!(s.as_ref(), "openai"),
4282            other => panic!("expected provider text, got {other:?}"),
4283        }
4284        let Value::Json(bytes) = store
4285            .get_config("red.ai.openai.default.secret_ref")
4286            .expect("secret ref config persisted")
4287        else {
4288            panic!("secret ref must be stored as structured JSON");
4289        };
4290        let reference: crate::serde_json::Value =
4291            crate::serde_json::from_slice(&bytes).expect("secret ref json");
4292        assert_eq!(
4293            reference.get("type").and_then(|v| v.as_str()),
4294            Some("secret_ref")
4295        );
4296        assert!(
4297            !String::from_utf8_lossy(&bytes).contains("hunter2"),
4298            "manifest password must not leak into secret ref config"
4299        );
4300
4301        let completed = store
4302            .get_config(BOOTSTRAP_COMPLETED_KEY)
4303            .expect("bootstrap completion persisted");
4304        assert!(matches!(completed, Value::Boolean(true)));
4305        assert!(
4306            store
4307                .get_config("system.bootstrap.manifest.registry_entries")
4308                .is_some(),
4309            "managed registry entries must be persisted internally"
4310        );
4311
4312        std::fs::remove_file(&manifest_path).expect("remove manifest after first boot");
4313        let restored_registry = Arc::new(crate::auth::registry::ConfigRegistry::new());
4314        crate::cli::bootstrap_manifest::rehydrate_manifest_registry(&runtime, &restored_registry)
4315            .expect("registry rehydrates without manifest file");
4316        assert!(restored_registry.get_active("managed-deny-drop").is_some());
4317        assert!(restored_registry.get_active("red.ai").is_some());
4318
4319        let fresh = Arc::new(AuthStore::new(crate::auth::AuthConfig::default()));
4320        apply_preset(&runtime, &fresh).expect("re-run must not need manifest file");
4321        assert!(fresh.needs_bootstrap());
4322
4323        clear_preset_env();
4324    }
4325
4326    #[test]
4327    fn production_preset_refuses_to_start_without_password() {
4328        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
4329        clear_preset_env();
4330        let cert_out = bootstrap_cert_path("production-missing-password");
4331        // SAFETY: env serialised by `no_auth_env_lock`.
4332        unsafe {
4333            std::env::set_var(PRESET_ENV, PRESET_PRODUCTION);
4334            std::env::set_var("REDDB_USERNAME", "ops");
4335            std::env::set_var(BOOTSTRAP_CERT_OUT_ENV, &cert_out);
4336        }
4337
4338        let (runtime, auth_store) = fresh_runtime_and_store();
4339        let err = apply_preset(&runtime, &auth_store).expect_err("must reject missing password");
4340        assert!(
4341            err.contains("REDDB_PASSWORD"),
4342            "error must name the missing env: {err}"
4343        );
4344
4345        // Nothing was persisted; nothing was created.
4346        assert!(auth_store.needs_bootstrap());
4347        assert!(runtime
4348            .db()
4349            .store()
4350            .get_config(BOOTSTRAP_COMPLETED_KEY)
4351            .is_none());
4352        assert!(!cert_out.exists());
4353
4354        clear_preset_env();
4355    }
4356
4357    #[test]
4358    fn re_running_production_after_first_boot_is_a_silent_skip() {
4359        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
4360        clear_preset_env();
4361        let cert_out = bootstrap_cert_path("production-rerun");
4362        // SAFETY: env serialised by `no_auth_env_lock`.
4363        unsafe {
4364            std::env::set_var(PRESET_ENV, PRESET_PRODUCTION);
4365            std::env::set_var("REDDB_USERNAME", "ops");
4366            std::env::set_var("REDDB_PASSWORD", "hunter2");
4367            std::env::set_var(BOOTSTRAP_CERT_OUT_ENV, &cert_out);
4368        }
4369
4370        let (runtime, auth_store) = fresh_runtime_and_store();
4371        apply_preset(&runtime, &auth_store).expect("first apply");
4372        assert_eq!(auth_store.list_users().len(), 1);
4373
4374        // Second invocation: same runtime/store, same env. Must be a
4375        // no-op — no error, no duplicate admin, no duplicate policy.
4376        // We do NOT reuse `auth_store` because production sealed it; the
4377        // idempotency hinge is the persisted config key, not the auth
4378        // store's in-memory seal. Build a fresh `AuthStore` as a restart
4379        // would and confirm `apply_preset` is a silent skip.
4380        let fresh = Arc::new(AuthStore::new(crate::auth::AuthConfig::default()));
4381        apply_preset(&runtime, &fresh).expect("re-run is silent-skip");
4382        assert!(
4383            fresh.needs_bootstrap(),
4384            "re-run must not create a second admin"
4385        );
4386        assert!(
4387            fresh.get_policy(FIRST_ADMIN_ALLOW_ALL_POLICY).is_none(),
4388            "re-run must not re-install the allow-all policy on the fresh store"
4389        );
4390
4391        clear_preset_env();
4392        let _ = std::fs::remove_file(cert_out);
4393    }
4394
4395    #[test]
4396    fn unrecognised_preset_value_is_rejected() {
4397        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
4398        clear_preset_env();
4399        // SAFETY: env serialised by `no_auth_env_lock`.
4400        unsafe {
4401            std::env::set_var(PRESET_ENV, "weird");
4402        }
4403
4404        let (runtime, auth_store) = fresh_runtime_and_store();
4405        let err = apply_preset(&runtime, &auth_store).expect_err("must reject unknown preset");
4406        assert!(err.contains("weird"), "error must echo the value: {err}");
4407        assert!(auth_store.needs_bootstrap());
4408
4409        clear_preset_env();
4410    }
4411
4412    #[test]
4413    fn no_auth_short_circuits_preset_entirely() {
4414        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
4415        clear_preset_env();
4416        // SAFETY: env serialised by `no_auth_env_lock`. Production creds
4417        // are set but `--no-auth` must win — no admin, no bootstrap state.
4418        unsafe {
4419            std::env::set_var(PRESET_ENV, PRESET_PRODUCTION);
4420            std::env::set_var("REDDB_USERNAME", "ops");
4421            std::env::set_var("REDDB_PASSWORD", "hunter2");
4422        }
4423
4424        let options = no_auth_test_config(true)
4425            .to_db_options()
4426            .expect("to_db_options");
4427        assert!(no_auth_active(&options));
4428
4429        // Mirror `build_runtime_with_telemetry`: when no_auth_active,
4430        // `apply_preset` is never called.
4431        let (runtime, auth_store) = fresh_runtime_and_store();
4432        if !no_auth_active(&options) {
4433            apply_preset(&runtime, &auth_store).expect("would apply preset");
4434        }
4435
4436        assert!(
4437            auth_store.needs_bootstrap(),
4438            "--no-auth must prevent any admin creation"
4439        );
4440        assert!(
4441            runtime
4442                .db()
4443                .store()
4444                .get_config(BOOTSTRAP_COMPLETED_KEY)
4445                .is_none(),
4446            "--no-auth must skip bootstrap-state persistence"
4447        );
4448
4449        clear_preset_env();
4450    }
4451
4452    #[test]
4453    fn implicit_bind_collision_degrades() {
4454        let held = TcpListener::bind("127.0.0.1:0").expect("hold test port");
4455        let addr = held.local_addr().expect("held addr").to_string();
4456        let mut readiness = TransportReadiness::default();
4457
4458        let listener =
4459            bind_listener_for_startup(&mut readiness, "http", &addr, false).expect("nonfatal");
4460
4461        assert!(listener.is_none());
4462        assert_eq!(readiness.active.len(), 0);
4463        assert_eq!(readiness.failed.len(), 1);
4464        assert!(!readiness.failed[0].explicit);
4465        assert_eq!(readiness.failed[0].bind_addr, addr);
4466    }
4467}