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