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