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 `red.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 runtime = RedDBRuntime::with_options(db_options.clone()).map_err(|err| {
1784        // Issue #205 — runtime construction failure is the canonical
1785        // StartupFailed phase. The audit sink isn't installed yet
1786        // (it would have been registered inside `with_options`), so
1787        // the emit falls through to tracing+eprintln only — operator
1788        // still sees it on stderr.
1789        let msg = err.to_string();
1790        crate::telemetry::operator_event::OperatorEvent::StartupFailed {
1791            phase: "runtime_construction".to_string(),
1792            error: msg.clone(),
1793        }
1794        .emit_global();
1795        msg
1796    })?;
1797
1798    // PLAN.md Phase 5 / W6 — opt into serverless writer-lease fencing
1799    // when `RED_LEASE_REQUIRED=true`. Failure here aborts boot: the
1800    // operator asked for a fence; running unfenced would silently
1801    // expose split-brain risk.
1802    crate::runtime::lease_loop::start_lease_loop_if_required(&runtime).map_err(|err| {
1803        let msg = err.to_string();
1804        crate::telemetry::operator_event::OperatorEvent::StartupFailed {
1805            phase: "lease_loop".to_string(),
1806            error: msg.clone(),
1807        }
1808        .emit_global();
1809        msg
1810    })?;
1811
1812    // #213 — edge-triggered disk-space watchdog. Watches the data
1813    // directory; falls back to polling when fanotify is unavailable
1814    // (non-Linux or unprivileged container).
1815    if let Some(data_path) = db_options.data_path.as_deref() {
1816        let watch_dir = data_path.parent().unwrap_or(data_path);
1817        crate::runtime::disk_space_monitor::DiskSpaceMonitor::new(watch_dir, 90).spawn();
1818    }
1819
1820    // #214 — inotify config hot-reload watcher. Watches the config file
1821    // (REDDB_CONFIG_FILE or /etc/reddb/config.json) for changes and
1822    // applies hot-reloadable keys without restart.
1823    {
1824        let config_path = crate::runtime::config_overlay::config_file_path();
1825        let store = runtime.db().store();
1826        crate::runtime::config_watcher::ConfigWatcher::new(config_path, store).spawn();
1827    }
1828
1829    // Phase 6 logging: merge red_config overrides onto the CLI-built
1830    // telemetry config, then install the global subscriber.
1831    let merged = merge_telemetry_with_config(
1832        cli_telemetry
1833            .unwrap_or_else(|| default_telemetry_for_path(db_options.data_path.as_deref())),
1834        &runtime,
1835    );
1836    let telemetry_guard = crate::telemetry::init(merged);
1837    if db_options.bounded_disk_residency {
1838        tracing::info!(
1839            target: "reddb::storage::residency",
1840            resident_memory_bytes_max = runtime.db().store().config().resident_memory_bytes_max,
1841            "bounded disk residency enabled"
1842        );
1843    }
1844
1845    let no_auth = no_auth_active(&db_options);
1846    let auth_store =
1847        if db_options.auth.vault_enabled {
1848            let pager =
1849                runtime.db().store().pager().cloned().ok_or_else(|| {
1850                    "vault requires a paged database (persistent mode)".to_string()
1851                })?;
1852            let store = AuthStore::with_vault(db_options.auth.clone(), pager)
1853                .map_err(|err| err.to_string())?;
1854            Arc::new(store)
1855        } else {
1856            Arc::new(AuthStore::new(db_options.auth.clone()))
1857        };
1858    auth_store.configure_control_events(
1859        runtime.control_event_ledger(),
1860        runtime.control_event_config(),
1861    );
1862    // ADR 0058 — cluster bootstrap authority fail-closed seam. A
1863    // cluster-shaped boot may only run auth bootstrap when a concrete
1864    // reserved-range owner can be proven; none is implemented yet, so the
1865    // seam rejects credentialled cluster boots and allows only the explicit
1866    // `--no-auth` / `--dev` development carveout. Issue #663 — when
1867    // `--no-auth` is active, deliberately skip the preset machinery so a
1868    // stray `REDDB_USERNAME`+`REDDB_PASSWORD` pair cannot silently create an
1869    // admin, defeating the point of anonymous mode.
1870    //
1871    // Issue #1230 — the durable bootstrap completion marker
1872    // (`system.bootstrap.completed`) is the authority path's record that
1873    // first boot already produced global auth state. Observing it makes a
1874    // restart or duplicate bootstrap attempt idempotent: the seam returns
1875    // `AlreadyComplete` and the caller rehydrates read-only state only,
1876    // recreating no users, reissuing no vault certificate, and reapplying no
1877    // mutable config over operator changes.
1878    let bootstrap_already_completed = runtime
1879        .db()
1880        .store()
1881        .try_get_config(BOOTSTRAP_COMPLETED_KEY)
1882        .map_err(|error| error.to_string())?
1883        .is_some();
1884    ensure_persisted_auth_state(
1885        bootstrap_already_completed,
1886        db_options.auth.require_auth,
1887        auth_store.needs_bootstrap(),
1888    )?;
1889    let disposition = crate::cluster::authorize_cluster_bootstrap(
1890        db_options.storage_profile.deploy_profile,
1891        no_auth,
1892        bootstrap.auth_bootstrap_input(),
1893        bootstrap_already_completed,
1894    )?;
1895    // Issue #1231 — wire cluster vault first boot through the bootstrap
1896    // authority. The authorized disposition selects the vault plan: the owner
1897    // path (`ProceedLocal`) opens the vault against the real cluster-global
1898    // auth store (`auth_store`, backed by the runtime pager) and consumes the
1899    // env/`_FILE` secret inputs to mint the certificate; a restart after
1900    // completion (`AlreadyComplete`) reopens and unseals that same store
1901    // without consuming secret inputs. A non-owner cluster boot already failed
1902    // closed above, so no scratch or per-member vault is minted.
1903    let vault_plan = crate::cluster::plan_vault_bootstrap(disposition);
1904    match disposition {
1905        crate::cluster::BootstrapDisposition::SkipDevBypass => {
1906            debug_assert_eq!(vault_plan, crate::cluster::VaultBootstrapPlan::SkipNoVault);
1907            eprintln!("{NO_AUTH_WARNING}");
1908            tracing::warn!("{NO_AUTH_WARNING}");
1909        }
1910        crate::cluster::BootstrapDisposition::AlreadyComplete => {
1911            // First boot is over. Rehydrate the read-only manifest registry
1912            // so config-derived state is observable, but create no users,
1913            // reissue no vault material, and reapply no mutable config
1914            // (issue #1230). `apply_preset_from_config` does exactly this
1915            // when the completion marker is present — matching the unseal-only
1916            // vault plan (no secret inputs consumed).
1917            debug_assert_eq!(
1918                vault_plan,
1919                crate::cluster::VaultBootstrapPlan::OpenClusterGlobalStore {
1920                    consume_secret_inputs: false,
1921                }
1922            );
1923            apply_preset_from_config(&runtime, &auth_store, bootstrap)?;
1924            tracing::info!("bootstrap already completed, unsealing existing cluster auth store");
1925        }
1926        crate::cluster::BootstrapDisposition::ProceedLocal => {
1927            // Owner first boot: open the vault against the real cluster-global
1928            // auth store and consume the secret inputs to mint the certificate.
1929            debug_assert_eq!(
1930                vault_plan,
1931                crate::cluster::VaultBootstrapPlan::OpenClusterGlobalStore {
1932                    consume_secret_inputs: true,
1933                }
1934            );
1935            apply_preset_from_config(&runtime, &auth_store, bootstrap)?;
1936            maybe_apply_policy_break_glass(&auth_store);
1937        }
1938    }
1939
1940    // Background session purge (every 5 minutes)
1941    {
1942        let store = Arc::clone(&auth_store);
1943        std::thread::Builder::new()
1944            .name("reddb-session-purge".into())
1945            .spawn(move || loop {
1946                std::thread::sleep(std::time::Duration::from_secs(300));
1947                store.purge_expired_sessions();
1948            })
1949            .ok();
1950    }
1951
1952    // Native wire listeners obtain their authentication authority from the
1953    // runtime, including deployments without an HTTP or gRPC server object.
1954    runtime.set_auth_store(Arc::clone(&auth_store));
1955    Ok((runtime, auth_store, telemetry_guard))
1956}
1957
1958/// Honour `REDDB_POLICY_BREAK_GLASS=1` at boot — see issue #713 and
1959/// the [`crate::auth::self_lock_guard`] module. Anything other than
1960/// `1`/`true`/`yes` (case-insensitive) is treated as not set.
1961fn maybe_apply_policy_break_glass(auth_store: &Arc<AuthStore>) {
1962    use crate::auth::self_lock_guard::BREAK_GLASS_ENV;
1963
1964    let enabled = std::env::var(BREAK_GLASS_ENV)
1965        .ok()
1966        .map(|v| {
1967            let trimmed = v.trim().to_ascii_lowercase();
1968            matches!(trimmed.as_str(), "1" | "true" | "yes")
1969        })
1970        .unwrap_or(false);
1971    if !enabled {
1972        return;
1973    }
1974    let now = crate::utils::now_unix_millis() as u128;
1975    match auth_store.apply_policy_break_glass(now) {
1976        Ok(()) => {
1977            tracing::warn!(env = BREAK_GLASS_ENV, "policy break-glass recovery applied");
1978        }
1979        Err(err) => {
1980            tracing::error!(env = BREAK_GLASS_ENV, %err, "policy break-glass recovery failed");
1981        }
1982    }
1983}
1984
1985/// Reserved config keys describing first-boot bootstrap state (issue #650).
1986/// Presence of [`BOOTSTRAP_COMPLETED_KEY`] is the idempotency hinge: when
1987/// it is set, [`apply_preset`] silently no-ops on subsequent boots so a
1988/// container restart with the same env is a no-op.
1989pub(crate) const BOOTSTRAP_COMPLETED_KEY: &str = "system.bootstrap.completed";
1990pub(crate) const BOOTSTRAP_PRESET_KEY: &str = "system.bootstrap.preset";
1991pub(crate) const BOOTSTRAP_FIRST_ADMIN_KEY: &str = "system.bootstrap.first_admin_id";
1992
1993/// Env vars selecting the bootstrap preset. `REDDB_BOOTSTRAP_PRESET`
1994/// is canonical; `REDDB_PRESET` remains accepted for compatibility.
1995pub(crate) const BOOTSTRAP_PRESET_ENV: &str = "REDDB_BOOTSTRAP_PRESET";
1996pub(crate) const PRESET_ENV: &str = "REDDB_PRESET";
1997/// Env equivalent of `--bootstrap-cert-out` (issue #1589). Honours the
1998/// `_FILE` companion via [`env_nonempty`].
1999pub(crate) const BOOTSTRAP_CERT_OUT_ENV: &str = "REDDB_BOOTSTRAP_CERT_OUT";
2000pub(crate) const PRESET_SIMPLE: &str = "simple";
2001pub(crate) const PRESET_PRODUCTION: &str = "production";
2002pub(crate) const PRESET_REGULATED: &str = "regulated";
2003pub(crate) const PRESET_CLOUD: &str = "cloud";
2004
2005/// Policy id installed by the `production` preset and attached to the
2006/// first admin. Grants `"*"` on `"*"` so the admin has policy-derived
2007/// broad authority (acceptance #3) — not an authorization bypass.
2008pub(crate) const FIRST_ADMIN_ALLOW_ALL_POLICY: &str = "system.bootstrap.first-admin-allow-all";
2009pub(crate) const REGULATED_PROTECT_MANAGED_POLICY: &str = "system.regulated.protect-managed";
2010pub(crate) const CLOUD_PROTECT_MANAGED_POLICY: &str = "system.cloud.protect-managed";
2011pub(crate) const CLOUD_CONFIG_NAMESPACE: &str = "red.config.cloud";
2012pub(crate) const REGULATED_AUDIT_CONFIG_NAMESPACE: &str = "red.config.audit";
2013pub(crate) const REGULATED_EVIDENCE_CONFIG_NAMESPACE: &str = "red.config.evidence";
2014pub(crate) const REGULATED_QUERY_AUDIT_CONFIG_NAMESPACE: &str = "red.config.query_audit";
2015
2016/// Apply the bootstrap preset selected by `REDDB_BOOTSTRAP_PRESET` (or
2017/// legacy `REDDB_PRESET`; default `simple`). Idempotent — if
2018/// `system.bootstrap.completed` is already set, this is a one-line
2019/// `tracing::info!` no-op and the server proceeds (issue #650
2020/// acceptance #5).
2021///
2022/// Caller must have already short-circuited the `--no-auth` / `--dev`
2023/// path (issue #663): when that flag is set, the preset must be skipped
2024/// entirely — no admin created, no bootstrap state written.
2025pub(crate) fn apply_preset(
2026    runtime: &RedDBRuntime,
2027    auth_store: &Arc<AuthStore>,
2028) -> Result<(), String> {
2029    let bootstrap = BootstrapConfig::default();
2030    apply_preset_from_config(runtime, auth_store, &bootstrap)
2031}
2032
2033fn apply_preset_from_config(
2034    runtime: &RedDBRuntime,
2035    auth_store: &Arc<AuthStore>,
2036    bootstrap: &BootstrapConfig,
2037) -> Result<(), String> {
2038    let store = runtime.db().store();
2039
2040    if store
2041        .try_get_config(BOOTSTRAP_COMPLETED_KEY)
2042        .map_err(|error| error.to_string())?
2043        .is_some()
2044    {
2045        crate::cli::bootstrap_manifest::rehydrate_manifest_registry(
2046            runtime,
2047            &runtime.config_registry(),
2048        )?;
2049        tracing::info!("bootstrap state present, skipping preset application");
2050        return Ok(());
2051    }
2052
2053    let preset = bootstrap.resolved_preset();
2054
2055    if let Some(path) = bootstrap.selected_manifest() {
2056        if let Some(manifest_bootstrap) =
2057            crate::cli::bootstrap_manifest::bootstrap_config_from_manifest_file(&path)?
2058        {
2059            apply_preset_from_config(runtime, auth_store, &manifest_bootstrap)?;
2060            return Ok(());
2061        }
2062        let first_admin_id = crate::cli::bootstrap_manifest::apply_manifest_file(
2063            runtime,
2064            auth_store,
2065            &runtime.config_registry(),
2066            &path,
2067        )?;
2068        persist_bootstrap_state(runtime, "manifest", Some(&first_admin_id))?;
2069        tracing::info!("bootstrap manifest applied");
2070        return Ok(());
2071    }
2072
2073    // The encrypted auth state is independently durable. If it already says
2074    // bootstrap completed, never reopen preset bootstrap merely because the
2075    // adjacent config marker was not flushed before an abrupt process exit.
2076    if !auth_store.needs_bootstrap() {
2077        persist_bootstrap_state(runtime, &preset, None)?;
2078        tracing::info!(preset = %preset, "durable auth state present, skipping preset bootstrap");
2079        return Ok(());
2080    }
2081
2082    // The simple preset does not create an administrator. With required auth,
2083    // leave the completion marker absent until a durable vault user exists so
2084    // a crash before the first HTTP/gRPC bootstrap does not turn into a
2085    // permanent lockout. Older volatile-auth installs that already carry the
2086    // marker still fail closed in `ensure_persisted_auth_state` above.
2087    if preset == PRESET_SIMPLE && auth_store.config().require_auth && auth_store.needs_bootstrap() {
2088        tracing::info!("required-auth simple preset is awaiting durable first-admin bootstrap");
2089        return Ok(());
2090    }
2091
2092    let mut certificate_output = if matches!(preset.as_str(), PRESET_PRODUCTION | PRESET_CLOUD)
2093        && auth_store.bootstrap_requires_certificate_delivery()
2094    {
2095        require_bootstrap_certificate_output(bootstrap, &preset)?;
2096        Some(open_bootstrap_certificate_output(bootstrap)?)
2097    } else {
2098        None
2099    };
2100    let first_admin_id = match preset.as_str() {
2101        PRESET_SIMPLE => {
2102            // `simple` is the default low-friction preset. Auth knobs
2103            // remain whatever the CLI/env set; we only persist the
2104            // bootstrap state so subsequent boots are idempotent.
2105            None
2106        }
2107        PRESET_PRODUCTION => Some(apply_production_preset_from_config(
2108            auth_store,
2109            bootstrap,
2110            &mut certificate_output,
2111        )?),
2112        PRESET_CLOUD => Some(apply_cloud_preset(
2113            runtime,
2114            auth_store,
2115            bootstrap,
2116            &mut certificate_output,
2117        )?),
2118        PRESET_REGULATED => {
2119            apply_regulated_preset(runtime, auth_store)?;
2120            None
2121        }
2122        other => {
2123            return Err(format!(
2124                "bootstrap preset {other:?} is not recognised (expected `simple`, `production`, `regulated`, or `cloud`)"
2125            ));
2126        }
2127    };
2128
2129    // Every preset mutation must be durable before the adjacent completion
2130    // marker can make bootstrap permanently ineligible on a later restart.
2131    auth_store
2132        .persist_to_vault_result()
2133        .map_err(|err| format!("persist bootstrap auth state: {err}"))?;
2134    persist_bootstrap_state(runtime, &preset, first_admin_id.as_deref())?;
2135    tracing::info!(preset = %preset, "bootstrap preset applied");
2136    Ok(())
2137}
2138
2139struct BootstrapCertificateOutput {
2140    path: PathBuf,
2141    file: Option<std::fs::File>,
2142    committed: bool,
2143}
2144
2145impl Drop for BootstrapCertificateOutput {
2146    fn drop(&mut self) {
2147        if self.committed {
2148            return;
2149        }
2150        self.file.take();
2151        let _ = std::fs::remove_file(&self.path);
2152    }
2153}
2154
2155/// Reserve the secure certificate destination before mutating bootstrap state.
2156/// A failed preset application removes the empty reservation on drop.
2157fn open_bootstrap_certificate_output(
2158    bootstrap: &BootstrapConfig,
2159) -> Result<BootstrapCertificateOutput, String> {
2160    let Some(path) = bootstrap.selected_cert_out() else {
2161        return Err("bootstrap certificate output is not configured".to_string());
2162    };
2163    let mut options = std::fs::OpenOptions::new();
2164    options.write(true).create_new(true);
2165    #[cfg(unix)]
2166    {
2167        use std::os::unix::fs::OpenOptionsExt;
2168        options.mode(0o600);
2169    }
2170    let file = options
2171        .open(&path)
2172        .map_err(|err| format!("create --bootstrap-cert-out {}: {err}", path.display()))?;
2173    Ok(BootstrapCertificateOutput {
2174        path,
2175        file: Some(file),
2176        committed: false,
2177    })
2178}
2179
2180fn stage_bootstrap_certificate(
2181    output: &mut BootstrapCertificateOutput,
2182    certificate_hex: &str,
2183) -> Result<(), String> {
2184    use std::io::Write;
2185    let path = output.path.clone();
2186    let file = output
2187        .file
2188        .as_mut()
2189        .ok_or_else(|| "bootstrap certificate output is closed".to_string())?;
2190    writeln!(file, "{certificate_hex}")
2191        .map_err(|err| format!("write --bootstrap-cert-out {}: {err}", path.display()))?;
2192    file.sync_all()
2193        .map_err(|err| format!("sync --bootstrap-cert-out {}: {err}", path.display()))?;
2194    Ok(())
2195}
2196
2197fn commit_bootstrap_certificate(mut output: BootstrapCertificateOutput) {
2198    let path = output.path.clone();
2199    output.committed = true;
2200    tracing::info!(
2201        target: "reddb::bootstrap",
2202        path = %path.display(),
2203        "first-boot self-bootstrap: wrote unseal certificate to --bootstrap-cert-out (consume via REDDB_CERTIFICATE_FILE on the next boot)"
2204    );
2205}
2206
2207fn bootstrap_with_certificate_output(
2208    auth_store: &Arc<AuthStore>,
2209    username: &str,
2210    password: &str,
2211    certificate_output: &mut Option<BootstrapCertificateOutput>,
2212) -> Result<crate::auth::store::BootstrapResult, String> {
2213    let mut certificate_staged = false;
2214    let result = auth_store
2215        .bootstrap_with_certificate_delivery(username, password, |certificate| {
2216            let output = certificate_output.as_mut().ok_or_else(|| {
2217                crate::auth::AuthError::Internal(
2218                    "bootstrap certificate output was not prepared".to_string(),
2219                )
2220            })?;
2221            stage_bootstrap_certificate(output, certificate)
2222                .map_err(crate::auth::AuthError::Internal)?;
2223            certificate_staged = true;
2224            Ok(())
2225        })
2226        .map_err(|err| format!("bootstrap first admin: {err}"))?;
2227
2228    // When the store was opened with an operator-supplied certificate there
2229    // is no newly generated seal to deliver before activation. Mirror that
2230    // existing credential into the requested file after bootstrap.
2231    if !certificate_staged {
2232        if let (Some(certificate), Some(output)) =
2233            (result.certificate.as_deref(), certificate_output.as_mut())
2234        {
2235            stage_bootstrap_certificate(output, certificate)?;
2236            certificate_staged = true;
2237        }
2238    }
2239    if certificate_staged {
2240        let output = certificate_output
2241            .take()
2242            .ok_or_else(|| "bootstrap certificate output was not prepared".to_string())?;
2243        commit_bootstrap_certificate(output);
2244    }
2245    Ok(result)
2246}
2247
2248fn ensure_persisted_auth_state(
2249    bootstrap_completed: bool,
2250    require_auth: bool,
2251    needs_bootstrap: bool,
2252) -> Result<(), String> {
2253    if bootstrap_completed && require_auth && needs_bootstrap {
2254        return Err(
2255            "persistent database is marked bootstrapped but its authentication state is empty; refusing to reopen public bootstrap"
2256                .to_string(),
2257        );
2258    }
2259    Ok(())
2260}
2261
2262fn require_bootstrap_certificate_output(
2263    bootstrap: &BootstrapConfig,
2264    preset: &str,
2265) -> Result<(), String> {
2266    if bootstrap.selected_cert_out().is_some() {
2267        return Ok(());
2268    }
2269    Err(format!(
2270        "bootstrap preset `{preset}` requires --bootstrap-cert-out (or REDDB_BOOTSTRAP_CERT_OUT); vault certificates are never written to logs"
2271    ))
2272}
2273
2274fn apply_production_preset_from_config(
2275    auth_store: &Arc<AuthStore>,
2276    bootstrap: &BootstrapConfig,
2277    certificate_output: &mut Option<BootstrapCertificateOutput>,
2278) -> Result<String, String> {
2279    use crate::auth::store::PrincipalRef;
2280    use crate::auth::UserId;
2281
2282    let username = bootstrap.production_username().ok_or_else(|| {
2283        "production preset requires --bootstrap-admin or REDDB_USERNAME (or REDDB_USERNAME_FILE)"
2284            .to_string()
2285    })?;
2286    let password = bootstrap.production_password().ok_or_else(|| {
2287        "production preset requires --bootstrap-admin-password or REDDB_PASSWORD (or REDDB_PASSWORD_FILE)"
2288            .to_string()
2289    })?;
2290
2291    // (1) Create the first admin as an ordinary platform-scoped user.
2292    // Managed guardrails are policy-derived; bootstrap does not stamp a
2293    // parallel ownership flag.
2294    let result =
2295        bootstrap_with_certificate_output(auth_store, &username, &password, certificate_output)?;
2296    if result.certificate.is_some() {
2297        tracing::warn!(
2298            "vault certificate issued by REDDB_PRESET=production and written to the configured certificate output"
2299        );
2300    }
2301    let first_admin = UserId::platform(result.user.username.clone());
2302
2303    // (2) Install the allow-all policy as an ordinary policy row.
2304    install_allow_all_policy(auth_store)?;
2305
2306    // (3) Attach the policy to the first admin.
2307    auth_store
2308        .attach_policy(
2309            PrincipalRef::User(first_admin.clone()),
2310            FIRST_ADMIN_ALLOW_ALL_POLICY,
2311        )
2312        .map_err(|err| format!("attach allow-all policy: {err}"))?;
2313
2314    Ok(first_admin.to_string())
2315}
2316
2317fn apply_cloud_preset(
2318    runtime: &RedDBRuntime,
2319    auth_store: &Arc<AuthStore>,
2320    bootstrap: &BootstrapConfig,
2321    certificate_output: &mut Option<BootstrapCertificateOutput>,
2322) -> Result<String, String> {
2323    use crate::auth::store::PrincipalRef;
2324    use crate::auth::{Role, UserId};
2325
2326    let head_username = bootstrap.cloud_head_username().ok_or_else(|| {
2327        "cloud preset requires --cloud-head-admin or REDDB_CLOUD_HEAD_ADMIN (or REDDB_USERNAME)"
2328            .to_string()
2329    })?;
2330    let head_password = bootstrap.cloud_head_password().ok_or_else(|| {
2331        "cloud preset requires --cloud-head-admin-password or REDDB_CLOUD_HEAD_ADMIN_PASSWORD (or REDDB_PASSWORD)"
2332            .to_string()
2333    })?;
2334    let customer_username = bootstrap.customer_username().ok_or_else(|| {
2335        "cloud preset requires --customer-admin or REDDB_CUSTOMER_ADMIN".to_string()
2336    })?;
2337    let customer_password = bootstrap.customer_password().ok_or_else(|| {
2338        "cloud preset requires --customer-admin-password or REDDB_CUSTOMER_ADMIN_PASSWORD"
2339            .to_string()
2340    })?;
2341    if head_username == customer_username {
2342        return Err("cloud preset requires distinct head and customer admin usernames".to_string());
2343    }
2344
2345    let head = bootstrap_with_certificate_output(
2346        auth_store,
2347        &head_username,
2348        &head_password,
2349        certificate_output,
2350    )
2351    .map_err(|err| format!("bootstrap cloud head admin: {err}"))?;
2352    let head_id = UserId::platform(head.user.username.clone());
2353
2354    auth_store
2355        .create_user(&customer_username, &customer_password, Role::Admin)
2356        .map_err(|err| format!("create cloud customer admin: {err}"))?;
2357    let customer_id = UserId::platform(customer_username.clone());
2358
2359    install_allow_all_policy(auth_store)?;
2360    for user_id in [&head_id, &customer_id] {
2361        auth_store
2362            .attach_policy(
2363                PrincipalRef::User(user_id.clone()),
2364                FIRST_ADMIN_ALLOW_ALL_POLICY,
2365            )
2366            .map_err(|err| format!("attach allow-all policy: {err}"))?;
2367    }
2368    install_cloud_guardrails(runtime, auth_store, &head_id)?;
2369    auth_store
2370        .attach_policy(
2371            PrincipalRef::User(customer_id),
2372            CLOUD_PROTECT_MANAGED_POLICY,
2373        )
2374        .map_err(|err| format!("attach cloud guardrail policy: {err}"))?;
2375
2376    Ok(head_id.to_string())
2377}
2378
2379pub(crate) fn install_allow_all_policy(auth_store: &AuthStore) -> Result<(), String> {
2380    use crate::auth::policies::Policy;
2381
2382    let policy = Policy::from_json_str(&format!(
2383        r#"{{
2384            "id": "{id}",
2385            "version": 1,
2386            "statements": [{{
2387                "effect": "allow",
2388                "actions": ["*"],
2389                "resources": ["*"]
2390            }}]
2391        }}"#,
2392        id = FIRST_ADMIN_ALLOW_ALL_POLICY
2393    ))
2394    .map_err(|err| format!("compile allow-all policy: {err}"))?;
2395    auth_store
2396        .put_policy(policy)
2397        .map_err(|err| format!("install allow-all policy: {err}"))
2398}
2399
2400fn install_cloud_guardrails(
2401    runtime: &RedDBRuntime,
2402    auth_store: &Arc<AuthStore>,
2403    head_admin: &crate::auth::UserId,
2404) -> Result<(), String> {
2405    use crate::auth::policies::Policy;
2406    use crate::auth::registry::EvidenceRequirement;
2407
2408    let policy = Policy::from_json_str(&format!(
2409        r#"{{
2410            "id": "{id}",
2411            "version": 1,
2412            "statements": [
2413                {{
2414                    "effect": "deny",
2415                    "actions": ["policy:put", "policy:drop", "policy:attach", "policy:detach"],
2416                    "resources": ["policy:{id}"]
2417                }},
2418                {{
2419                    "effect": "deny",
2420                    "actions": [
2421                        "user:delete",
2422                        "user:disable",
2423                        "user:password:change",
2424                        "user:role:update"
2425                    ],
2426                    "resources": ["user:{head_admin}"]
2427                }},
2428                {{
2429                    "effect": "deny",
2430                    "actions": ["config:write"],
2431                    "resources": ["config:{cloud}.*"]
2432                }}
2433            ]
2434        }}"#,
2435        id = CLOUD_PROTECT_MANAGED_POLICY,
2436        head_admin = head_admin,
2437        cloud = CLOUD_CONFIG_NAMESPACE,
2438    ))
2439    .map_err(|err| format!("compile cloud guardrail policy: {err}"))?;
2440    auth_store
2441        .put_policy(policy)
2442        .map_err(|err| format!("install cloud guardrail policy: {err}"))?;
2443
2444    let now_ms = crate::utils::now_unix_millis() as u128;
2445    let entries = vec![
2446        regulated_registry_entry(
2447            CLOUD_PROTECT_MANAGED_POLICY,
2448            crate::auth::managed_policy::RESOURCE_TYPE_POLICY,
2449            "policy",
2450            "policy:*",
2451            &format!("policy:{CLOUD_PROTECT_MANAGED_POLICY}"),
2452            EvidenceRequirement::Metadata,
2453            now_ms,
2454        ),
2455        regulated_registry_entry(
2456            CLOUD_CONFIG_NAMESPACE,
2457            crate::auth::managed_config::RESOURCE_TYPE_CONFIG_NAMESPACE,
2458            "config_namespace",
2459            "config:write",
2460            &format!("config:{CLOUD_CONFIG_NAMESPACE}.*"),
2461            EvidenceRequirement::Metadata,
2462            now_ms,
2463        ),
2464    ];
2465    for entry in entries.iter().cloned() {
2466        runtime
2467            .config_registry()
2468            .restore_bootstrap_entry(entry)
2469            .map_err(|err| format!("install cloud registry entry: {err}"))?;
2470    }
2471    crate::cli::bootstrap_manifest::persist_registry_state(runtime, &entries)?;
2472    Ok(())
2473}
2474
2475fn apply_regulated_preset(
2476    runtime: &RedDBRuntime,
2477    auth_store: &Arc<AuthStore>,
2478) -> Result<(), String> {
2479    use crate::auth::policies::Policy;
2480    use crate::auth::registry::EvidenceRequirement;
2481    use crate::auth::store::{PrincipalRef, PUBLIC_IAM_GROUP};
2482
2483    runtime.query_audit().enable_infrastructure();
2484
2485    let policy = Policy::from_json_str(&format!(
2486        r#"{{
2487            "id": "{id}",
2488            "version": 1,
2489            "statements": [
2490                {{
2491                    "effect": "deny",
2492                    "actions": ["policy:put", "policy:drop", "policy:attach", "policy:detach"],
2493                    "resources": ["policy:{id}"]
2494                }},
2495                {{
2496                    "effect": "deny",
2497                    "actions": ["config:write"],
2498                    "resources": [
2499                        "config:{audit}.*",
2500                        "config:{evidence}.*",
2501                        "config:{query_audit}.*"
2502                    ]
2503                }}
2504            ]
2505        }}"#,
2506        id = REGULATED_PROTECT_MANAGED_POLICY,
2507        audit = REGULATED_AUDIT_CONFIG_NAMESPACE,
2508        evidence = REGULATED_EVIDENCE_CONFIG_NAMESPACE,
2509        query_audit = REGULATED_QUERY_AUDIT_CONFIG_NAMESPACE,
2510    ))
2511    .map_err(|err| format!("compile regulated guardrail policy: {err}"))?;
2512    auth_store
2513        .put_policy(policy)
2514        .map_err(|err| format!("install regulated guardrail policy: {err}"))?;
2515    auth_store
2516        .attach_policy(
2517            PrincipalRef::Group(PUBLIC_IAM_GROUP.to_string()),
2518            REGULATED_PROTECT_MANAGED_POLICY,
2519        )
2520        .map_err(|err| format!("attach regulated guardrail policy: {err}"))?;
2521
2522    let now_ms = crate::utils::now_unix_millis() as u128;
2523    let entries = vec![
2524        regulated_registry_entry(
2525            REGULATED_PROTECT_MANAGED_POLICY,
2526            crate::auth::managed_policy::RESOURCE_TYPE_POLICY,
2527            "iam_policy",
2528            "policy:*",
2529            &format!("policy:{REGULATED_PROTECT_MANAGED_POLICY}"),
2530            EvidenceRequirement::Metadata,
2531            now_ms,
2532        ),
2533        regulated_registry_entry(
2534            REGULATED_AUDIT_CONFIG_NAMESPACE,
2535            crate::auth::managed_config::RESOURCE_TYPE_CONFIG_NAMESPACE,
2536            "config_namespace",
2537            "config:write",
2538            &format!("config:{REGULATED_AUDIT_CONFIG_NAMESPACE}.*"),
2539            EvidenceRequirement::Metadata,
2540            now_ms,
2541        ),
2542        regulated_registry_entry(
2543            REGULATED_EVIDENCE_CONFIG_NAMESPACE,
2544            crate::auth::managed_config::RESOURCE_TYPE_CONFIG_NAMESPACE,
2545            "config_namespace",
2546            "config:write",
2547            &format!("config:{REGULATED_EVIDENCE_CONFIG_NAMESPACE}.*"),
2548            EvidenceRequirement::Metadata,
2549            now_ms,
2550        ),
2551        regulated_registry_entry(
2552            REGULATED_QUERY_AUDIT_CONFIG_NAMESPACE,
2553            crate::auth::managed_config::RESOURCE_TYPE_CONFIG_NAMESPACE,
2554            "config_namespace",
2555            "config:write",
2556            &format!("config:{REGULATED_QUERY_AUDIT_CONFIG_NAMESPACE}.*"),
2557            EvidenceRequirement::Metadata,
2558            now_ms,
2559        ),
2560    ];
2561
2562    for entry in entries.iter().cloned() {
2563        runtime
2564            .config_registry()
2565            .restore_bootstrap_entry(entry)
2566            .map_err(|err| format!("install regulated registry entry: {err}"))?;
2567    }
2568    crate::cli::bootstrap_manifest::persist_registry_state(runtime, &entries)?;
2569    Ok(())
2570}
2571
2572fn regulated_registry_entry(
2573    id: &str,
2574    resource_type: &str,
2575    schema: &str,
2576    required_action: &str,
2577    required_resource: &str,
2578    evidence_requirement: crate::auth::registry::EvidenceRequirement,
2579    updated_at_ms: u128,
2580) -> crate::auth::registry::ConfigRegistryEntry {
2581    crate::auth::registry::ConfigRegistryEntry {
2582        id: id.to_string(),
2583        version: 1,
2584        resource_type: resource_type.to_string(),
2585        schema: schema.to_string(),
2586        mutability: crate::auth::registry::Mutability::Immutable,
2587        sensitivity: crate::auth::registry::Sensitivity::Internal,
2588        managed: true,
2589        required_action: required_action.to_string(),
2590        required_resource: required_resource.to_string(),
2591        evidence_requirement,
2592        updated_by: "system:regulated-preset".to_string(),
2593        updated_at_ms,
2594    }
2595}
2596
2597fn persist_bootstrap_state(
2598    runtime: &RedDBRuntime,
2599    preset: &str,
2600    first_admin_id: Option<&str>,
2601) -> Result<(), String> {
2602    let store = runtime.db().store();
2603    let mut tree = crate::serde_json::Map::new();
2604    tree.insert(
2605        BOOTSTRAP_COMPLETED_KEY.to_string(),
2606        crate::serde_json::Value::Bool(true),
2607    );
2608    tree.insert(
2609        BOOTSTRAP_PRESET_KEY.to_string(),
2610        crate::serde_json::Value::String(preset.to_string()),
2611    );
2612    if let Some(id) = first_admin_id {
2613        tree.insert(
2614            BOOTSTRAP_FIRST_ADMIN_KEY.to_string(),
2615            crate::serde_json::Value::String(id.to_string()),
2616        );
2617    }
2618    let json = crate::serde_json::Value::Object(tree);
2619    store
2620        .try_set_config_tree("", &json)
2621        .map_err(|error| error.to_string())?;
2622    Ok(())
2623}
2624
2625/// Read `red.logging.*` keys from the persistent config store and
2626/// merge them into the CLI-built `TelemetryConfig`. Merge priority:
2627/// explicit CLI flag > red_config > built-in default.
2628///
2629/// The "was a flag passed" signal comes from the `*_explicit` bools
2630/// on `TelemetryConfig`, populated by the CLI parser. This replaces
2631/// an earlier equality-to-default heuristic that silently dropped
2632/// config whenever the CLI-derived default diverged from
2633/// `TelemetryConfig::default()` (e.g. path-derived `log_dir`,
2634/// non-TTY `format`) and that silently overrode `--no-log-file`.
2635fn merge_telemetry_with_config(
2636    mut cli: crate::telemetry::TelemetryConfig,
2637    runtime: &RedDBRuntime,
2638) -> crate::telemetry::TelemetryConfig {
2639    use reddb_types::Value;
2640
2641    let store = runtime.db().store();
2642
2643    if !cli.level_explicit {
2644        if let Some(Value::Text(v)) = store.get_config("red.logging.level") {
2645            cli.level_filter = v.to_string();
2646        }
2647    }
2648    if !cli.format_explicit {
2649        if let Some(Value::Text(v)) = store.get_config("red.logging.format") {
2650            if let Some(parsed) = crate::telemetry::LogFormat::parse(&v) {
2651                cli.format = parsed;
2652            }
2653        }
2654    }
2655    if !cli.rotation_keep_days_explicit {
2656        match store.get_config("red.logging.keep_days") {
2657            Some(Value::Integer(n)) if n >= 0 && n <= u16::MAX as i64 => {
2658                cli.rotation_keep_days = n as u16
2659            }
2660            Some(Value::UnsignedInteger(n)) if n <= u16::MAX as u64 => {
2661                cli.rotation_keep_days = n as u16
2662            }
2663            Some(Value::Text(v)) => {
2664                if let Ok(n) = v.parse::<u16>() {
2665                    cli.rotation_keep_days = n;
2666                }
2667            }
2668            _ => {}
2669        }
2670    }
2671    if !cli.file_prefix_explicit {
2672        if let Some(Value::Text(v)) = store.get_config("red.logging.file_prefix") {
2673            if !v.is_empty() {
2674                cli.file_prefix = v.to_string();
2675            }
2676        }
2677    }
2678    // --no-log-file is a kill-switch: config cannot resurrect the
2679    // file sink. Explicit --log-dir also wins.
2680    if !cli.log_dir_explicit && !cli.log_file_disabled {
2681        if let Some(Value::Text(v)) = store.get_config("red.logging.dir") {
2682            if !v.is_empty() {
2683                cli.log_dir = Some(std::path::PathBuf::from(v.as_ref()));
2684            }
2685        }
2686    }
2687
2688    cli
2689}
2690
2691#[cfg(test)]
2692mod telemetry_merge_tests {
2693    use super::*;
2694    use crate::telemetry::{LogFormat, TelemetryConfig};
2695
2696    fn fresh_runtime() -> RedDBRuntime {
2697        RedDBRuntime::with_options(RedDBOptions::in_memory()).expect("runtime")
2698    }
2699
2700    fn set_str(runtime: &RedDBRuntime, key: &str, value: &str) {
2701        runtime
2702            .db()
2703            .store()
2704            .set_config_tree(key, &crate::serde_json::Value::String(value.to_string()));
2705    }
2706
2707    fn cli_base() -> TelemetryConfig {
2708        // Emulate default_telemetry_for_path(Some(path)) on a non-TTY host:
2709        // log_dir = Some(...), format = Json. Nothing marked explicit.
2710        TelemetryConfig {
2711            log_dir: Some(std::path::PathBuf::from("/tmp/reddb-default/logs")),
2712            format: LogFormat::Json,
2713            ..Default::default()
2714        }
2715    }
2716
2717    #[test]
2718    fn config_log_dir_promoted_when_flag_absent() {
2719        let runtime = fresh_runtime();
2720        set_str(&runtime, "red.logging.dir", "/var/log/reddb");
2721        let merged = merge_telemetry_with_config(cli_base(), &runtime);
2722        assert_eq!(
2723            merged.log_dir.as_deref(),
2724            Some(std::path::Path::new("/var/log/reddb"))
2725        );
2726    }
2727
2728    #[test]
2729    fn explicit_log_dir_wins_over_config() {
2730        let runtime = fresh_runtime();
2731        set_str(&runtime, "red.logging.dir", "/var/log/reddb");
2732        let mut cli = cli_base();
2733        cli.log_dir = Some(std::path::PathBuf::from("/custom/dir"));
2734        cli.log_dir_explicit = true;
2735        let merged = merge_telemetry_with_config(cli, &runtime);
2736        assert_eq!(
2737            merged.log_dir.as_deref(),
2738            Some(std::path::Path::new("/custom/dir"))
2739        );
2740    }
2741
2742    #[test]
2743    fn no_log_file_beats_config_log_dir() {
2744        let runtime = fresh_runtime();
2745        set_str(&runtime, "red.logging.dir", "/var/log/reddb");
2746        let mut cli = cli_base();
2747        cli.log_dir = None;
2748        cli.log_file_disabled = true;
2749        let merged = merge_telemetry_with_config(cli, &runtime);
2750        assert!(
2751            merged.log_dir.is_none(),
2752            "--no-log-file must veto config dir"
2753        );
2754    }
2755
2756    #[test]
2757    fn config_format_promoted_on_non_tty_default() {
2758        // On non-TTY, default_telemetry_for_path yields format=Json even
2759        // though TelemetryConfig::default() is Pretty. The old equality
2760        // check silently dropped config here.
2761        let runtime = fresh_runtime();
2762        set_str(&runtime, "red.logging.format", "pretty");
2763        let merged = merge_telemetry_with_config(cli_base(), &runtime);
2764        assert_eq!(merged.format, LogFormat::Pretty);
2765    }
2766
2767    #[test]
2768    fn explicit_format_wins_over_config() {
2769        let runtime = fresh_runtime();
2770        set_str(&runtime, "red.logging.format", "pretty");
2771        let mut cli = cli_base();
2772        cli.format = LogFormat::Json;
2773        cli.format_explicit = true;
2774        let merged = merge_telemetry_with_config(cli, &runtime);
2775        assert_eq!(merged.format, LogFormat::Json);
2776    }
2777}
2778
2779#[inline(never)]
2780fn build_http_server(
2781    runtime: RedDBRuntime,
2782    auth_store: Arc<AuthStore>,
2783    bind_addr: String,
2784) -> RedDBServer {
2785    build_http_server_with_transport_readiness(
2786        runtime,
2787        auth_store,
2788        bind_addr,
2789        TransportReadiness::default(),
2790    )
2791}
2792
2793/// Apply the resolved HTTP limits to a freshly-built `RedDBServer`.
2794///
2795/// Centralised here so every `run_*` path goes through the same
2796/// resolver and the structured startup log line carries the same
2797/// `http_limits.*` fields regardless of transport combination.
2798fn apply_http_limits(
2799    server: RedDBServer,
2800    config: &ServerCommandConfig,
2801    runtime: &RedDBRuntime,
2802) -> RedDBServer {
2803    let store = runtime.db().store();
2804    let resolved =
2805        crate::server::http_limits::resolve_http_limits(&config.http_limits_cli, |key| match store
2806            .get_config(key)
2807        {
2808            Some(reddb_types::Value::Text(v)) => Some(v.to_string()),
2809            Some(reddb_types::Value::Integer(n)) if n >= 0 => Some(n.to_string()),
2810            Some(reddb_types::Value::UnsignedInteger(n)) => Some(n.to_string()),
2811            _ => None,
2812        });
2813    tracing::info!(
2814        target: "reddb::http_limits",
2815        max_handlers = resolved.max_handlers,
2816        execution_workers = resolved.execution_workers,
2817        handler_timeout_ms = resolved.handler_timeout_ms,
2818        retry_after_secs = resolved.retry_after_secs,
2819        max_inflight_per_principal = resolved.max_inflight_per_principal,
2820        "http_limits resolved"
2821    );
2822    server.with_http_limits(resolved)
2823}
2824
2825/// `red server --ui` (issue #1047, ADR 0051): attach the resolved red-ui
2826/// bundle directory to an HTTP server so it also serves the bundle as
2827/// static assets. No-op when `--ui` is off.
2828///
2829/// An explicit `--ui-dir <DIR>` is served as-is (no download); otherwise
2830/// the pinned bundle is resolved from the local cache, downloading on
2831/// first use (ADR 0050). A resolution failure is fatal: the operator
2832/// explicitly asked for the UI, so silently serving API-only would be
2833/// surprising and would mask an offline/checksum problem.
2834fn apply_ui_bundle(
2835    server: RedDBServer,
2836    config: &ServerCommandConfig,
2837) -> Result<RedDBServer, String> {
2838    if !config.ui {
2839        return Ok(server);
2840    }
2841    let ui_dir = match &config.ui_dir {
2842        Some(dir) => dir.clone(),
2843        None => {
2844            let cache_root = crate::server::ui_bundle_resolver::reddb_user_cache_root()
2845                .unwrap_or_else(|_| std::env::temp_dir().join("reddb"));
2846            crate::server::ui_bundle_resolver::resolve_ui_bundle(
2847                &cache_root,
2848                &crate::server::ui_bundle_resolver::HttpFetcher,
2849            )
2850            .map_err(|err| format!("resolve red-ui bundle for --ui: {err}"))?
2851        }
2852    };
2853    tracing::info!(target: "reddb::ui", dir = %ui_dir.display(), "serving red-ui bundle on HTTP surface");
2854    Ok(server.with_ui_dir(ui_dir))
2855}
2856
2857#[inline(never)]
2858fn build_http_server_with_transport_readiness(
2859    runtime: RedDBRuntime,
2860    auth_store: Arc<AuthStore>,
2861    bind_addr: String,
2862    transport_readiness: TransportReadiness,
2863) -> RedDBServer {
2864    RedDBServer::with_options(
2865        runtime,
2866        ServerOptions {
2867            bind_addr,
2868            transport_readiness,
2869            ..ServerOptions::default()
2870        },
2871    )
2872    .with_auth(auth_store)
2873}
2874
2875/// PLAN.md Phase 6.2 — build a listener that only serves
2876/// `/admin/*` + `/metrics` + `/health/*`. Defaults to `127.0.0.1`
2877/// when the env var has no host (loopback-only by default per spec).
2878#[inline(never)]
2879fn build_admin_only_server(
2880    runtime: RedDBRuntime,
2881    auth_store: Arc<AuthStore>,
2882    bind_addr: String,
2883) -> RedDBServer {
2884    RedDBServer::with_options(
2885        runtime,
2886        ServerOptions {
2887            bind_addr,
2888            surface: crate::server::ServerSurface::AdminOnly,
2889            ..ServerOptions::default()
2890        },
2891    )
2892    .with_auth(auth_store)
2893}
2894
2895/// PLAN.md Phase 6.2 — build a listener that only serves `/metrics`
2896/// + `/health/*`. Suitable for Prometheus scrape ports that may be
2897///   exposed wider than the admin port.
2898#[inline(never)]
2899fn build_metrics_only_server(
2900    runtime: RedDBRuntime,
2901    auth_store: Arc<AuthStore>,
2902    bind_addr: String,
2903) -> RedDBServer {
2904    RedDBServer::with_options(
2905        runtime,
2906        ServerOptions {
2907            bind_addr,
2908            surface: crate::server::ServerSurface::MetricsOnly,
2909            ..ServerOptions::default()
2910        },
2911    )
2912    .with_auth(auth_store)
2913}
2914
2915/// Spawn dedicated admin / metrics listeners when the operator set
2916/// `RED_ADMIN_BIND` / `RED_METRICS_BIND`. Both are optional; when
2917/// unset the existing listener keeps serving everything (back-compat).
2918fn spawn_admin_metrics_listeners(runtime: &RedDBRuntime, auth_store: &Arc<AuthStore>) {
2919    if let Some(addr) = env_nonempty("RED_ADMIN_BIND") {
2920        let server = build_admin_only_server(runtime.clone(), auth_store.clone(), addr.clone());
2921        let _ = server.serve_in_background();
2922        tracing::info!(transport = "http", surface = "admin", bind = %addr, "listener online");
2923    }
2924    if let Some(addr) = env_nonempty("RED_METRICS_BIND") {
2925        let server = build_metrics_only_server(runtime.clone(), auth_store.clone(), addr.clone());
2926        let _ = server.serve_in_background();
2927        tracing::info!(transport = "http", surface = "metrics", bind = %addr, "listener online");
2928    }
2929}
2930
2931/// Resolve the HTTP TLS config from CLI / env / dev defaults.
2932fn resolve_http_tls_config(
2933    config: &ServerCommandConfig,
2934) -> Result<crate::server::tls::HttpTlsConfig, String> {
2935    match (&config.http_tls_cert, &config.http_tls_key) {
2936        (Some(cert), Some(key)) => Ok(crate::server::tls::HttpTlsConfig {
2937            cert_path: cert.clone(),
2938            key_path: key.clone(),
2939            client_ca_path: config.http_tls_client_ca.clone(),
2940        }),
2941        (None, None) => {
2942            // Dev-mode auto-generate next to the data directory.
2943            let dir = config
2944                .path
2945                .as_ref()
2946                .and_then(|p| p.parent().map(std::path::PathBuf::from))
2947                .unwrap_or_else(|| std::path::PathBuf::from("."));
2948            let auto = crate::server::tls::auto_generate_dev_cert(&dir)
2949                .map_err(|err| format!("HTTP TLS dev: {err}"))?;
2950            Ok(crate::server::tls::HttpTlsConfig {
2951                cert_path: auto.cert_path,
2952                key_path: auto.key_path,
2953                client_ca_path: config.http_tls_client_ca.clone(),
2954            })
2955        }
2956        _ => Err("HTTP TLS requires both --http-tls-cert and --http-tls-key (or neither, with RED_HTTP_TLS_DEV=1)".to_string()),
2957    }
2958}
2959
2960#[cfg(test)]
2961mod tests {
2962    use super::*;
2963
2964    #[test]
2965    fn server_runtime_receives_the_bootstrapped_auth_authority() {
2966        let mut options = RedDBOptions::in_memory();
2967        options.auth.enabled = true;
2968        let (runtime, store, _telemetry) =
2969            build_runtime_with_telemetry(options, None).expect("server runtime");
2970        let installed = runtime.auth_store().expect("runtime auth authority");
2971        assert!(Arc::ptr_eq(&store, &installed));
2972    }
2973
2974    #[test]
2975    fn runtime_workers_use_both_cores_on_small_instances() {
2976        assert_eq!(suggested_worker_count(1), 1);
2977        assert_eq!(suggested_worker_count(2), 2);
2978        assert_eq!(suggested_worker_count(4), 3);
2979    }
2980
2981    #[test]
2982    fn render_systemd_unit_contains_expected_execstart() {
2983        let config = SystemdServiceConfig {
2984            service_name: "reddb".to_string(),
2985            binary_path: PathBuf::from("/usr/local/bin/red"),
2986            run_user: "reddb".to_string(),
2987            run_group: "reddb".to_string(),
2988            data_path: reddb_file::default_service_database_path(),
2989            router_bind_addr: None,
2990            grpc_bind_addr: Some("0.0.0.0:55055".to_string()),
2991            http_bind_addr: None,
2992        };
2993
2994        let unit = render_systemd_unit(&config);
2995        assert!(unit.contains("ExecStart=/usr/local/bin/red server --path /var/lib/reddb/data.rdb --grpc-bind 0.0.0.0:55055"));
2996        assert!(unit.contains("ReadWritePaths=/var/lib/reddb"));
2997    }
2998
2999    #[test]
3000    fn systemd_service_config_derives_paths() {
3001        let config = SystemdServiceConfig {
3002            service_name: "reddb-api".to_string(),
3003            binary_path: PathBuf::from("/usr/local/bin/red"),
3004            run_user: "reddb".to_string(),
3005            run_group: "reddb".to_string(),
3006            data_path: PathBuf::from("/srv/reddb/live/data.rdb"),
3007            router_bind_addr: None,
3008            grpc_bind_addr: None,
3009            http_bind_addr: Some("127.0.0.1:5000".to_string()),
3010        };
3011
3012        assert_eq!(config.data_dir(), PathBuf::from("/srv/reddb/live"));
3013        assert_eq!(
3014            config.unit_path(),
3015            PathBuf::from("/etc/systemd/system/reddb-api.service")
3016        );
3017    }
3018
3019    #[test]
3020    fn render_systemd_unit_supports_dual_transport() {
3021        let config = SystemdServiceConfig {
3022            service_name: "reddb".to_string(),
3023            binary_path: PathBuf::from("/usr/local/bin/red"),
3024            run_user: "reddb".to_string(),
3025            run_group: "reddb".to_string(),
3026            data_path: reddb_file::default_service_database_path(),
3027            router_bind_addr: None,
3028            grpc_bind_addr: Some("0.0.0.0:55055".to_string()),
3029            http_bind_addr: Some("0.0.0.0:5000".to_string()),
3030        };
3031
3032        let unit = render_systemd_unit(&config);
3033        assert!(unit.contains("--grpc-bind 0.0.0.0:55055"));
3034        assert!(unit.contains("--http-bind 0.0.0.0:5000"));
3035    }
3036
3037    #[test]
3038    fn render_systemd_unit_supports_router_mode() {
3039        let config = SystemdServiceConfig {
3040            service_name: "reddb".to_string(),
3041            binary_path: PathBuf::from("/usr/local/bin/red"),
3042            run_user: "reddb".to_string(),
3043            run_group: "reddb".to_string(),
3044            data_path: reddb_file::default_service_database_path(),
3045            router_bind_addr: Some(DEFAULT_ROUTER_BIND_ADDR.to_string()),
3046            grpc_bind_addr: None,
3047            http_bind_addr: None,
3048        };
3049
3050        let unit = render_systemd_unit(&config);
3051        assert!(unit.contains("--bind 127.0.0.1:5050"));
3052        assert!(!unit.contains("--grpc-bind"));
3053        assert!(!unit.contains("--http-bind"));
3054    }
3055
3056    #[test]
3057    fn explicit_bind_collision_is_fatal() {
3058        let held = TcpListener::bind("127.0.0.1:0").expect("hold test port");
3059        let addr = held.local_addr().expect("held addr").to_string();
3060        let mut readiness = TransportReadiness::default();
3061
3062        let error = bind_listener_for_startup(&mut readiness, "http", &addr, true).unwrap_err();
3063
3064        assert!(error.contains("explicit http listener bind"));
3065        assert_eq!(readiness.active.len(), 0);
3066        assert_eq!(readiness.failed.len(), 1);
3067        assert!(readiness.failed[0].explicit);
3068        assert_eq!(readiness.failed[0].bind_addr, addr);
3069    }
3070
3071    // ---------- Issue #663 — `--no-auth` / `--dev` ----------
3072
3073    // Env access in tests is process-global; serialise the two
3074    // `--no-auth` tests so the REDDB_USERNAME / REDDB_PASSWORD pair
3075    // one of them sets cannot leak into the other under cargo's
3076    // default parallel runner.
3077    fn no_auth_env_lock() -> &'static std::sync::Mutex<()> {
3078        static LOCK: std::sync::OnceLock<std::sync::Mutex<()>> = std::sync::OnceLock::new();
3079        LOCK.get_or_init(|| std::sync::Mutex::new(()))
3080    }
3081
3082    fn bootstrap_cert_path(label: &str) -> PathBuf {
3083        std::env::temp_dir().join(format!(
3084            "reddb-{label}-{}-{}.cert",
3085            std::process::id(),
3086            std::time::SystemTime::now()
3087                .duration_since(std::time::UNIX_EPOCH)
3088                .unwrap_or_default()
3089                .as_nanos()
3090        ))
3091    }
3092
3093    #[test]
3094    fn secure_presets_require_certificate_file_delivery() {
3095        let _guard = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3096        unsafe {
3097            std::env::remove_var(BOOTSTRAP_CERT_OUT_ENV);
3098        }
3099        let missing = BootstrapConfig::default();
3100        assert!(require_bootstrap_certificate_output(&missing, PRESET_PRODUCTION).is_err());
3101
3102        let configured = BootstrapConfig {
3103            cert_out: Some(bootstrap_cert_path("required-output")),
3104            ..BootstrapConfig::default()
3105        };
3106        assert!(require_bootstrap_certificate_output(&configured, PRESET_PRODUCTION).is_ok());
3107    }
3108
3109    #[test]
3110    fn official_images_enable_persistent_vault_auth() {
3111        for dockerfile in [
3112            include_str!("../../../Dockerfile"),
3113            include_str!("../../../docker/Dockerfile.release"),
3114            include_str!("../../../docker/Dockerfile.musl"),
3115        ] {
3116            assert!(dockerfile.contains("REDDB_AUTH=true"));
3117            assert!(dockerfile.contains("REDDB_REQUIRE_AUTH=true"));
3118            assert!(dockerfile.contains("REDDB_VAULT=true"));
3119            assert!(!dockerfile.contains("REDDB_VAULT=false"));
3120        }
3121    }
3122
3123    #[test]
3124    fn completed_required_auth_database_cannot_reenter_bootstrap() {
3125        let error = ensure_persisted_auth_state(true, true, true)
3126            .expect_err("empty auth state after durable completion must fail closed");
3127        assert!(error.contains("refusing to reopen public bootstrap"));
3128
3129        assert!(ensure_persisted_auth_state(false, true, true).is_ok());
3130        assert!(ensure_persisted_auth_state(true, false, true).is_ok());
3131        assert!(ensure_persisted_auth_state(true, true, false).is_ok());
3132    }
3133
3134    #[test]
3135    fn failed_certificate_delivery_reservation_is_removed() {
3136        let path = bootstrap_cert_path("failed-reservation");
3137        let bootstrap = BootstrapConfig {
3138            cert_out: Some(path.clone()),
3139            ..BootstrapConfig::default()
3140        };
3141
3142        let output = open_bootstrap_certificate_output(&bootstrap).expect("reserve output");
3143        assert!(path.exists());
3144        #[cfg(unix)]
3145        {
3146            use std::os::unix::fs::PermissionsExt;
3147            let mode = std::fs::metadata(&path)
3148                .expect("certificate metadata")
3149                .permissions()
3150                .mode()
3151                & 0o777;
3152            assert_eq!(mode, 0o600);
3153        }
3154        drop(output);
3155        assert!(!path.exists());
3156    }
3157
3158    #[test]
3159    fn configured_vault_credentials_do_not_require_certificate_output() {
3160        use crate::storage::engine::pager::{Pager, PagerConfig};
3161
3162        let config = crate::auth::AuthConfig {
3163            enabled: true,
3164            require_auth: true,
3165            vault_enabled: true,
3166            ..crate::auth::AuthConfig::default()
3167        };
3168
3169        let passphrase_dir = bootstrap_cert_path("passphrase-vault");
3170        std::fs::create_dir_all(&passphrase_dir).expect("passphrase temp directory");
3171        let passphrase_pager = Arc::new(
3172            Pager::open(passphrase_dir.join("test.rdb"), PagerConfig::default())
3173                .expect("passphrase pager"),
3174        );
3175        let passphrase_store = Arc::new(
3176            AuthStore::with_vault_passphrase(
3177                config.clone(),
3178                Arc::clone(&passphrase_pager),
3179                "test-passphrase",
3180            )
3181            .expect("passphrase vault"),
3182        );
3183        assert!(!passphrase_store.bootstrap_requires_certificate_delivery());
3184        bootstrap_with_certificate_output(&passphrase_store, "admin", "secret", &mut None)
3185            .expect("passphrase bootstrap without certificate output");
3186        drop(passphrase_store);
3187        drop(passphrase_pager);
3188        let _ = std::fs::remove_dir_all(passphrase_dir);
3189
3190        let certificate_dir = bootstrap_cert_path("certificate-vault");
3191        std::fs::create_dir_all(&certificate_dir).expect("certificate temp directory");
3192        let certificate_pager = Arc::new(
3193            Pager::open(certificate_dir.join("test.rdb"), PagerConfig::default())
3194                .expect("certificate pager"),
3195        );
3196        let certificate = "11".repeat(32);
3197        let certificate_store = Arc::new(
3198            AuthStore::with_vault_certificate(config, Arc::clone(&certificate_pager), &certificate)
3199                .expect("certificate vault"),
3200        );
3201        assert!(!certificate_store.bootstrap_requires_certificate_delivery());
3202        bootstrap_with_certificate_output(&certificate_store, "admin", "secret", &mut None)
3203            .expect("certificate bootstrap without duplicate output");
3204        drop(certificate_store);
3205        drop(certificate_pager);
3206        let _ = std::fs::remove_dir_all(certificate_dir);
3207    }
3208
3209    #[test]
3210    fn required_auth_simple_preset_waits_for_durable_admin() {
3211        let _guard = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3212        let runtime = RedDBRuntime::with_options(RedDBOptions::in_memory()).expect("runtime");
3213        let auth_store = Arc::new(AuthStore::new(crate::auth::AuthConfig {
3214            enabled: true,
3215            require_auth: true,
3216            vault_enabled: true,
3217            ..crate::auth::AuthConfig::default()
3218        }));
3219
3220        apply_preset(&runtime, &auth_store).expect("pending simple preset");
3221        assert!(runtime
3222            .db()
3223            .store()
3224            .try_get_config(BOOTSTRAP_COMPLETED_KEY)
3225            .expect("completion lookup")
3226            .is_none());
3227    }
3228
3229    #[test]
3230    fn server_options_preserve_explicit_disk_residency() {
3231        let _guard = no_auth_env_lock().lock().unwrap();
3232        let mut config = no_auth_test_config(true);
3233        assert!(!config.to_db_options().unwrap().bounded_disk_residency);
3234        config.bounded_disk_residency = true;
3235        assert!(config.to_db_options().unwrap().bounded_disk_residency);
3236    }
3237
3238    #[test]
3239    fn replication_term_parser_defaults_and_rejects_unsafe_values() {
3240        assert_eq!(
3241            parse_replication_term(None).expect("default term"),
3242            crate::replication::DEFAULT_REPLICATION_TERM
3243        );
3244        assert_eq!(parse_replication_term(Some("8")).expect("explicit term"), 8);
3245        for raw in ["", "0", "-1", "abc"] {
3246            let error = parse_replication_term(Some(raw)).expect_err("invalid term");
3247            assert!(error.contains("positive integer"), "{error}");
3248        }
3249    }
3250
3251    fn no_auth_test_config(no_auth: bool) -> ServerCommandConfig {
3252        ServerCommandConfig {
3253            path: None,
3254            router_bind_addr: Some(DEFAULT_ROUTER_BIND_ADDR.to_string()),
3255            router_bind_explicit: false,
3256            grpc_bind_addr: None,
3257            grpc_bind_explicit: false,
3258            grpc_tls_bind_addr: None,
3259            grpc_tls_cert: None,
3260            grpc_tls_key: None,
3261            grpc_tls_client_ca: None,
3262            http_bind_addr: None,
3263            http_bind_explicit: false,
3264            http_tls_bind_addr: None,
3265            http_tls_cert: None,
3266            http_tls_key: None,
3267            http_tls_client_ca: None,
3268            wire_bind_addr: None,
3269            wire_bind_explicit: false,
3270            wire_tls_bind_addr: None,
3271            wire_tls_cert: None,
3272            wire_tls_key: None,
3273            pg_bind_addr: None,
3274            create_if_missing: true,
3275            bounded_disk_residency: false,
3276            read_only: false,
3277            role: "standalone".to_string(),
3278            primary_addr: None,
3279            storage_profile: StorageProfileSelection::embedded_single_file(),
3280            auth: false,
3281            require_auth: false,
3282            // Operator-set `--vault`: `--no-auth` must override this
3283            // alongside REDDB_USERNAME/PASSWORD.
3284            vault: true,
3285            no_auth,
3286            workers: None,
3287            telemetry: None,
3288            http_limits_cli: crate::server::HttpLimitsCliInput::default(),
3289            ui: false,
3290            ui_dir: None,
3291            bootstrap: BootstrapConfig::default(),
3292        }
3293    }
3294
3295    #[test]
3296    fn no_auth_flag_disables_every_auth_knob_and_stamps_metadata() {
3297        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3298        // Pre-existing env that *would* turn auth on if `--no-auth`
3299        // weren't the last word. The acceptance criterion is that
3300        // the flag wins over env.
3301        // SAFETY: serialised by `no_auth_env_lock` above.
3302        unsafe {
3303            std::env::set_var("REDDB_USERNAME", "admin");
3304            std::env::set_var("REDDB_PASSWORD", "hunter2");
3305        }
3306        let config = no_auth_test_config(true);
3307        let options = config.to_db_options().expect("to_db_options");
3308
3309        assert!(no_auth_active(&options), "metadata should be stamped");
3310        assert!(!options.auth.enabled, "auth.enabled must be forced off");
3311        assert!(
3312            !options.auth.require_auth,
3313            "require_auth must be forced off"
3314        );
3315        assert!(
3316            !options.auth.vault_enabled,
3317            "vault_enabled must be forced off (overrides --vault)"
3318        );
3319        assert_eq!(
3320            options.metadata.get(NO_AUTH_META).map(String::as_str),
3321            Some("true"),
3322        );
3323
3324        // SAFETY: serialised by `no_auth_env_lock` above.
3325        unsafe {
3326            std::env::remove_var("REDDB_USERNAME");
3327            std::env::remove_var("REDDB_PASSWORD");
3328        }
3329    }
3330
3331    #[test]
3332    fn default_behaviour_without_no_auth_flag_is_unchanged() {
3333        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3334        let config = no_auth_test_config(false);
3335        let options = config.to_db_options().expect("to_db_options");
3336
3337        assert!(
3338            !no_auth_active(&options),
3339            "default boot must not be marked no-auth"
3340        );
3341        assert!(
3342            options.metadata.get(NO_AUTH_META).is_none(),
3343            "metadata key must be absent when flag is off"
3344        );
3345        // `--vault` should still take effect when `--no-auth` is not set.
3346        assert!(options.auth.vault_enabled);
3347    }
3348
3349    #[test]
3350    fn no_auth_active_blocks_bootstrap_from_env() {
3351        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3352        // SAFETY: serialised by `no_auth_env_lock` above. The pair
3353        // would normally cause `AuthStore::bootstrap_from_env` to
3354        // create an admin; the boot pipeline must suppress that call
3355        // whenever `no_auth_active` is true.
3356        unsafe {
3357            std::env::set_var("REDDB_USERNAME", "admin");
3358            std::env::set_var("REDDB_PASSWORD", "hunter2");
3359        }
3360
3361        let options = no_auth_test_config(true)
3362            .to_db_options()
3363            .expect("to_db_options");
3364
3365        // Mirror the exact branch in `build_runtime_with_telemetry`:
3366        // build a non-vault AuthStore from `options.auth`, then call
3367        // `bootstrap_from_env` *only* when the no-auth gate is off.
3368        let auth_store = AuthStore::new(options.auth.clone());
3369        if !no_auth_active(&options) {
3370            auth_store.bootstrap_from_env();
3371        }
3372
3373        assert!(
3374            auth_store.needs_bootstrap(),
3375            "no admin user must be bootstrapped under --no-auth even with REDDB_USERNAME/PASSWORD set"
3376        );
3377
3378        // SAFETY: serialised by `no_auth_env_lock` above.
3379        unsafe {
3380            std::env::remove_var("REDDB_USERNAME");
3381            std::env::remove_var("REDDB_PASSWORD");
3382        }
3383    }
3384
3385    // ---------- Issue #650 — bootstrap presets ----------
3386
3387    // Preset tests mutate process-global env (`REDDB_PRESET`,
3388    // `REDDB_USERNAME`, `REDDB_PASSWORD`) and the global tracing
3389    // subscriber. Share the no_auth lock so they don't race with each
3390    // other or with the --no-auth tests above.
3391    fn clear_preset_env() {
3392        // SAFETY: callers hold `no_auth_env_lock()`.
3393        unsafe {
3394            std::env::remove_var(BOOTSTRAP_PRESET_ENV);
3395            std::env::remove_var(PRESET_ENV);
3396            std::env::remove_var("REDDB_BOOTSTRAP_MANIFEST");
3397            std::env::remove_var("REDDB_AUTH");
3398            std::env::remove_var("REDDB_REQUIRE_AUTH");
3399            std::env::remove_var("REDDB_NO_AUTH");
3400            std::env::remove_var("REDDB_DEV");
3401            std::env::remove_var("REDDB_VAULT");
3402            std::env::remove_var("REDDB_USERNAME");
3403            std::env::remove_var("REDDB_PASSWORD");
3404            std::env::remove_var("REDDB_USERNAME_FILE");
3405            std::env::remove_var("REDDB_PASSWORD_FILE");
3406            std::env::remove_var(BOOTSTRAP_CERT_OUT_ENV);
3407            std::env::remove_var("REDDB_CLOUD_HEAD_ADMIN");
3408            std::env::remove_var("REDDB_CLOUD_HEAD_ADMIN_PASSWORD");
3409            std::env::remove_var("REDDB_CLOUD_HEAD_ADMIN_PASSWORD_FILE");
3410            std::env::remove_var("REDDB_CUSTOMER_ADMIN");
3411            std::env::remove_var("REDDB_CUSTOMER_ADMIN_PASSWORD");
3412            std::env::remove_var("REDDB_CUSTOMER_ADMIN_PASSWORD_FILE");
3413        }
3414    }
3415
3416    fn clear_backup_env() {
3417        // SAFETY: callers hold `no_auth_env_lock()`.
3418        unsafe {
3419            std::env::remove_var("REDDB_BACKUP_S3_ENDPOINT");
3420            std::env::remove_var("REDDB_BACKUP_S3_BUCKET");
3421            std::env::remove_var("REDDB_BACKUP_S3_PREFIX");
3422            std::env::remove_var("REDDB_BACKUP_S3_ACCESS_KEY_ID");
3423            std::env::remove_var("REDDB_BACKUP_S3_SECRET_ACCESS_KEY");
3424            std::env::remove_var("REDDB_BACKUP_S3_REGION");
3425            std::env::remove_var("REDDB_BACKUP_CHECKPOINT_INTERVAL_SECS");
3426            std::env::remove_var("REDDB_BACKUP_WAL_FLUSH_INTERVAL_SECS");
3427            std::env::remove_var("REDDB_BACKUP_PAUSE_ON_LAG_SECS");
3428        }
3429    }
3430
3431    fn fresh_runtime_and_store() -> (RedDBRuntime, Arc<AuthStore>) {
3432        let runtime = RedDBRuntime::with_options(RedDBOptions::in_memory()).expect("runtime");
3433        let auth_store = Arc::new(AuthStore::new(crate::auth::AuthConfig::default()));
3434        (runtime, auth_store)
3435    }
3436
3437    #[test]
3438    fn auth_env_knobs_enable_auth_require_auth_and_vault() {
3439        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3440        clear_preset_env();
3441        // SAFETY: env serialised by `no_auth_env_lock`.
3442        unsafe {
3443            std::env::set_var("REDDB_AUTH", "true");
3444            std::env::set_var("REDDB_REQUIRE_AUTH", "true");
3445            std::env::set_var("REDDB_VAULT", "true");
3446        }
3447
3448        let mut config = no_auth_test_config(false);
3449        config.vault = false;
3450        let options = config.to_db_options().expect("to_db_options");
3451
3452        assert!(options.auth.enabled);
3453        assert!(options.auth.require_auth);
3454        assert!(options.auth.vault_enabled);
3455
3456        clear_preset_env();
3457    }
3458
3459    #[test]
3460    fn production_and_cloud_presets_force_auth_require_auth_and_vault() {
3461        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3462        clear_preset_env();
3463
3464        for preset in [PRESET_PRODUCTION, PRESET_CLOUD] {
3465            let mut config = no_auth_test_config(false);
3466            config.vault = false;
3467            config.bootstrap.preset = Some(preset.to_string());
3468
3469            let options = config.to_db_options().expect("to_db_options");
3470            assert!(options.auth.enabled, "{preset} should enable auth");
3471            assert!(
3472                options.auth.require_auth,
3473                "{preset} should require authenticated requests"
3474            );
3475            assert!(options.auth.vault_enabled, "{preset} should enable vault");
3476            assert!(!no_auth_active(&options));
3477        }
3478
3479        clear_preset_env();
3480    }
3481
3482    #[test]
3483    fn no_auth_env_overrides_preset_forced_auth() {
3484        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3485        clear_preset_env();
3486        // SAFETY: env serialised by `no_auth_env_lock`.
3487        unsafe {
3488            std::env::set_var("REDDB_NO_AUTH", "true");
3489            std::env::set_var(BOOTSTRAP_PRESET_ENV, PRESET_CLOUD);
3490        }
3491
3492        let mut config = no_auth_test_config(false);
3493        config.auth = true;
3494        config.require_auth = true;
3495        let options = config.to_db_options().expect("to_db_options");
3496
3497        assert!(no_auth_active(&options));
3498        assert!(!options.auth.enabled);
3499        assert!(!options.auth.require_auth);
3500        assert!(!options.auth.vault_enabled);
3501
3502        clear_preset_env();
3503    }
3504
3505    #[test]
3506    fn simple_preset_is_default_and_persists_state() {
3507        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3508        clear_preset_env();
3509
3510        let (runtime, auth_store) = fresh_runtime_and_store();
3511        apply_preset(&runtime, &auth_store).expect("simple preset applies cleanly");
3512
3513        // No admin was created — `simple` is anonymous-friendly.
3514        assert!(
3515            auth_store.needs_bootstrap(),
3516            "simple preset must not create an admin"
3517        );
3518
3519        // Bootstrap state persisted so the next boot is a no-op.
3520        let store = runtime.db().store();
3521        let completed = store
3522            .get_config(BOOTSTRAP_COMPLETED_KEY)
3523            .expect("completed key persisted");
3524        assert!(matches!(completed, reddb_types::Value::Boolean(true)));
3525        let preset = store
3526            .get_config(BOOTSTRAP_PRESET_KEY)
3527            .expect("preset key persisted");
3528        match preset {
3529            reddb_types::Value::Text(s) => assert_eq!(s.as_ref(), PRESET_SIMPLE),
3530            other => panic!("expected Text(simple), got {other:?}"),
3531        }
3532        assert!(
3533            store.get_config(BOOTSTRAP_FIRST_ADMIN_KEY).is_none(),
3534            "simple preset must not record a first admin"
3535        );
3536
3537        clear_preset_env();
3538    }
3539
3540    #[test]
3541    fn production_preset_creates_first_admin_with_allow_all_policy() {
3542        use crate::auth::policies::{EvalContext, ResourceRef};
3543        use crate::auth::UserId;
3544
3545        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3546        clear_preset_env();
3547        let cert_out = bootstrap_cert_path("production-admin");
3548        // SAFETY: env serialised by `no_auth_env_lock`.
3549        unsafe {
3550            std::env::set_var(PRESET_ENV, PRESET_PRODUCTION);
3551            std::env::set_var("REDDB_USERNAME", "ops");
3552            std::env::set_var("REDDB_PASSWORD", "hunter2");
3553            std::env::set_var(BOOTSTRAP_CERT_OUT_ENV, &cert_out);
3554        }
3555
3556        let (runtime, auth_store) = fresh_runtime_and_store();
3557        apply_preset(&runtime, &auth_store).expect("production preset applies cleanly");
3558
3559        // Admin exists and the auth store is sealed.
3560        assert!(
3561            !auth_store.needs_bootstrap(),
3562            "production preset must seal bootstrap"
3563        );
3564        let users = auth_store.list_users();
3565        assert_eq!(users.len(), 1);
3566        let admin = &users[0];
3567        assert_eq!(admin.username, "ops");
3568        assert!(
3569            admin.tenant_id.is_none(),
3570            "first admin must be platform-scoped (tenant=None)"
3571        );
3572
3573        // Allow-all policy was installed and attached to the first admin.
3574        let policy = auth_store
3575            .get_policy(FIRST_ADMIN_ALLOW_ALL_POLICY)
3576            .expect("allow-all policy installed");
3577        assert!(!policy.statements.is_empty());
3578
3579        // Verify policy-derived authority via the policy evaluator —
3580        // not a bypass. Any action on any resource must Allow.
3581        let actor = UserId::platform("ops");
3582        let ctx = EvalContext {
3583            principal_tenant: None,
3584            current_tenant: None,
3585            peer_ip: None,
3586            mfa_present: false,
3587            now_ms: 1_700_000_000_000,
3588            principal_is_admin_role: true,
3589            principal_is_platform_scoped: true,
3590        };
3591        let arbitrary_resource = ResourceRef::new("config", "red.config.audit.enabled");
3592        assert!(
3593            auth_store.check_policy_authz(&actor, "config:write", &arbitrary_resource, &ctx),
3594            "allow-all policy must grant arbitrary actions via the evaluator"
3595        );
3596
3597        // Persisted state records the first admin id.
3598        let store = runtime.db().store();
3599        match store
3600            .get_config(BOOTSTRAP_FIRST_ADMIN_KEY)
3601            .expect("first_admin_id persisted")
3602        {
3603            reddb_types::Value::Text(s) => assert_eq!(s.as_ref(), "ops"),
3604            other => panic!("expected Text(ops), got {other:?}"),
3605        }
3606        match store.get_config(BOOTSTRAP_PRESET_KEY).unwrap() {
3607            reddb_types::Value::Text(s) => assert_eq!(s.as_ref(), PRESET_PRODUCTION),
3608            other => panic!("expected Text(production), got {other:?}"),
3609        }
3610
3611        clear_preset_env();
3612        let _ = std::fs::remove_file(cert_out);
3613    }
3614
3615    #[test]
3616    fn bootstrap_preset_env_takes_precedence_over_legacy_preset_env() {
3617        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3618        clear_preset_env();
3619        // SAFETY: env serialised by `no_auth_env_lock`.
3620        unsafe {
3621            std::env::set_var(BOOTSTRAP_PRESET_ENV, PRESET_REGULATED);
3622            std::env::set_var(PRESET_ENV, PRESET_SIMPLE);
3623        }
3624
3625        let options = no_auth_test_config(false)
3626            .to_db_options()
3627            .expect("regulated options");
3628        assert!(
3629            options.control_events.compliance_mode,
3630            "canonical REDDB_BOOTSTRAP_PRESET should win over REDDB_PRESET"
3631        );
3632        assert!(options.query_audit.enabled);
3633
3634        clear_preset_env();
3635    }
3636
3637    #[test]
3638    fn cloud_preset_creates_system_head_and_customer_admins() {
3639        use crate::auth::policies::{EvalContext, ResourceRef};
3640        use crate::auth::UserId;
3641
3642        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3643        clear_preset_env();
3644        let cert_out = bootstrap_cert_path("cloud-admins");
3645        // SAFETY: env serialised by `no_auth_env_lock`.
3646        unsafe {
3647            std::env::set_var(BOOTSTRAP_PRESET_ENV, PRESET_CLOUD);
3648            std::env::set_var("REDDB_CLOUD_HEAD_ADMIN", "head");
3649            std::env::set_var("REDDB_CLOUD_HEAD_ADMIN_PASSWORD", "head-pass");
3650            std::env::set_var("REDDB_CUSTOMER_ADMIN", "customer");
3651            std::env::set_var("REDDB_CUSTOMER_ADMIN_PASSWORD", "customer-pass");
3652            std::env::set_var(BOOTSTRAP_CERT_OUT_ENV, &cert_out);
3653        }
3654
3655        let (runtime, auth_store) = fresh_runtime_and_store();
3656        apply_preset(&runtime, &auth_store).expect("cloud preset applies cleanly");
3657
3658        let head = auth_store
3659            .get_user(None, "head")
3660            .expect("head admin should exist");
3661        assert_eq!(head.tenant_id, None);
3662        let customer = auth_store
3663            .get_user(None, "customer")
3664            .expect("customer admin should exist");
3665        assert_eq!(customer.tenant_id, None);
3666
3667        let ctx = EvalContext {
3668            principal_tenant: None,
3669            current_tenant: None,
3670            peer_ip: None,
3671            mfa_present: false,
3672            now_ms: 1_700_000_000_000,
3673            principal_is_admin_role: true,
3674            principal_is_platform_scoped: true,
3675        };
3676        assert!(auth_store.check_policy_authz(
3677            &UserId::platform("customer"),
3678            "config:write",
3679            &ResourceRef::new("config", "red.config.customer.enabled"),
3680            &ctx,
3681        ));
3682        assert!(
3683            auth_store.check_policy_authz(
3684                &UserId::platform("head"),
3685                "config:write",
3686                &ResourceRef::new("config", "red.config.cloud.enabled"),
3687                &ctx,
3688            ),
3689            "cloud head keeps allow-all authority over managed cloud config"
3690        );
3691        assert!(
3692            !auth_store.check_policy_authz(
3693                &UserId::platform("customer"),
3694                "config:write",
3695                &ResourceRef::new("config", "red.config.cloud.enabled"),
3696                &ctx,
3697            ),
3698            "cloud customer must be denied managed cloud config writes"
3699        );
3700        assert!(
3701            !auth_store.check_policy_authz(
3702                &UserId::platform("customer"),
3703                "policy:drop",
3704                &ResourceRef::new("policy", CLOUD_PROTECT_MANAGED_POLICY),
3705                &ctx,
3706            ),
3707            "cloud customer must be denied mutations of the managed guardrail policy"
3708        );
3709
3710        assert!(auth_store.get_user(None, "head").is_some());
3711        assert!(auth_store
3712            .get_policy(CLOUD_PROTECT_MANAGED_POLICY)
3713            .is_some());
3714        assert!(runtime
3715            .config_registry()
3716            .get_active(CLOUD_CONFIG_NAMESPACE)
3717            .is_some());
3718
3719        let store = runtime.db().store();
3720        match store
3721            .get_config(BOOTSTRAP_FIRST_ADMIN_KEY)
3722            .expect("head admin id persisted")
3723        {
3724            reddb_types::Value::Text(s) => assert_eq!(s.as_ref(), "head"),
3725            other => panic!("expected Text(head), got {other:?}"),
3726        }
3727        match store.get_config(BOOTSTRAP_PRESET_KEY).unwrap() {
3728            reddb_types::Value::Text(s) => assert_eq!(s.as_ref(), PRESET_CLOUD),
3729            other => panic!("expected Text(cloud), got {other:?}"),
3730        }
3731
3732        clear_preset_env();
3733        let _ = std::fs::remove_file(cert_out);
3734    }
3735
3736    #[test]
3737    fn cloud_preset_customer_cannot_disable_head_admin_or_drop_guardrail_policy() {
3738        use crate::auth::Role;
3739        use crate::runtime::mvcc::{clear_current_auth_identity, set_current_auth_identity};
3740
3741        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3742        clear_preset_env();
3743
3744        let bootstrap = BootstrapConfig {
3745            preset: Some(PRESET_CLOUD.to_string()),
3746            cloud_head_admin: Some("red_admin".to_string()),
3747            cloud_head_admin_password: Some("head-pass".to_string()),
3748            customer_admin: Some("admin".to_string()),
3749            customer_admin_password: Some("customer-pass".to_string()),
3750            cert_out: Some(bootstrap_cert_path("cloud-guardrail")),
3751            ..BootstrapConfig::default()
3752        };
3753        let (runtime, auth_store) = fresh_runtime_and_store();
3754        apply_preset_from_config(&runtime, &auth_store, &bootstrap)
3755            .expect("cloud preset applies cleanly");
3756        runtime.set_auth_store(Arc::clone(&auth_store));
3757
3758        set_current_auth_identity("admin".to_string(), Role::Admin);
3759        let disable_head = runtime.execute_query("ALTER USER red_admin DISABLE");
3760        let drop_guardrail =
3761            runtime.execute_query(&format!("DROP POLICY '{}'", CLOUD_PROTECT_MANAGED_POLICY));
3762        let create_user = runtime.execute_query("CREATE USER app_user WITH PASSWORD 'p' ROLE read");
3763        let disable_user = runtime.execute_query("ALTER USER app_user DISABLE");
3764        clear_current_auth_identity();
3765
3766        let disable_head_err = disable_head.expect_err("customer admin must not disable red_admin");
3767        assert!(
3768            disable_head_err.to_string().contains("user:disable"),
3769            "error should name the denied lifecycle action: {disable_head_err}"
3770        );
3771        assert!(
3772            auth_store
3773                .get_user(None, "red_admin")
3774                .expect("red_admin should still exist")
3775                .enabled,
3776            "denied mutation must leave red_admin enabled"
3777        );
3778
3779        let drop_guardrail_err =
3780            drop_guardrail.expect_err("customer admin must not drop cloud guardrail policy");
3781        assert!(
3782            drop_guardrail_err.to_string().contains("managed policy"),
3783            "error should name the managed policy guardrail: {drop_guardrail_err}"
3784        );
3785        assert!(auth_store
3786            .get_policy(CLOUD_PROTECT_MANAGED_POLICY)
3787            .is_some());
3788
3789        create_user.expect("customer admin should retain normal create-user authority");
3790        disable_user.expect("customer admin should retain normal user-disable authority");
3791        assert!(
3792            !auth_store
3793                .get_user(None, "app_user")
3794                .expect("app_user should exist")
3795                .enabled,
3796            "normal capability should still apply outside the reserved head admin"
3797        );
3798
3799        let _ = bootstrap.cert_out.as_ref().map(std::fs::remove_file);
3800        clear_preset_env();
3801    }
3802
3803    #[test]
3804    fn cloud_preset_cli_admin_alias_wins_over_cloud_env_password() {
3805        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3806        clear_preset_env();
3807        // SAFETY: env serialised by `no_auth_env_lock`.
3808        unsafe {
3809            std::env::set_var("REDDB_CLOUD_HEAD_ADMIN_PASSWORD", "env-head-pass");
3810        }
3811
3812        let bootstrap = BootstrapConfig {
3813            preset: Some(PRESET_CLOUD.to_string()),
3814            admin_username: Some("head".to_string()),
3815            admin_password: Some("cli-head-pass".to_string()),
3816            customer_admin: Some("customer".to_string()),
3817            customer_admin_password: Some("customer-pass".to_string()),
3818            cert_out: Some(bootstrap_cert_path("cloud-alias")),
3819            ..BootstrapConfig::default()
3820        };
3821        let (runtime, auth_store) = fresh_runtime_and_store();
3822        apply_preset_from_config(&runtime, &auth_store, &bootstrap)
3823            .expect("cloud preset applies cleanly");
3824
3825        auth_store
3826            .authenticate("head", "cli-head-pass")
3827            .expect("CLI alias password should win");
3828        assert!(
3829            auth_store.authenticate("head", "env-head-pass").is_err(),
3830            "cloud-specific env password must not beat CLI alias"
3831        );
3832
3833        let _ = bootstrap.cert_out.as_ref().map(std::fs::remove_file);
3834        clear_preset_env();
3835    }
3836
3837    #[test]
3838    fn bootstrap_intent_manifest_matches_cloud_flag_path() {
3839        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3840        clear_preset_env();
3841
3842        let manifest_dir = std::env::current_dir()
3843            .expect("current dir")
3844            .join(".red/tmp/bootstrap-manifest-tests");
3845        std::fs::create_dir_all(&manifest_dir).expect("create manifest test dir");
3846        let unique = format!(
3847            "{}-{}",
3848            std::process::id(),
3849            std::time::SystemTime::now()
3850                .duration_since(std::time::UNIX_EPOCH)
3851                .unwrap_or_default()
3852                .as_millis()
3853        );
3854        let head_password = manifest_dir.join(format!("head-{unique}.secret"));
3855        let customer_password = manifest_dir.join(format!("customer-{unique}.secret"));
3856        let manifest_path = manifest_dir.join(format!("intent-{unique}.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        std::fs::write(
3860            &manifest_path,
3861            format!(
3862                r#"{{
3863                    "bootstrap": {{
3864                        "preset": "cloud",
3865                        "cloud_head_admin": {{
3866                            "username": "head",
3867                            "password_file": "{}"
3868                        }},
3869                        "customer_admin": {{
3870                            "username": "customer",
3871                            "password_file": "{}"
3872                        }}
3873                    }}
3874                }}"#,
3875                head_password.display(),
3876                customer_password.display()
3877            ),
3878        )
3879        .expect("write manifest");
3880
3881        let mut config = no_auth_test_config(false);
3882        config.bootstrap.manifest = Some(manifest_path.clone());
3883        let options = config.to_db_options().expect("manifest options");
3884        assert!(options.auth.enabled);
3885        assert!(options.auth.require_auth);
3886        assert!(options.auth.vault_enabled);
3887
3888        let (runtime, auth_store) = fresh_runtime_and_store();
3889        apply_preset_from_config(&runtime, &auth_store, &config.bootstrap)
3890            .expect("manifest intent applies cleanly");
3891
3892        auth_store
3893            .authenticate("head", "head-pass")
3894            .expect("head password comes from file");
3895        auth_store
3896            .authenticate("customer", "customer-pass")
3897            .expect("customer password comes from file");
3898        assert!(auth_store.get_user(None, "head").is_some());
3899        assert!(auth_store.get_user(None, "customer").is_some());
3900        assert!(auth_store
3901            .get_policy(CLOUD_PROTECT_MANAGED_POLICY)
3902            .is_some());
3903        match runtime
3904            .db()
3905            .store()
3906            .get_config(BOOTSTRAP_PRESET_KEY)
3907            .expect("preset key persisted")
3908        {
3909            reddb_types::Value::Text(s) => assert_eq!(s.as_ref(), PRESET_CLOUD),
3910            other => panic!("expected Text(cloud), got {other:?}"),
3911        }
3912
3913        apply_preset_from_config(&runtime, &auth_store, &config.bootstrap)
3914            .expect("second boot is idempotent");
3915
3916        let _ = std::fs::remove_file(&manifest_path);
3917        let _ = std::fs::remove_file(&head_password);
3918        let _ = std::fs::remove_file(&customer_password);
3919        clear_preset_env();
3920    }
3921
3922    #[test]
3923    fn regulated_preset_enables_query_audit_infrastructure_without_rules() {
3924        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3925        clear_preset_env();
3926        // SAFETY: env serialised by `no_auth_env_lock`.
3927        unsafe {
3928            std::env::set_var(PRESET_ENV, PRESET_REGULATED);
3929        }
3930
3931        let (runtime, auth_store) = fresh_runtime_and_store();
3932        apply_preset(&runtime, &auth_store).expect("regulated preset applies cleanly");
3933
3934        assert!(runtime.query_audit().is_enabled());
3935        assert!(runtime.query_audit().rules().is_empty());
3936        assert!(
3937            runtime
3938                .db()
3939                .store()
3940                .get_collection(crate::runtime::query_audit::QUERY_AUDIT_COLLECTION)
3941                .is_some(),
3942            "regulated preset should create the query-audit stream"
3943        );
3944
3945        runtime
3946            .execute_query("CREATE TABLE docs (id INT)")
3947            .expect("create table");
3948        runtime
3949            .execute_query("INSERT INTO docs (id) VALUES (1)")
3950            .expect("insert");
3951        runtime.execute_query("SELECT * FROM docs").expect("select");
3952        let rows = runtime
3953            .db()
3954            .store()
3955            .get_collection(crate::runtime::query_audit::QUERY_AUDIT_COLLECTION)
3956            .expect("query audit collection")
3957            .query_all(|_| true);
3958        assert!(
3959            rows.is_empty(),
3960            "regulated preset must not globally audit every query"
3961        );
3962
3963        clear_preset_env();
3964    }
3965
3966    #[test]
3967    fn managed_backup_env_rejects_primary_replica_single_file_storage() {
3968        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3969        clear_backup_env();
3970        // SAFETY: env serialised by `no_auth_env_lock`.
3971        unsafe {
3972            std::env::set_var("REDDB_BACKUP_S3_ENDPOINT", "https://s3.example.test");
3973            std::env::set_var("REDDB_BACKUP_S3_BUCKET", "reddb");
3974            std::env::set_var("REDDB_BACKUP_S3_PREFIX", "clusters/prod");
3975            std::env::set_var("REDDB_BACKUP_S3_ACCESS_KEY_ID", "AK");
3976            std::env::set_var("REDDB_BACKUP_S3_SECRET_ACCESS_KEY", "SK");
3977        }
3978
3979        let mut config = no_auth_test_config(false);
3980        config.role = "primary".to_string();
3981        config.storage_profile = crate::storage::StorageDeployPreset::PrimaryReplicaDev.selection();
3982
3983        let err = config.to_db_options().unwrap_err();
3984        assert!(err.contains("managed backup"), "got: {err}");
3985        assert!(err.contains("operational-directory"), "got: {err}");
3986
3987        clear_backup_env();
3988    }
3989
3990    #[test]
3991    fn lease_only_backend_preserves_primary_replica_single_file_storage() {
3992        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3993        clear_backup_env();
3994        // SAFETY: env serialised by `no_auth_env_lock`.
3995        unsafe {
3996            std::env::set_var("RED_BACKEND", "fs");
3997            std::env::set_var("RED_REMOTE_KEY", "lease-only-test");
3998        }
3999
4000        let mut config = no_auth_test_config(false);
4001        config.role = "primary".to_string();
4002        config.storage_profile = crate::storage::StorageDeployPreset::PrimaryReplicaDev.selection();
4003
4004        let options = config.to_db_options().expect("lease-only backend options");
4005        assert!(options.remote_backend_atomic.is_some());
4006        assert_eq!(
4007            options.storage_profile,
4008            crate::storage::StorageDeployPreset::PrimaryReplicaDev.selection()
4009        );
4010
4011        // SAFETY: env serialised by `no_auth_env_lock`.
4012        unsafe {
4013            std::env::remove_var("RED_BACKEND");
4014            std::env::remove_var("RED_REMOTE_KEY");
4015        }
4016    }
4017
4018    #[test]
4019    fn regulated_preset_installs_managed_evidence_guardrails_end_to_end() {
4020        use crate::auth::policies::{EvalContext, Policy, ResourceRef};
4021        use crate::auth::store::PrincipalRef;
4022        use crate::auth::{Role, UserId};
4023        use crate::runtime::mvcc::{clear_current_auth_identity, set_current_auth_identity};
4024        use reddb_types::Value;
4025
4026        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
4027        clear_preset_env();
4028        // SAFETY: env serialised by `no_auth_env_lock`.
4029        unsafe {
4030            std::env::set_var(PRESET_ENV, PRESET_REGULATED);
4031        }
4032
4033        let options = no_auth_test_config(false)
4034            .to_db_options()
4035            .expect("regulated options");
4036        assert!(
4037            options.control_events.compliance_mode,
4038            "regulated preset must enable fail-closed control evidence before runtime boot"
4039        );
4040        assert!(
4041            options.query_audit.enabled && options.query_audit.rules.is_empty(),
4042            "regulated preset must enable query-audit infrastructure without global rules"
4043        );
4044
4045        let runtime = RedDBRuntime::with_options(options).expect("runtime");
4046        let auth_store = Arc::new(AuthStore::new(crate::auth::AuthConfig::default()));
4047        apply_preset(&runtime, &auth_store).expect("regulated preset applies cleanly");
4048        runtime.set_auth_store(Arc::clone(&auth_store));
4049
4050        assert!(runtime.control_events_require_persistence());
4051        assert!(runtime.query_audit().is_enabled());
4052        assert!(runtime.query_audit().rules().is_empty());
4053        assert!(auth_store
4054            .get_policy(REGULATED_PROTECT_MANAGED_POLICY)
4055            .is_some());
4056
4057        let managed_policy = runtime
4058            .config_registry()
4059            .get_active(REGULATED_PROTECT_MANAGED_POLICY)
4060            .expect("regulated managed policy registry entry");
4061        assert!(managed_policy.managed);
4062        assert_eq!(managed_policy.resource_type, "policy");
4063        assert!(
4064            runtime
4065                .config_registry()
4066                .get_active(REGULATED_AUDIT_CONFIG_NAMESPACE)
4067                .expect("regulated audit config namespace")
4068                .managed
4069        );
4070
4071        let registry_rows = runtime
4072            .execute_query(&format!(
4073                "SELECT id, managed FROM red.registry WHERE id = '{}'",
4074                REGULATED_PROTECT_MANAGED_POLICY
4075            ))
4076            .expect("red.registry query");
4077        assert_eq!(registry_rows.result.records.len(), 1);
4078        assert_eq!(
4079            registry_rows.result.records[0].get("managed"),
4080            Some(&Value::Boolean(true))
4081        );
4082
4083        let managed_policy_rows = runtime
4084            .execute_query(&format!(
4085                "SELECT policy_id FROM red.managed_policies WHERE policy_id = '{}'",
4086                REGULATED_PROTECT_MANAGED_POLICY
4087            ))
4088            .expect("red.managed_policies query");
4089        assert_eq!(managed_policy_rows.result.records.len(), 1);
4090
4091        let capability_rows = runtime
4092            .execute_query(
4093                "SELECT action FROM red.control_capabilities WHERE action = 'evidence:export'",
4094            )
4095            .expect("red.control_capabilities query");
4096        assert_eq!(capability_rows.result.records.len(), 1);
4097
4098        auth_store
4099            .create_user("alice", "p", Role::Admin)
4100            .expect("create ordinary admin");
4101        let allow_all = Policy::from_json_str(
4102            r#"{
4103                "id": "alice-allow-all",
4104                "version": 1,
4105                "statements": [{
4106                    "effect": "allow",
4107                    "actions": ["*"],
4108                    "resources": ["*"]
4109                }]
4110            }"#,
4111        )
4112        .expect("allow-all policy");
4113        auth_store.put_policy(allow_all).expect("install allow-all");
4114        auth_store
4115            .attach_policy(
4116                PrincipalRef::User(UserId::platform("alice")),
4117                "alice-allow-all",
4118            )
4119            .expect("attach allow-all");
4120        let ctx = EvalContext {
4121            principal_tenant: None,
4122            current_tenant: None,
4123            peer_ip: None,
4124            mfa_present: false,
4125            now_ms: 1_700_000_000_000,
4126            principal_is_admin_role: true,
4127            principal_is_platform_scoped: true,
4128        };
4129        assert!(
4130            !auth_store.check_policy_authz(
4131                &UserId::platform("alice"),
4132                "policy:drop",
4133                &ResourceRef::new("policy", REGULATED_PROTECT_MANAGED_POLICY),
4134                &ctx,
4135            ),
4136            "managed guardrail deny policy must be effective for ordinary admins"
4137        );
4138
4139        set_current_auth_identity("alice".to_string(), Role::Admin);
4140        let denied = runtime.execute_query(&format!(
4141            "DROP POLICY '{}'",
4142            REGULATED_PROTECT_MANAGED_POLICY
4143        ));
4144        clear_current_auth_identity();
4145        let err = denied.expect_err("managed policy guardrail must deny ordinary admin");
4146        assert!(
4147            err.to_string().contains("managed policy"),
4148            "error should name the managed guardrail: {err}"
4149        );
4150        assert!(
4151            auth_store
4152                .get_policy(REGULATED_PROTECT_MANAGED_POLICY)
4153                .is_some(),
4154            "denied mutation must leave managed policy installed"
4155        );
4156
4157        let denied_events = runtime
4158            .execute_query(&format!(
4159                "SELECT action, resource, outcome FROM red.control_events \
4160                 WHERE action = 'policy:drop' AND resource = 'policy:{}'",
4161                REGULATED_PROTECT_MANAGED_POLICY
4162            ))
4163            .expect("red.control_events denied policy drop");
4164        assert_eq!(denied_events.result.records.len(), 1);
4165        assert_eq!(
4166            denied_events.result.records[0].get("outcome"),
4167            Some(&Value::text("denied"))
4168        );
4169
4170        set_current_auth_identity("alice".to_string(), Role::Admin);
4171        let config_denied = runtime.execute_query("SET CONFIG red.config.audit.enabled = true");
4172        clear_current_auth_identity();
4173        let err = config_denied.expect_err("managed config guardrail must deny ordinary admin");
4174        assert!(
4175            err.to_string().contains("managed config"),
4176            "error should name the managed config guardrail: {err}"
4177        );
4178
4179        let denied_config_events = runtime
4180            .execute_query(
4181                "SELECT action, resource, outcome FROM red.control_events \
4182                 WHERE action = 'config:write' AND resource = 'config:red.config.audit.enabled'",
4183            )
4184            .expect("red.control_events denied config write");
4185        assert_eq!(denied_config_events.result.records.len(), 1);
4186        assert_eq!(
4187            denied_config_events.result.records[0].get("outcome"),
4188            Some(&Value::text("denied"))
4189        );
4190
4191        runtime
4192            .execute_query("CREATE TABLE regulated_docs (id INT)")
4193            .expect("create user table");
4194        runtime
4195            .execute_query("SELECT * FROM regulated_docs")
4196            .expect("select user table");
4197        let audit_rows = runtime
4198            .db()
4199            .store()
4200            .get_collection(crate::runtime::query_audit::QUERY_AUDIT_COLLECTION)
4201            .expect("query audit collection")
4202            .query_all(|_| true);
4203        assert!(
4204            audit_rows.is_empty(),
4205            "regulated preset must not globally audit data-plane queries"
4206        );
4207
4208        clear_preset_env();
4209    }
4210
4211    #[test]
4212    fn bootstrap_manifest_installs_initial_users_policies_guardrails_and_config() {
4213        use crate::auth::policies::{EvalContext, ResourceRef};
4214        use crate::auth::UserId;
4215        use reddb_types::Value;
4216
4217        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
4218        clear_preset_env();
4219
4220        let manifest_dir = std::env::current_dir()
4221            .expect("current dir")
4222            .join(".red/tmp/bootstrap-manifest-tests");
4223        std::fs::create_dir_all(&manifest_dir).expect("create manifest test dir");
4224        let manifest_path = manifest_dir.join(format!(
4225            "reddb-bootstrap-manifest-{}-{}.json",
4226            std::process::id(),
4227            std::time::SystemTime::now()
4228                .duration_since(std::time::UNIX_EPOCH)
4229                .unwrap_or_default()
4230                .as_millis()
4231        ));
4232        std::fs::write(
4233            &manifest_path,
4234            r#"{
4235                "users": [
4236                    {
4237                        "username": "ops",
4238                        "password": "hunter2",
4239                        "role": "admin"
4240                    }
4241                ],
4242                "policies": [
4243                    {
4244                        "id": "bootstrap-registry-admin",
4245                        "version": 1,
4246                        "statements": [
4247                            {
4248                                "effect": "allow",
4249                                "actions": ["red.registry:*", "policy:*", "config:write", "select"],
4250                                "resources": ["registry:*", "policy:*", "config:*", "collection:docs"]
4251                            }
4252                        ]
4253                    }
4254                ],
4255                "managed_policies": [
4256                    {
4257                        "id": "managed-deny-drop",
4258                        "version": 1,
4259                        "statements": [
4260                            {
4261                                "effect": "deny",
4262                                "actions": ["policy:drop"],
4263                                "resources": ["policy:managed-deny-drop"]
4264                            }
4265                        ],
4266                        "required_resource": "policy:managed-deny-drop",
4267                        "evidence": "full"
4268                    }
4269                ],
4270                "attachments": [
4271                    {"user": "ops", "policy": "bootstrap-registry-admin"}
4272                ],
4273                "managed_config_namespaces": [
4274                    {
4275                        "id": "red.ai",
4276                        "required_action": "config:write",
4277                        "required_resource": "config:red.ai.*",
4278                        "evidence": "metadata"
4279                    }
4280                ],
4281                "config": [
4282                    {"key": "red.ai.default.provider", "value": "openai"},
4283                    {
4284                        "key": "red.ai.openai.default.secret_ref",
4285                        "secret_ref": {"collection": "red.vault", "key": "openai"}
4286                    }
4287                ],
4288                "actor": "ops"
4289            }"#,
4290        )
4291        .expect("write manifest");
4292        // SAFETY: env serialised by `no_auth_env_lock`.
4293        unsafe {
4294            std::env::set_var("REDDB_BOOTSTRAP_MANIFEST", &manifest_path);
4295        }
4296
4297        let (runtime, auth_store) = fresh_runtime_and_store();
4298        apply_preset(&runtime, &auth_store).expect("manifest applies cleanly");
4299
4300        let users = auth_store.list_users();
4301        assert_eq!(users.len(), 1);
4302        assert_eq!(users[0].username, "ops");
4303        assert!(users[0].tenant_id.is_none());
4304
4305        let actor = UserId::platform("ops");
4306        let ctx = EvalContext {
4307            principal_tenant: None,
4308            current_tenant: None,
4309            peer_ip: None,
4310            mfa_present: false,
4311            now_ms: 1_700_000_000_000,
4312            principal_is_admin_role: true,
4313            principal_is_platform_scoped: true,
4314        };
4315        // Manifest fixture pins a canonical data-plane read action.
4316        assert!(auth_store.check_policy_authz(
4317            &actor,
4318            "select",
4319            &ResourceRef::new("collection", "docs"),
4320            &ctx
4321        ));
4322
4323        let managed_policy = runtime
4324            .config_registry()
4325            .get_active("managed-deny-drop")
4326            .expect("managed policy registry entry");
4327        assert!(managed_policy.managed);
4328        assert_eq!(managed_policy.resource_type, "policy");
4329        let managed_config = runtime
4330            .config_registry()
4331            .get_active("red.ai")
4332            .expect("managed config namespace registry entry");
4333        assert!(managed_config.managed);
4334        assert_eq!(managed_config.resource_type, "config_namespace");
4335
4336        let store = runtime.db().store();
4337        match store
4338            .get_config("red.ai.default.provider")
4339            .expect("plain config persisted")
4340        {
4341            Value::Text(s) => assert_eq!(s.as_ref(), "openai"),
4342            other => panic!("expected provider text, got {other:?}"),
4343        }
4344        let Value::Json(bytes) = store
4345            .get_config("red.ai.openai.default.secret_ref")
4346            .expect("secret ref config persisted")
4347        else {
4348            panic!("secret ref must be stored as structured JSON");
4349        };
4350        let reference: crate::serde_json::Value =
4351            crate::serde_json::from_slice(&bytes).expect("secret ref json");
4352        assert_eq!(
4353            reference.get("type").and_then(|v| v.as_str()),
4354            Some("secret_ref")
4355        );
4356        assert!(
4357            !String::from_utf8_lossy(&bytes).contains("hunter2"),
4358            "manifest password must not leak into secret ref config"
4359        );
4360
4361        let completed = store
4362            .get_config(BOOTSTRAP_COMPLETED_KEY)
4363            .expect("bootstrap completion persisted");
4364        assert!(matches!(completed, Value::Boolean(true)));
4365        assert!(
4366            store
4367                .get_config("system.bootstrap.manifest.registry_entries")
4368                .is_some(),
4369            "managed registry entries must be persisted internally"
4370        );
4371
4372        std::fs::remove_file(&manifest_path).expect("remove manifest after first boot");
4373        let restored_registry = Arc::new(crate::auth::registry::ConfigRegistry::new());
4374        crate::cli::bootstrap_manifest::rehydrate_manifest_registry(&runtime, &restored_registry)
4375            .expect("registry rehydrates without manifest file");
4376        assert!(restored_registry.get_active("managed-deny-drop").is_some());
4377        assert!(restored_registry.get_active("red.ai").is_some());
4378
4379        let fresh = Arc::new(AuthStore::new(crate::auth::AuthConfig::default()));
4380        apply_preset(&runtime, &fresh).expect("re-run must not need manifest file");
4381        assert!(fresh.needs_bootstrap());
4382
4383        clear_preset_env();
4384    }
4385
4386    #[test]
4387    fn production_preset_refuses_to_start_without_password() {
4388        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
4389        clear_preset_env();
4390        let cert_out = bootstrap_cert_path("production-missing-password");
4391        // SAFETY: env serialised by `no_auth_env_lock`.
4392        unsafe {
4393            std::env::set_var(PRESET_ENV, PRESET_PRODUCTION);
4394            std::env::set_var("REDDB_USERNAME", "ops");
4395            std::env::set_var(BOOTSTRAP_CERT_OUT_ENV, &cert_out);
4396        }
4397
4398        let (runtime, auth_store) = fresh_runtime_and_store();
4399        let err = apply_preset(&runtime, &auth_store).expect_err("must reject missing password");
4400        assert!(
4401            err.contains("REDDB_PASSWORD"),
4402            "error must name the missing env: {err}"
4403        );
4404
4405        // Nothing was persisted; nothing was created.
4406        assert!(auth_store.needs_bootstrap());
4407        assert!(runtime
4408            .db()
4409            .store()
4410            .get_config(BOOTSTRAP_COMPLETED_KEY)
4411            .is_none());
4412        assert!(!cert_out.exists());
4413
4414        clear_preset_env();
4415    }
4416
4417    #[test]
4418    fn re_running_production_after_first_boot_is_a_silent_skip() {
4419        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
4420        clear_preset_env();
4421        let cert_out = bootstrap_cert_path("production-rerun");
4422        // SAFETY: env serialised by `no_auth_env_lock`.
4423        unsafe {
4424            std::env::set_var(PRESET_ENV, PRESET_PRODUCTION);
4425            std::env::set_var("REDDB_USERNAME", "ops");
4426            std::env::set_var("REDDB_PASSWORD", "hunter2");
4427            std::env::set_var(BOOTSTRAP_CERT_OUT_ENV, &cert_out);
4428        }
4429
4430        let (runtime, auth_store) = fresh_runtime_and_store();
4431        apply_preset(&runtime, &auth_store).expect("first apply");
4432        assert_eq!(auth_store.list_users().len(), 1);
4433
4434        // Second invocation: same runtime/store, same env. Must be a
4435        // no-op — no error, no duplicate admin, no duplicate policy.
4436        // We do NOT reuse `auth_store` because production sealed it; the
4437        // idempotency hinge is the persisted config key, not the auth
4438        // store's in-memory seal. Build a fresh `AuthStore` as a restart
4439        // would and confirm `apply_preset` is a silent skip.
4440        let fresh = Arc::new(AuthStore::new(crate::auth::AuthConfig::default()));
4441        apply_preset(&runtime, &fresh).expect("re-run is silent-skip");
4442        assert!(
4443            fresh.needs_bootstrap(),
4444            "re-run must not create a second admin"
4445        );
4446        assert!(
4447            fresh.get_policy(FIRST_ADMIN_ALLOW_ALL_POLICY).is_none(),
4448            "re-run must not re-install the allow-all policy on the fresh store"
4449        );
4450
4451        clear_preset_env();
4452        let _ = std::fs::remove_file(cert_out);
4453    }
4454
4455    #[test]
4456    fn unrecognised_preset_value_is_rejected() {
4457        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
4458        clear_preset_env();
4459        // SAFETY: env serialised by `no_auth_env_lock`.
4460        unsafe {
4461            std::env::set_var(PRESET_ENV, "weird");
4462        }
4463
4464        let (runtime, auth_store) = fresh_runtime_and_store();
4465        let err = apply_preset(&runtime, &auth_store).expect_err("must reject unknown preset");
4466        assert!(err.contains("weird"), "error must echo the value: {err}");
4467        assert!(auth_store.needs_bootstrap());
4468
4469        clear_preset_env();
4470    }
4471
4472    #[test]
4473    fn no_auth_short_circuits_preset_entirely() {
4474        let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
4475        clear_preset_env();
4476        // SAFETY: env serialised by `no_auth_env_lock`. Production creds
4477        // are set but `--no-auth` must win — no admin, no bootstrap state.
4478        unsafe {
4479            std::env::set_var(PRESET_ENV, PRESET_PRODUCTION);
4480            std::env::set_var("REDDB_USERNAME", "ops");
4481            std::env::set_var("REDDB_PASSWORD", "hunter2");
4482        }
4483
4484        let options = no_auth_test_config(true)
4485            .to_db_options()
4486            .expect("to_db_options");
4487        assert!(no_auth_active(&options));
4488
4489        // Mirror `build_runtime_with_telemetry`: when no_auth_active,
4490        // `apply_preset` is never called.
4491        let (runtime, auth_store) = fresh_runtime_and_store();
4492        if !no_auth_active(&options) {
4493            apply_preset(&runtime, &auth_store).expect("would apply preset");
4494        }
4495
4496        assert!(
4497            auth_store.needs_bootstrap(),
4498            "--no-auth must prevent any admin creation"
4499        );
4500        assert!(
4501            runtime
4502                .db()
4503                .store()
4504                .get_config(BOOTSTRAP_COMPLETED_KEY)
4505                .is_none(),
4506            "--no-auth must skip bootstrap-state persistence"
4507        );
4508
4509        clear_preset_env();
4510    }
4511
4512    #[test]
4513    fn implicit_bind_collision_degrades() {
4514        let held = TcpListener::bind("127.0.0.1:0").expect("hold test port");
4515        let addr = held.local_addr().expect("held addr").to_string();
4516        let mut readiness = TransportReadiness::default();
4517
4518        let listener =
4519            bind_listener_for_startup(&mut readiness, "http", &addr, false).expect("nonfatal");
4520
4521        assert!(listener.is_none());
4522        assert_eq!(readiness.active.len(), 0);
4523        assert_eq!(readiness.failed.len(), 1);
4524        assert!(!readiness.failed[0].explicit);
4525        assert_eq!(readiness.failed[0].bind_addr, addr);
4526    }
4527}