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