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
23pub fn detect_runtime_config() -> RuntimeConfig {
25 let cpus = thread::available_parallelism()
26 .map(|n| n.get())
27 .unwrap_or(1);
28
29 let suggested_workers = cpus.saturating_sub(1).max(1);
31
32 RuntimeConfig {
33 available_cpus: cpus,
34 suggested_workers,
35 stack_size: 8 * 1024 * 1024, }
37}
38
39#[derive(Debug, Clone)]
40pub struct RuntimeConfig {
41 pub available_cpus: usize,
42 pub suggested_workers: usize,
43 pub stack_size: usize,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum ServerTransport {
48 Grpc,
49 Http,
50 Wire,
51}
52
53impl ServerTransport {
54 pub const fn as_str(self) -> &'static str {
55 match self {
56 Self::Grpc => "gRPC",
57 Self::Http => "HTTP",
58 Self::Wire => "wire",
59 }
60 }
61
62 pub const fn default_bind_addr(self) -> &'static str {
63 match self {
64 Self::Grpc => "127.0.0.1:55055",
65 Self::Http => "127.0.0.1:5000",
66 Self::Wire => "127.0.0.1:5050",
67 }
68 }
69}
70
71#[derive(Debug, Clone)]
72pub struct ServerCommandConfig {
73 pub path: Option<PathBuf>,
74 pub router_bind_addr: Option<String>,
75 pub router_bind_explicit: bool,
76 pub grpc_bind_addr: Option<String>,
77 pub grpc_bind_explicit: bool,
78 pub grpc_tls_bind_addr: Option<String>,
82 pub grpc_tls_cert: Option<PathBuf>,
88 pub grpc_tls_key: Option<PathBuf>,
91 pub grpc_tls_client_ca: Option<PathBuf>,
96 pub http_bind_addr: Option<String>,
97 pub http_bind_explicit: bool,
98 pub http_tls_bind_addr: Option<String>,
102 pub http_tls_cert: Option<PathBuf>,
105 pub http_tls_key: Option<PathBuf>,
108 pub http_tls_client_ca: Option<PathBuf>,
112 pub wire_bind_addr: Option<String>,
113 pub wire_bind_explicit: bool,
114 pub wire_tls_bind_addr: Option<String>,
116 pub wire_tls_cert: Option<PathBuf>,
118 pub wire_tls_key: Option<PathBuf>,
120 pub pg_bind_addr: Option<String>,
124 pub create_if_missing: bool,
125 pub read_only: bool,
126 pub role: String,
127 pub primary_addr: Option<String>,
128 pub storage_profile: StorageProfileSelection,
129 pub auth: bool,
132 pub require_auth: bool,
134 pub vault: bool,
135 pub no_auth: bool,
146 pub workers: Option<usize>,
148 pub telemetry: Option<crate::telemetry::TelemetryConfig>,
151 pub http_limits_cli: crate::server::HttpLimitsCliInput,
156 pub ui: bool,
161 pub ui_dir: Option<PathBuf>,
165 pub bootstrap: BootstrapConfig,
168}
169
170#[derive(Debug, Clone, Default)]
171pub struct BootstrapConfig {
172 pub preset: Option<String>,
173 pub manifest: Option<PathBuf>,
174 pub admin_username: Option<String>,
175 pub admin_password: Option<String>,
176 pub cloud_head_admin: Option<String>,
177 pub cloud_head_admin_password: Option<String>,
178 pub customer_admin: Option<String>,
179 pub customer_admin_password: Option<String>,
180 pub cert_out: Option<PathBuf>,
189}
190
191#[derive(Debug, Clone)]
192pub struct SystemdServiceConfig {
193 pub service_name: String,
194 pub binary_path: PathBuf,
195 pub run_user: String,
196 pub run_group: String,
197 pub data_path: PathBuf,
198 pub router_bind_addr: Option<String>,
199 pub grpc_bind_addr: Option<String>,
200 pub http_bind_addr: Option<String>,
201}
202
203impl SystemdServiceConfig {
204 pub fn data_dir(&self) -> PathBuf {
205 self.data_path
206 .parent()
207 .map(PathBuf::from)
208 .unwrap_or_else(|| PathBuf::from("."))
209 }
210
211 pub fn unit_path(&self) -> PathBuf {
212 PathBuf::from(format!("/etc/systemd/system/{}.service", self.service_name))
213 }
214}
215
216pub fn default_telemetry_for_path(
221 path: Option<&std::path::Path>,
222) -> crate::telemetry::TelemetryConfig {
223 let log_dir = match path {
224 Some(p) => p
225 .parent()
226 .map(|parent| parent.join("logs"))
227 .or_else(|| Some(std::path::PathBuf::from("./logs"))),
228 None => None, };
230 crate::telemetry::TelemetryConfig {
231 log_dir,
232 file_prefix: "reddb.log".to_string(),
233 level_filter: std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()),
234 format: if std::io::IsTerminal::is_terminal(&std::io::stderr()) {
235 crate::telemetry::LogFormat::Pretty
236 } else {
237 crate::telemetry::LogFormat::Json
238 },
239 rotation_keep_days: 14,
240 service_name: "reddb",
241 level_explicit: false,
243 format_explicit: false,
244 rotation_keep_days_explicit: false,
245 file_prefix_explicit: false,
246 log_dir_explicit: false,
247 log_file_disabled: false,
248 }
249}
250
251const BACKUP_INTERVAL_META_CHECKPOINT: &str = "red.boot.backup.checkpoint_interval_secs";
258const BACKUP_INTERVAL_META_WAL_FLUSH: &str = "red.boot.backup.wal_flush_interval_secs";
259const BACKUP_KIND_META: &str = "red.boot.backup.backend_kind";
260const BACKUP_PAUSE_ON_LAG_META: &str = "red.boot.backup.pause_on_lag_secs";
264
265pub(crate) const NO_AUTH_META: &str = "red.boot.no_auth";
273
274pub(crate) fn no_auth_active(options: &RedDBOptions) -> bool {
277 options
278 .metadata
279 .get(NO_AUTH_META)
280 .map(|v| v == "true")
281 .unwrap_or(false)
282}
283
284const NO_AUTH_WARNING: &str =
289 "⚠ auth disabled (--no-auth) — anonymous access, do NOT use in production";
290
291impl ServerCommandConfig {
292 fn to_db_options(&self) -> Result<RedDBOptions, String> {
293 let mut options = match &self.path {
294 Some(path) => RedDBOptions::persistent(path),
295 None => RedDBOptions::in_memory(),
296 };
297
298 options.mode = StorageMode::Persistent;
299 options.create_if_missing = self.create_if_missing;
300 options.read_only = self.read_only
307 || env_nonempty("RED_READONLY")
308 .or_else(|| env_nonempty("REDDB_READONLY"))
309 .map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on"))
310 .unwrap_or(false)
311 || self.path.as_ref().is_some_and(|data_path| {
312 crate::server::handlers_admin::load_runtime_readonly(std::path::Path::new(
313 data_path,
314 ))
315 .unwrap_or(false)
316 });
317
318 options.replication = match self.role.as_str() {
319 "primary" => ReplicationConfig::primary(),
320 "replica" => {
321 let primary_addr = self
322 .primary_addr
323 .clone()
324 .unwrap_or_else(|| "http://127.0.0.1:55055".to_string());
325 ReplicationConfig::replica(primary_addr)
332 }
333 _ => ReplicationConfig::standalone(),
334 };
335 options.storage_profile = self.storage_profile.validate()?;
336
337 let no_auth = self.no_auth || env_truthy("REDDB_NO_AUTH") || env_truthy("REDDB_DEV");
338 let preset = self.bootstrap.resolved_preset_for_options()?;
339 let preset_requires_auth = matches!(preset.as_str(), PRESET_PRODUCTION | PRESET_CLOUD);
340
341 if self.auth || env_truthy("REDDB_AUTH") || preset_requires_auth {
342 options.auth.enabled = true;
343 }
344 if self.require_auth || env_truthy("REDDB_REQUIRE_AUTH") || preset_requires_auth {
345 options.auth.enabled = true;
346 options.auth.require_auth = true;
347 }
348 if self.vault || env_truthy("REDDB_VAULT") || preset_requires_auth {
349 options.auth.vault_enabled = true;
350 }
351
352 if no_auth {
363 options.auth.enabled = false;
364 options.auth.require_auth = false;
365 options.auth.vault_enabled = false;
366 options
367 .metadata
368 .insert(NO_AUTH_META.to_string(), "true".to_string());
369 }
370
371 let bootstrap_intent_present = !matches!(
391 self.bootstrap.auth_bootstrap_input(),
392 crate::cluster::AuthBootstrapInput::None
393 );
394 let path_absent = self.path.as_ref().is_some_and(|path| !path.exists());
395 let embedded_single_file = options.storage_profile.deploy_profile
398 == crate::storage::DeployProfile::Embedded
399 && options.storage_profile.packaging == crate::storage::StoragePackaging::SingleFile;
400 if options.auth.vault_enabled
401 && bootstrap_intent_present
402 && path_absent
403 && embedded_single_file
404 {
405 options.storage_profile.packaging =
406 crate::storage::StoragePackaging::OperationalDirectory;
407 options.create_if_missing = true;
408 tracing::info!(
409 target: "reddb::bootstrap",
410 "first-boot self-bootstrap: creating paged vault in place for a fresh database with a bootstrap intent"
411 );
412 }
413
414 if let Some(raw) = env_nonempty("REDDB_COMPLIANCE_MODE") {
419 options.control_events.compliance_mode = matches!(
420 raw.to_ascii_lowercase().as_str(),
421 "1" | "true" | "yes" | "on"
422 );
423 }
424 if preset == PRESET_REGULATED {
425 options.control_events.compliance_mode = true;
426 options.query_audit = crate::runtime::query_audit::QueryAuditConfig::regulated();
427 }
428
429 match crate::backup_bootstrap::from_env(|k| std::env::var(k).ok()) {
434 Err(msg) => {
435 return Err(format!("backup bootstrap: {msg}"));
436 }
437 Ok(Some(cfg)) => {
438 apply_backup_config(&mut options, &cfg);
439 }
440 Ok(None) => {
441 configure_remote_backend_from_env(&mut options);
442 }
443 }
444
445 if options.remote_backend.is_some()
446 || options
447 .metadata
448 .contains_key(BACKUP_INTERVAL_META_CHECKPOINT)
449 {
450 let mut selection = options.storage_profile;
451 selection.managed_backup = true;
452 options.storage_profile = selection.validate()?;
453 }
454
455 Ok(options)
456 }
457
458 pub fn enabled_transports(&self) -> Vec<ServerTransport> {
459 let mut transports = Vec::with_capacity(3);
460 if self.router_bind_addr.is_some() || self.grpc_bind_addr.is_some() {
461 transports.push(ServerTransport::Grpc);
462 }
463 if self.router_bind_addr.is_some() || self.http_bind_addr.is_some() {
464 transports.push(ServerTransport::Http);
465 }
466 if self.router_bind_addr.is_some() || self.wire_bind_addr.is_some() {
467 transports.push(ServerTransport::Wire);
468 }
469 transports
470 }
471}
472
473fn env_nonempty(name: &str) -> Option<String> {
482 if let Some(value) = crate::utils::env_with_file_fallback(name) {
483 let trimmed = value.trim();
484 if !trimmed.is_empty() {
485 return Some(trimmed.to_string());
486 }
487 return None;
488 }
489 env_nonempty_plain(name)
490}
491
492fn env_nonempty_plain(name: &str) -> Option<String> {
493 crate::utils::env_with_file_fallback(name)
494}
495
496fn env_truthy(name: &str) -> bool {
497 env_nonempty(name)
498 .map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on"))
499 .unwrap_or(false)
500}
501
502impl BootstrapConfig {
503 fn resolved_preset(&self) -> String {
504 self.preset
505 .as_deref()
506 .map(str::trim)
507 .filter(|s| !s.is_empty())
508 .map(str::to_string)
509 .or_else(|| env_nonempty(BOOTSTRAP_PRESET_ENV).or_else(|| env_nonempty(PRESET_ENV)))
510 .unwrap_or_else(|| PRESET_SIMPLE.to_string())
511 }
512
513 fn resolved_preset_for_options(&self) -> Result<String, String> {
514 if let Some(path) = self.selected_manifest() {
515 if let Some(manifest_bootstrap) =
516 crate::cli::bootstrap_manifest::bootstrap_config_from_manifest_file(&path)?
517 {
518 return Ok(manifest_bootstrap.resolved_preset());
519 }
520 }
521 Ok(self.resolved_preset())
522 }
523
524 fn selected_manifest(&self) -> Option<PathBuf> {
525 self.manifest.clone().or_else(|| {
526 env_nonempty(crate::cli::bootstrap_manifest::MANIFEST_ENV).map(PathBuf::from)
527 })
528 }
529
530 fn selected_cert_out(&self) -> Option<PathBuf> {
535 self.cert_out
536 .clone()
537 .or_else(|| env_nonempty(BOOTSTRAP_CERT_OUT_ENV).map(PathBuf::from))
538 }
539
540 fn auth_bootstrap_input(&self) -> crate::cluster::AuthBootstrapInput {
544 use crate::cluster::AuthBootstrapInput;
545 if self.selected_manifest().is_some() {
546 return AuthBootstrapInput::Manifest;
547 }
548 let preset = self.resolved_preset();
549 let preset_creates_auth = matches!(
550 preset.as_str(),
551 PRESET_PRODUCTION | PRESET_CLOUD | PRESET_REGULATED
552 );
553 let credentials_present =
558 self.production_username().is_some() && self.production_password().is_some();
559 if preset_creates_auth || credentials_present {
560 AuthBootstrapInput::Env
561 } else {
562 AuthBootstrapInput::None
563 }
564 }
565
566 fn explicit_value(value: &Option<String>) -> Option<String> {
567 value
568 .as_deref()
569 .map(str::trim)
570 .filter(|s| !s.is_empty())
571 .map(str::to_string)
572 }
573
574 fn value_or_env(value: &Option<String>, env: &str) -> Option<String> {
575 Self::explicit_value(value).or_else(|| env_nonempty(env))
576 }
577
578 fn production_username(&self) -> Option<String> {
579 Self::value_or_env(&self.admin_username, "REDDB_USERNAME")
580 }
581
582 fn production_password(&self) -> Option<String> {
583 Self::value_or_env(&self.admin_password, "REDDB_PASSWORD")
584 }
585
586 fn cloud_head_username(&self) -> Option<String> {
587 Self::explicit_value(&self.cloud_head_admin)
588 .or_else(|| Self::explicit_value(&self.admin_username))
589 .or_else(|| env_nonempty("REDDB_CLOUD_HEAD_ADMIN"))
590 .or_else(|| env_nonempty("REDDB_USERNAME"))
591 }
592
593 fn cloud_head_password(&self) -> Option<String> {
594 Self::explicit_value(&self.cloud_head_admin_password)
595 .or_else(|| Self::explicit_value(&self.admin_password))
596 .or_else(|| env_nonempty("REDDB_CLOUD_HEAD_ADMIN_PASSWORD"))
597 .or_else(|| env_nonempty("REDDB_PASSWORD"))
598 }
599
600 fn customer_username(&self) -> Option<String> {
601 Self::value_or_env(&self.customer_admin, "REDDB_CUSTOMER_ADMIN")
602 }
603
604 fn customer_password(&self) -> Option<String> {
605 Self::value_or_env(
606 &self.customer_admin_password,
607 "REDDB_CUSTOMER_ADMIN_PASSWORD",
608 )
609 }
610}
611
612fn apply_backup_config(options: &mut RedDBOptions, cfg: &crate::backup_bootstrap::BackupConfig) {
618 let endpoint_host = endpoint_host(&cfg.endpoint);
619
620 options.metadata.insert(
621 BACKUP_INTERVAL_META_CHECKPOINT.to_string(),
622 cfg.checkpoint_interval_secs.to_string(),
623 );
624 options.metadata.insert(
625 BACKUP_INTERVAL_META_WAL_FLUSH.to_string(),
626 cfg.wal_flush_interval_secs.to_string(),
627 );
628 options
629 .metadata
630 .insert(BACKUP_KIND_META.to_string(), "s3".to_string());
631 options.metadata.insert(
632 BACKUP_PAUSE_ON_LAG_META.to_string(),
633 cfg.pause_on_lag_secs.to_string(),
634 );
635
636 #[cfg(feature = "backend-s3")]
637 {
638 let s3_cfg = crate::storage::backend::S3Config {
639 endpoint: cfg.endpoint.clone(),
640 bucket: cfg.bucket.clone(),
641 key_prefix: cfg.prefix.clone(),
642 access_key: cfg.access_key_id.clone(),
643 secret_key: cfg.secret_access_key.clone(),
644 region: cfg.region.clone(),
645 path_style: true,
646 };
647 let backend = Arc::new(crate::storage::backend::S3Backend::new(s3_cfg));
648 options.remote_backend = Some(backend.clone());
649 options.remote_backend_atomic = Some(backend);
650 let trimmed = cfg.prefix.trim_end_matches('/');
655 options.remote_key = Some(reddb_file::remote_database_key(trimmed));
656
657 tracing::info!(
658 backend = "s3",
659 endpoint = %endpoint_host,
660 bucket = %cfg.bucket,
661 prefix = %cfg.prefix,
662 checkpoint_interval_secs = cfg.checkpoint_interval_secs,
663 wal_flush_interval_secs = cfg.wal_flush_interval_secs,
664 "backup backend configured from REDDB_BACKUP_* env"
665 );
666 }
667
668 #[cfg(not(feature = "backend-s3"))]
669 {
670 tracing::warn!(
671 backend = "s3",
672 endpoint = %endpoint_host,
673 bucket = %cfg.bucket,
674 prefix = %cfg.prefix,
675 "REDDB_BACKUP_S3_* configured but binary built without `backend-s3` feature; \
676 backend wiring skipped (archiver/checkpointer also disabled)"
677 );
678 }
679}
680
681fn endpoint_host(endpoint: &str) -> &str {
682 let after_scheme = endpoint
683 .split_once("://")
684 .map(|(_, r)| r)
685 .unwrap_or(endpoint);
686 after_scheme.split('/').next().unwrap_or(after_scheme)
687}
688
689fn spawn_backup_tasks_if_configured(
695 options: &RedDBOptions,
696 runtime: &RedDBRuntime,
697) -> Option<BackupTasksHandle> {
698 let checkpoint_secs: u64 = options
699 .metadata
700 .get(BACKUP_INTERVAL_META_CHECKPOINT)?
701 .parse()
702 .ok()?;
703 let wal_secs: u64 = options
704 .metadata
705 .get(BACKUP_INTERVAL_META_WAL_FLUSH)?
706 .parse()
707 .ok()?;
708 let pause_on_lag_secs: u64 = options
711 .metadata
712 .get(BACKUP_PAUSE_ON_LAG_META)
713 .and_then(|raw| raw.parse().ok())
714 .unwrap_or(0);
715 options.remote_backend.as_ref()?;
716
717 let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
718
719 if pause_on_lag_secs > 0 {
724 let now_ms = std::time::SystemTime::now()
725 .duration_since(std::time::UNIX_EPOCH)
726 .map(|d| d.as_millis() as u64)
727 .unwrap_or(0);
728 runtime
729 .write_gate()
730 .configure_archive_lag_pause(pause_on_lag_secs, now_ms);
731 tracing::info!(
732 pause_on_lag_secs,
733 "archive-lag pause enabled — engine will transition to read-only after threshold seconds of archiver silence"
734 );
735 }
736
737 let checkpoint_handle = {
738 let stop = Arc::clone(&stop);
739 let runtime = runtime.clone();
740 let interval = Duration::from_secs(checkpoint_secs);
741 thread::Builder::new()
742 .name("red-checkpointer".into())
743 .spawn(move || {
744 periodic_loop(stop, interval, move || {
745 if let Err(err) = runtime.checkpoint() {
746 tracing::warn!(error = %err, "periodic checkpoint failed");
747 }
748 })
749 })
750 .ok()
751 };
752
753 let archiver_handle = {
754 let stop = Arc::clone(&stop);
755 let runtime = runtime.clone();
756 let interval = Duration::from_secs(wal_secs);
757 let lag_enabled = pause_on_lag_secs > 0;
758 thread::Builder::new()
759 .name("red-wal-archiver".into())
760 .spawn(move || {
761 periodic_loop(stop, interval, move || match runtime.trigger_backup() {
762 Ok(_) if lag_enabled => {
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.write_gate().record_archive_success(now_ms);
768 runtime.write_gate().evaluate_archive_lag(now_ms);
772 }
773 Ok(_) => {}
774 Err(err) => {
775 tracing::warn!(error = %err, "periodic WAL archive/backup failed");
776 }
777 })
778 })
779 .ok()
780 };
781
782 let lag_monitor_handle = if pause_on_lag_secs > 0 {
787 let stop = Arc::clone(&stop);
788 let runtime = runtime.clone();
789 let interval = Duration::from_secs(5);
793 thread::Builder::new()
794 .name("red-archive-lag-monitor".into())
795 .spawn(move || {
796 periodic_loop(stop, interval, move || {
797 let now_ms = std::time::SystemTime::now()
798 .duration_since(std::time::UNIX_EPOCH)
799 .map(|d| d.as_millis() as u64)
800 .unwrap_or(0);
801 let was_paused = runtime.write_gate().is_auto_paused();
802 let now_paused = runtime.write_gate().evaluate_archive_lag(now_ms);
803 if now_paused && !was_paused {
804 tracing::warn!(
805 pause_on_lag_secs,
806 last_archive_at_ms = runtime.write_gate().last_archive_at_ms(),
807 "WAL archive lag exceeded threshold — entering graceful read-only mode (issue #519)"
808 );
809 } else if !now_paused && was_paused {
810 tracing::info!(
811 "WAL archive caught up — exiting graceful read-only mode (issue #519)"
812 );
813 }
814 })
815 })
816 .ok()
817 } else {
818 None
819 };
820
821 tracing::info!(
822 checkpoint_interval_secs = checkpoint_secs,
823 wal_flush_interval_secs = wal_secs,
824 "backup tasks spawned (checkpointer + WAL archiver)"
825 );
826
827 Some(BackupTasksHandle {
828 stop,
829 _checkpoint_handle: checkpoint_handle,
830 _archiver_handle: archiver_handle,
831 _lag_monitor_handle: lag_monitor_handle,
832 })
833}
834
835pub struct BackupTasksHandle {
838 stop: Arc<std::sync::atomic::AtomicBool>,
839 _checkpoint_handle: Option<thread::JoinHandle<()>>,
840 _archiver_handle: Option<thread::JoinHandle<()>>,
841 _lag_monitor_handle: Option<thread::JoinHandle<()>>,
844}
845
846impl Drop for BackupTasksHandle {
847 fn drop(&mut self) {
848 self.stop.store(true, std::sync::atomic::Ordering::Release);
849 }
850}
851
852fn periodic_loop<F: FnMut()>(
853 stop: Arc<std::sync::atomic::AtomicBool>,
854 interval: Duration,
855 mut tick: F,
856) {
857 let wake = Duration::from_secs(1);
860 let mut elapsed = Duration::ZERO;
861 while !stop.load(std::sync::atomic::Ordering::Acquire) {
862 thread::sleep(wake);
863 elapsed += wake;
864 if elapsed >= interval {
865 tick();
866 elapsed = Duration::ZERO;
867 }
868 }
869}
870
871fn configure_remote_backend_from_env(options: &mut RedDBOptions) {
872 let backend = env_nonempty("RED_BACKEND")
878 .or_else(|| env_nonempty("REDDB_REMOTE_BACKEND"))
879 .unwrap_or_else(|| "none".to_string())
880 .to_ascii_lowercase();
881
882 match backend.as_str() {
883 "s3" | "minio" | "r2" => {
888 #[cfg(feature = "backend-s3")]
889 {
890 if let Some(config) = s3_config_from_env() {
891 let remote_key = env_nonempty("RED_REMOTE_KEY")
892 .or_else(|| env_nonempty("REDDB_REMOTE_KEY"))
893 .unwrap_or_else(|| reddb_file::remote_database_key("clusters/dev"));
894 let backend = Arc::new(crate::storage::backend::S3Backend::new(config));
895 options.remote_backend = Some(backend.clone());
896 options.remote_backend_atomic = Some(backend);
897 options.remote_key = Some(remote_key);
898 }
899 }
900 #[cfg(not(feature = "backend-s3"))]
901 {
902 tracing::warn!(
903 backend = %backend,
904 "RED_BACKEND={backend} requested but binary was built without `backend-s3` feature"
905 );
906 }
907 }
908 "fs" | "local" => {
913 let base_path = env_nonempty("RED_FS_PATH").or_else(|| env_nonempty("REDDB_FS_PATH"));
914 let remote_key = match (
915 base_path,
916 env_nonempty("RED_REMOTE_KEY").or_else(|| env_nonempty("REDDB_REMOTE_KEY")),
917 ) {
918 (Some(base), Some(rel)) => Some(format!(
919 "{}/{}",
920 base.trim_end_matches('/'),
921 rel.trim_start_matches('/')
922 )),
923 (Some(base), None) => Some(reddb_file::remote_database_key(&format!(
924 "{}/clusters/dev",
925 base.trim_end_matches('/')
926 ))),
927 (None, Some(rel)) => Some(rel),
928 (None, None) => None,
929 };
930 if let Some(key) = remote_key {
931 let backend = Arc::new(crate::storage::backend::LocalBackend);
932 options.remote_backend = Some(backend.clone());
933 options.remote_backend_atomic = Some(backend);
934 options.remote_key = Some(key);
935 }
936 }
937 "http" => {
942 let base_url = match env_nonempty("RED_HTTP_BACKEND_URL")
943 .or_else(|| env_nonempty("REDDB_HTTP_BACKEND_URL"))
944 {
945 Some(u) => u,
946 None => {
947 tracing::warn!(
948 "RED_BACKEND=http requires RED_HTTP_BACKEND_URL — backend disabled"
949 );
950 return;
951 }
952 };
953 let prefix = env_nonempty("RED_HTTP_BACKEND_PREFIX")
954 .or_else(|| env_nonempty("REDDB_HTTP_BACKEND_PREFIX"))
955 .unwrap_or_default();
956 let auth_header = if let Some(path) = env_nonempty("RED_HTTP_BACKEND_AUTH_HEADER_FILE")
957 .or_else(|| env_nonempty("REDDB_HTTP_BACKEND_AUTH_HEADER_FILE"))
958 {
959 std::fs::read_to_string(&path)
960 .ok()
961 .map(|s| s.trim().to_string())
962 .filter(|s| !s.is_empty())
963 } else {
964 env_nonempty("RED_HTTP_BACKEND_AUTH_HEADER")
965 .or_else(|| env_nonempty("REDDB_HTTP_BACKEND_AUTH_HEADER"))
966 };
967
968 let mut config =
969 crate::storage::backend::HttpBackendConfig::new(base_url).with_prefix(prefix);
970 if let Some(auth) = auth_header {
971 config = config.with_auth_header(auth);
972 }
973 let conditional_writes = env_truthy("RED_HTTP_CONDITIONAL_WRITES")
974 || env_truthy("RED_HTTP_BACKEND_CONDITIONAL_WRITES")
975 || env_truthy("REDDB_HTTP_BACKEND_CONDITIONAL_WRITES");
976 config = config.with_conditional_writes(conditional_writes);
977 if conditional_writes {
982 match crate::storage::backend::AtomicHttpBackend::try_new(config.clone()) {
983 Ok(atomic) => {
984 let atomic_arc = Arc::new(atomic);
985 options.remote_backend = Some(atomic_arc.clone());
986 options.remote_backend_atomic = Some(atomic_arc);
987 }
988 Err(err) => {
989 tracing::warn!(error = %err, "AtomicHttpBackend init failed; falling back to plain HTTP (no CAS)");
990 options.remote_backend =
991 Some(Arc::new(crate::storage::backend::HttpBackend::new(config)));
992 }
993 }
994 } else {
995 options.remote_backend =
996 Some(Arc::new(crate::storage::backend::HttpBackend::new(config)));
997 }
998 options.remote_key = env_nonempty("RED_REMOTE_KEY")
999 .or_else(|| env_nonempty("REDDB_REMOTE_KEY"))
1000 .or_else(|| Some(reddb_file::remote_database_key("clusters/dev")));
1001 }
1002 "none" | "" => {}
1005 other => {
1006 tracing::warn!(
1007 backend = %other,
1008 "unknown RED_BACKEND value — supported: s3 | fs | http | none"
1009 );
1010 }
1011 }
1012}
1013
1014#[cfg(feature = "backend-s3")]
1019fn env_s3(suffix: &str) -> Option<String> {
1020 env_nonempty(&format!("RED_S3_{suffix}"))
1021 .or_else(|| env_nonempty(&format!("REDDB_S3_{suffix}")))
1022}
1023
1024#[cfg(feature = "backend-s3")]
1030fn env_s3_secret(suffix: &str) -> Option<String> {
1031 let file_key_red = format!("RED_S3_{suffix}_FILE");
1032 let file_key_legacy = format!("REDDB_S3_{suffix}_FILE");
1033 if let Some(path) = env_nonempty(&file_key_red).or_else(|| env_nonempty(&file_key_legacy)) {
1034 return std::fs::read_to_string(&path)
1035 .ok()
1036 .map(|s| s.trim().to_string())
1037 .filter(|s| !s.is_empty());
1038 }
1039 env_s3(suffix)
1040}
1041
1042#[cfg(feature = "backend-s3")]
1043fn s3_config_from_env() -> Option<crate::storage::backend::S3Config> {
1044 let endpoint = env_s3("ENDPOINT")?;
1045 let bucket = env_s3("BUCKET")?;
1046 let access_key = env_s3_secret("ACCESS_KEY")?;
1047 let secret_key = env_s3_secret("SECRET_KEY")?;
1048 let region = env_s3("REGION").unwrap_or_else(|| "us-east-1".to_string());
1049 let key_prefix = env_s3("KEY_PREFIX")
1050 .or_else(|| env_s3("PREFIX"))
1051 .unwrap_or_default();
1052 let path_style = env_s3("PATH_STYLE")
1053 .map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on"))
1054 .unwrap_or(true);
1055 Some(crate::storage::backend::S3Config {
1056 endpoint,
1057 bucket,
1058 key_prefix,
1059 access_key,
1060 secret_key,
1061 region,
1062 path_style,
1063 })
1064}
1065
1066pub fn render_systemd_unit(config: &SystemdServiceConfig) -> String {
1067 let data_dir = config.data_dir();
1068 let exec_start = render_systemd_exec_start(config);
1069 format!(
1070 "[Unit]\n\
1071Description=RedDB unified database service\n\
1072After=network-online.target\n\
1073Wants=network-online.target\n\
1074\n\
1075[Service]\n\
1076Type=simple\n\
1077User={user}\n\
1078Group={group}\n\
1079WorkingDirectory={workdir}\n\
1080ExecStart={exec_start}\n\
1081Restart=always\n\
1082RestartSec=2\n\
1083LimitSTACK=16M\n\
1084NoNewPrivileges=true\n\
1085PrivateTmp=true\n\
1086ProtectSystem=strict\n\
1087ProtectHome=true\n\
1088ProtectControlGroups=true\n\
1089ProtectKernelTunables=true\n\
1090ProtectKernelModules=true\n\
1091RestrictNamespaces=true\n\
1092LockPersonality=true\n\
1093MemoryDenyWriteExecute=true\n\
1094ReadWritePaths={workdir}\n\
1095\n\
1096[Install]\n\
1097WantedBy=multi-user.target\n",
1098 user = config.run_user,
1099 group = config.run_group,
1100 workdir = data_dir.display(),
1101 exec_start = exec_start,
1102 )
1103}
1104
1105#[cfg(target_os = "linux")]
1114pub fn install_systemd_service(config: &SystemdServiceConfig) -> Result<(), String> {
1115 ensure_root()?;
1116 ensure_command_available("systemctl")?;
1117 ensure_command_available("getent")?;
1118 ensure_command_available("groupadd")?;
1119 ensure_command_available("useradd")?;
1120 ensure_command_available("install")?;
1121 ensure_executable(&config.binary_path)?;
1122
1123 if !command_success("getent", ["group", config.run_group.as_str()])? {
1124 run_command("groupadd", ["--system", config.run_group.as_str()])?;
1125 }
1126
1127 if !command_success("id", ["-u", config.run_user.as_str()])? {
1128 let data_dir = config.data_dir();
1129 run_command(
1130 "useradd",
1131 [
1132 "--system",
1133 "--gid",
1134 config.run_group.as_str(),
1135 "--home-dir",
1136 data_dir.to_string_lossy().as_ref(),
1137 "--shell",
1138 "/usr/sbin/nologin",
1139 config.run_user.as_str(),
1140 ],
1141 )?;
1142 }
1143
1144 let data_dir = config.data_dir();
1145 run_command(
1146 "install",
1147 [
1148 "-d",
1149 "-o",
1150 config.run_user.as_str(),
1151 "-g",
1152 config.run_group.as_str(),
1153 "-m",
1154 "0750",
1155 data_dir.to_string_lossy().as_ref(),
1156 ],
1157 )?;
1158
1159 std::fs::write(config.unit_path(), render_systemd_unit(config))
1160 .map_err(|err| format!("failed to write systemd unit: {err}"))?;
1161
1162 run_command("systemctl", ["daemon-reload"])?;
1163 run_command(
1164 "systemctl",
1165 [
1166 "enable",
1167 "--now",
1168 format!("{}.service", config.service_name).as_str(),
1169 ],
1170 )?;
1171
1172 Ok(())
1173}
1174
1175#[cfg(not(target_os = "linux"))]
1180pub fn install_systemd_service(_config: &SystemdServiceConfig) -> Result<(), String> {
1181 Err("systemd install is Linux-only — use sc.exe (Windows) or \
1182 launchd (macOS) to install the service manually using the \
1183 unit printed by `red service print-unit`"
1184 .to_string())
1185}
1186
1187#[cfg(target_os = "linux")]
1188fn ensure_root() -> Result<(), String> {
1189 let output = Command::new("id")
1190 .arg("-u")
1191 .output()
1192 .map_err(|err| format!("failed to determine current uid: {err}"))?;
1193 if !output.status.success() {
1194 return Err("failed to determine current uid".to_string());
1195 }
1196 let uid = String::from_utf8_lossy(&output.stdout);
1197 if uid.trim() != "0" {
1198 return Err("run this command as root (sudo)".to_string());
1199 }
1200 Ok(())
1201}
1202
1203#[cfg(target_os = "linux")]
1204fn ensure_command_available(command: &str) -> Result<(), String> {
1205 let status = Command::new("sh")
1206 .args(["-lc", &format!("command -v {command} >/dev/null 2>&1")])
1207 .status()
1208 .map_err(|err| format!("failed to check command '{command}': {err}"))?;
1209 if status.success() {
1210 Ok(())
1211 } else {
1212 Err(format!("required command not found: {command}"))
1213 }
1214}
1215
1216#[cfg(target_os = "linux")]
1217fn ensure_executable(path: &std::path::Path) -> Result<(), String> {
1218 let metadata = std::fs::metadata(path)
1219 .map_err(|err| format!("binary not found '{}': {err}", path.display()))?;
1220 #[cfg(unix)]
1221 {
1222 use std::os::unix::fs::PermissionsExt;
1223 if metadata.permissions().mode() & 0o111 == 0 {
1224 return Err(format!("binary is not executable: {}", path.display()));
1225 }
1226 }
1227 #[cfg(not(unix))]
1228 {
1229 if !metadata.is_file() {
1230 return Err(format!("binary is not a file: {}", path.display()));
1231 }
1232 }
1233 Ok(())
1234}
1235
1236#[cfg(target_os = "linux")]
1237fn command_success<const N: usize>(program: &str, args: [&str; N]) -> Result<bool, String> {
1238 Command::new(program)
1239 .args(args)
1240 .status()
1241 .map(|status| status.success())
1242 .map_err(|err| format!("failed to run {program}: {err}"))
1243}
1244
1245#[cfg(target_os = "linux")]
1246fn run_command<const N: usize>(program: &str, args: [&str; N]) -> Result<(), String> {
1247 let output = Command::new(program)
1248 .args(args)
1249 .output()
1250 .map_err(|err| format!("failed to run {program}: {err}"))?;
1251 if output.status.success() {
1252 return Ok(());
1253 }
1254
1255 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
1256 let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
1257 let detail = if !stderr.is_empty() {
1258 stderr
1259 } else if !stdout.is_empty() {
1260 stdout
1261 } else {
1262 format!("exit status {}", output.status)
1263 };
1264 Err(format!("{program} failed: {detail}"))
1265}
1266
1267fn render_systemd_exec_start(config: &SystemdServiceConfig) -> String {
1268 let mut parts = vec![
1269 config.binary_path.display().to_string(),
1270 "server".to_string(),
1271 "--path".to_string(),
1272 config.data_path.display().to_string(),
1273 ];
1274
1275 if let Some(bind_addr) = &config.router_bind_addr {
1276 parts.push("--bind".to_string());
1277 parts.push(bind_addr.clone());
1278 } else if let Some(bind_addr) = &config.grpc_bind_addr {
1279 parts.push("--grpc-bind".to_string());
1280 parts.push(bind_addr.clone());
1281 }
1282 if let Some(bind_addr) = &config.http_bind_addr {
1283 parts.push("--http-bind".to_string());
1284 parts.push(bind_addr.clone());
1285 }
1286
1287 parts.join(" ")
1288}
1289
1290pub fn probe_listener(target: &str, timeout: Duration) -> bool {
1291 let addresses: Vec<SocketAddr> = match target.to_socket_addrs() {
1292 Ok(addresses) => addresses.collect(),
1293 Err(_) => return false,
1294 };
1295
1296 addresses
1297 .into_iter()
1298 .any(|address| TcpStream::connect_timeout(&address, timeout).is_ok())
1299}
1300
1301pub fn bind_listener_for_startup(
1319 readiness: &mut TransportReadiness,
1320 transport: &str,
1321 bind_addr: &str,
1322 explicit: bool,
1323) -> Result<Option<TcpListener>, String> {
1324 match TcpListener::bind(bind_addr) {
1325 Ok(listener) => {
1326 readiness.active(transport, bind_addr, explicit);
1327 Ok(Some(listener))
1328 }
1329 Err(err) => {
1330 let reason = format!("{transport} listener bind {bind_addr}: {err}");
1331 readiness.failed(transport, bind_addr, explicit, reason.clone());
1332 if explicit {
1333 tracing::error!(
1334 transport,
1335 bind = %bind_addr,
1336 error = %err,
1337 "fatal explicit bind failure"
1338 );
1339 Err(format!("explicit {reason}"))
1340 } else {
1341 tracing::warn!(
1342 transport,
1343 bind = %bind_addr,
1344 error = %err,
1345 "non-fatal implicit bind failure; listener degraded"
1346 );
1347 Ok(None)
1348 }
1349 }
1350 }
1351}
1352
1353async fn spawn_lifecycle_signal_handler(runtime: RedDBRuntime) {
1376 let backup_on_shutdown = std::env::var("RED_BACKUP_ON_SHUTDOWN")
1377 .ok()
1378 .map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on"))
1379 .unwrap_or(true);
1380
1381 #[cfg(unix)]
1382 {
1383 use tokio::signal::unix::{signal, SignalKind};
1384
1385 let mut sigterm = match signal(SignalKind::terminate()) {
1386 Ok(s) => s,
1387 Err(err) => {
1388 tracing::warn!(
1389 error = %err,
1390 "could not install SIGTERM handler; orchestrator graceful shutdown will fall back to SIGKILL"
1391 );
1392 return;
1393 }
1394 };
1395 let mut sigint = match signal(SignalKind::interrupt()) {
1396 Ok(s) => s,
1397 Err(err) => {
1398 tracing::warn!(error = %err, "could not install SIGINT handler");
1399 return;
1400 }
1401 };
1402 let mut sighup = match signal(SignalKind::hangup()) {
1408 Ok(s) => Some(s),
1409 Err(err) => {
1410 tracing::warn!(error = %err, "could not install SIGHUP handler; secret reload via signal disabled");
1411 None
1412 }
1413 };
1414
1415 let reload_runtime = runtime.clone();
1416 tokio::spawn(async move {
1417 loop {
1418 let signal_name = match &mut sighup {
1419 Some(hup) => tokio::select! {
1420 _ = sigterm.recv() => "SIGTERM",
1421 _ = sigint.recv() => "SIGINT",
1422 _ = hup.recv() => "SIGHUP",
1423 },
1424 None => tokio::select! {
1425 _ = sigterm.recv() => "SIGTERM",
1426 _ = sigint.recv() => "SIGINT",
1427 },
1428 };
1429
1430 if signal_name == "SIGHUP" {
1431 handle_sighup_reload(&reload_runtime);
1432 continue; }
1434
1435 tracing::info!(
1436 signal = signal_name,
1437 "lifecycle signal received; shutting down"
1438 );
1439 match runtime.graceful_shutdown(backup_on_shutdown) {
1440 Ok(report) => {
1441 tracing::info!(
1442 duration_ms = report.duration_ms,
1443 flushed_wal = report.flushed_wal,
1444 final_checkpoint = report.final_checkpoint,
1445 backup_uploaded = report.backup_uploaded,
1446 "graceful shutdown complete"
1447 );
1448 }
1449 Err(err) => {
1450 tracing::error!(error = %err, "graceful shutdown failed");
1451 crate::telemetry::operator_event::OperatorEvent::ShutdownForced {
1457 reason: format!("graceful shutdown failed: {err}"),
1458 }
1459 .emit_global();
1460 }
1461 }
1462 std::process::exit(0);
1463 }
1464 });
1465 }
1466
1467 #[cfg(not(unix))]
1468 {
1469 tokio::spawn(async move {
1470 let interrupted = tokio::signal::ctrl_c().await;
1471 if let Err(err) = interrupted {
1472 tracing::warn!(error = %err, "could not install Ctrl+C handler");
1473 return;
1474 }
1475
1476 tracing::info!(
1477 signal = "Ctrl+C",
1478 "lifecycle signal received; shutting down"
1479 );
1480 match runtime.graceful_shutdown(backup_on_shutdown) {
1481 Ok(report) => {
1482 tracing::info!(
1483 duration_ms = report.duration_ms,
1484 flushed_wal = report.flushed_wal,
1485 final_checkpoint = report.final_checkpoint,
1486 backup_uploaded = report.backup_uploaded,
1487 "graceful shutdown complete"
1488 );
1489 }
1490 Err(err) => {
1491 tracing::error!(error = %err, "graceful shutdown failed");
1492 }
1493 }
1494 std::process::exit(0);
1495 });
1496 }
1497}
1498
1499fn handle_sighup_reload(runtime: &RedDBRuntime) {
1508 let now_ms = std::time::SystemTime::now()
1509 .duration_since(std::time::UNIX_EPOCH)
1510 .map(|d| d.as_millis() as u64)
1511 .unwrap_or(0);
1512 tracing::info!(
1513 target: "reddb::secrets",
1514 ts_unix_ms = now_ms,
1515 "SIGHUP received; secrets will be re-read from *_FILE on next access"
1516 );
1517 use crate::runtime::audit_log::{AuditAuthSource, AuditEvent, AuditFieldEscaper, Outcome};
1522 runtime.audit_log().record_event(
1523 AuditEvent::builder("config/sighup_reload")
1524 .source(AuditAuthSource::System)
1525 .resource("secrets")
1526 .outcome(Outcome::Success)
1527 .field(AuditFieldEscaper::field("ts_unix_ms", now_ms))
1528 .build(),
1529 );
1530}
1531
1532fn resolve_pg_wire_tls(config: &ServerCommandConfig) -> Option<crate::wire::WireTlsConfig> {
1543 let tls_configured = config.wire_tls_bind_addr.is_some()
1544 || (config.wire_tls_cert.is_some() && config.wire_tls_key.is_some());
1545 if !tls_configured {
1546 return None;
1547 }
1548 match resolve_wire_tls_config(config) {
1549 Ok(cfg) => Some(cfg),
1550 Err(e) => {
1551 tracing::error!(
1552 err = %e,
1553 "pg wire TLS config error; listener will run plaintext"
1554 );
1555 None
1556 }
1557 }
1558}
1559
1560fn resolve_grpc_tls_options(config: &ServerCommandConfig) -> Result<crate::GrpcTlsOptions, String> {
1574 let (cert_pem, key_pem) = match (&config.grpc_tls_cert, &config.grpc_tls_key) {
1575 (Some(cert), Some(key)) => {
1576 let cert_pem = std::fs::read(cert)
1577 .map_err(|e| format!("read grpc cert {}: {e}", cert.display()))?;
1578 let key_pem =
1579 std::fs::read(key).map_err(|e| format!("read grpc key {}: {e}", key.display()))?;
1580 (cert_pem, key_pem)
1581 }
1582 _ => {
1583 let dev = std::env::var("RED_GRPC_TLS_DEV")
1585 .ok()
1586 .map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on"))
1587 .unwrap_or(false);
1588 if !dev {
1589 return Err("gRPC TLS configured but no cert/key supplied — set \
1590 REDDB_GRPC_TLS_CERT / REDDB_GRPC_TLS_KEY (or \
1591 RED_GRPC_TLS_DEV=1 to auto-generate a self-signed cert)"
1592 .to_string());
1593 }
1594 let dir = config
1595 .path
1596 .as_ref()
1597 .and_then(|p| p.parent())
1598 .map(PathBuf::from)
1599 .unwrap_or_else(|| PathBuf::from("."));
1600 let (cert_pem_str, key_pem_str) =
1601 crate::wire::tls::generate_self_signed_cert("localhost")
1602 .map_err(|e| format!("auto-generate dev grpc cert: {e}"))?;
1603
1604 let fp = sha256_pem_fingerprint(cert_pem_str.as_bytes());
1609 tracing::warn!(
1610 target: "reddb::security",
1611 transport = "grpc",
1612 cert_sha256 = %fp,
1613 "RED_GRPC_TLS_DEV=1: using auto-generated self-signed cert; \
1614 DO NOT use in production"
1615 );
1616 let cert_path = dir.join("grpc-tls-cert.pem");
1618 let key_path = dir.join("grpc-tls-key.pem");
1619 if !cert_path.exists() || !key_path.exists() {
1620 let _ = std::fs::create_dir_all(&dir);
1621 std::fs::write(&cert_path, cert_pem_str.as_bytes())
1622 .map_err(|e| format!("write grpc dev cert: {e}"))?;
1623 std::fs::write(&key_path, key_pem_str.as_bytes())
1624 .map_err(|e| format!("write grpc dev key: {e}"))?;
1625 #[cfg(unix)]
1626 {
1627 use std::os::unix::fs::PermissionsExt;
1628 let _ =
1629 std::fs::set_permissions(&key_path, std::fs::Permissions::from_mode(0o600));
1630 }
1631 }
1632 (cert_pem_str.into_bytes(), key_pem_str.into_bytes())
1633 }
1634 };
1635
1636 let client_ca_pem = match &config.grpc_tls_client_ca {
1637 Some(path) => Some(
1638 std::fs::read(path)
1639 .map_err(|e| format!("read grpc client CA {}: {e}", path.display()))?,
1640 ),
1641 None => None,
1642 };
1643
1644 Ok(crate::GrpcTlsOptions {
1645 cert_pem,
1646 key_pem,
1647 client_ca_pem,
1648 })
1649}
1650
1651fn sha256_pem_fingerprint(pem: &[u8]) -> String {
1654 use sha2::{Digest, Sha256};
1655 let mut h = Sha256::new();
1656 h.update(pem);
1657 let d = h.finalize();
1658 let mut buf = String::with_capacity(64);
1659 for b in d.iter() {
1660 buf.push_str(&format!("{b:02x}"));
1661 }
1662 buf
1663}
1664
1665fn resolve_wire_tls_config(
1667 config: &ServerCommandConfig,
1668) -> Result<crate::wire::WireTlsConfig, String> {
1669 match (&config.wire_tls_cert, &config.wire_tls_key) {
1670 (Some(cert), Some(key)) => Ok(crate::wire::WireTlsConfig {
1671 cert_path: cert.clone(),
1672 key_path: key.clone(),
1673 }),
1674 _ => {
1675 let dir = config
1677 .path
1678 .as_ref()
1679 .and_then(|p| p.parent())
1680 .map(PathBuf::from)
1681 .unwrap_or_else(|| PathBuf::from("."));
1682 crate::wire::tls::auto_generate_cert(&dir).map_err(|e| e.to_string())
1683 }
1684 }
1685}
1686
1687#[inline(never)]
1688fn build_runtime_and_auth_store(
1689 config: &ServerCommandConfig,
1690 db_options: RedDBOptions,
1691) -> Result<
1692 (
1693 RedDBRuntime,
1694 Arc<AuthStore>,
1695 Option<crate::telemetry::TelemetryGuard>,
1696 ),
1697 String,
1698> {
1699 build_runtime_with_bootstrap(db_options, config.telemetry.clone(), &config.bootstrap)
1706}
1707
1708pub(crate) fn build_runtime_with_telemetry(
1718 db_options: RedDBOptions,
1719 cli_telemetry: Option<crate::telemetry::TelemetryConfig>,
1720) -> Result<
1721 (
1722 RedDBRuntime,
1723 Arc<AuthStore>,
1724 Option<crate::telemetry::TelemetryGuard>,
1725 ),
1726 String,
1727> {
1728 let bootstrap = BootstrapConfig::default();
1729 build_runtime_with_bootstrap(db_options, cli_telemetry, &bootstrap)
1730}
1731
1732fn build_runtime_with_bootstrap(
1733 db_options: RedDBOptions,
1734 cli_telemetry: Option<crate::telemetry::TelemetryConfig>,
1735 bootstrap: &BootstrapConfig,
1736) -> Result<
1737 (
1738 RedDBRuntime,
1739 Arc<AuthStore>,
1740 Option<crate::telemetry::TelemetryGuard>,
1741 ),
1742 String,
1743> {
1744 let runtime = RedDBRuntime::with_options(db_options.clone()).map_err(|err| {
1745 let msg = err.to_string();
1751 crate::telemetry::operator_event::OperatorEvent::StartupFailed {
1752 phase: "runtime_construction".to_string(),
1753 error: msg.clone(),
1754 }
1755 .emit_global();
1756 msg
1757 })?;
1758
1759 crate::runtime::lease_loop::start_lease_loop_if_required(&runtime).map_err(|err| {
1764 let msg = err.to_string();
1765 crate::telemetry::operator_event::OperatorEvent::StartupFailed {
1766 phase: "lease_loop".to_string(),
1767 error: msg.clone(),
1768 }
1769 .emit_global();
1770 msg
1771 })?;
1772
1773 if let Some(data_path) = db_options.data_path.as_deref() {
1777 let watch_dir = data_path.parent().unwrap_or(data_path);
1778 crate::runtime::disk_space_monitor::DiskSpaceMonitor::new(watch_dir, 90).spawn();
1779 }
1780
1781 {
1785 let config_path = crate::runtime::config_overlay::config_file_path();
1786 let store = runtime.db().store();
1787 crate::runtime::config_watcher::ConfigWatcher::new(config_path, store).spawn();
1788 }
1789
1790 let merged = merge_telemetry_with_config(
1793 cli_telemetry
1794 .unwrap_or_else(|| default_telemetry_for_path(db_options.data_path.as_deref())),
1795 &runtime,
1796 );
1797 let telemetry_guard = crate::telemetry::init(merged);
1798
1799 let no_auth = no_auth_active(&db_options);
1800 let auth_store =
1801 if db_options.auth.vault_enabled {
1802 let pager =
1803 runtime.db().store().pager().cloned().ok_or_else(|| {
1804 "vault requires a paged database (persistent mode)".to_string()
1805 })?;
1806 let store = AuthStore::with_vault(db_options.auth.clone(), pager)
1807 .map_err(|err| err.to_string())?;
1808 Arc::new(store)
1809 } else {
1810 Arc::new(AuthStore::new(db_options.auth.clone()))
1811 };
1812 auth_store.configure_control_events(
1813 runtime.control_event_ledger(),
1814 runtime.control_event_config(),
1815 );
1816 let bootstrap_already_completed = runtime
1833 .db()
1834 .store()
1835 .get_config(BOOTSTRAP_COMPLETED_KEY)
1836 .is_some();
1837 let disposition = crate::cluster::authorize_cluster_bootstrap(
1838 db_options.storage_profile.deploy_profile,
1839 no_auth,
1840 bootstrap.auth_bootstrap_input(),
1841 bootstrap_already_completed,
1842 )?;
1843 let vault_plan = crate::cluster::plan_vault_bootstrap(disposition);
1852 match disposition {
1853 crate::cluster::BootstrapDisposition::SkipDevBypass => {
1854 debug_assert_eq!(vault_plan, crate::cluster::VaultBootstrapPlan::SkipNoVault);
1855 eprintln!("{NO_AUTH_WARNING}");
1856 tracing::warn!("{NO_AUTH_WARNING}");
1857 }
1858 crate::cluster::BootstrapDisposition::AlreadyComplete => {
1859 debug_assert_eq!(
1866 vault_plan,
1867 crate::cluster::VaultBootstrapPlan::OpenClusterGlobalStore {
1868 consume_secret_inputs: false,
1869 }
1870 );
1871 apply_preset_from_config(&runtime, &auth_store, bootstrap)?;
1872 tracing::info!("bootstrap already completed, unsealing existing cluster auth store");
1873 }
1874 crate::cluster::BootstrapDisposition::ProceedLocal => {
1875 debug_assert_eq!(
1878 vault_plan,
1879 crate::cluster::VaultBootstrapPlan::OpenClusterGlobalStore {
1880 consume_secret_inputs: true,
1881 }
1882 );
1883 apply_preset_from_config(&runtime, &auth_store, bootstrap)?;
1884 maybe_apply_policy_break_glass(&auth_store);
1885 }
1886 }
1887
1888 {
1890 let store = Arc::clone(&auth_store);
1891 std::thread::Builder::new()
1892 .name("reddb-session-purge".into())
1893 .spawn(move || loop {
1894 std::thread::sleep(std::time::Duration::from_secs(300));
1895 store.purge_expired_sessions();
1896 })
1897 .ok();
1898 }
1899
1900 Ok((runtime, auth_store, telemetry_guard))
1901}
1902
1903fn maybe_apply_policy_break_glass(auth_store: &Arc<AuthStore>) {
1907 use crate::auth::self_lock_guard::BREAK_GLASS_ENV;
1908
1909 let enabled = std::env::var(BREAK_GLASS_ENV)
1910 .ok()
1911 .map(|v| {
1912 let trimmed = v.trim().to_ascii_lowercase();
1913 matches!(trimmed.as_str(), "1" | "true" | "yes")
1914 })
1915 .unwrap_or(false);
1916 if !enabled {
1917 return;
1918 }
1919 let now = crate::utils::now_unix_millis() as u128;
1920 match auth_store.apply_policy_break_glass(now) {
1921 Ok(()) => {
1922 tracing::warn!(env = BREAK_GLASS_ENV, "policy break-glass recovery applied");
1923 }
1924 Err(err) => {
1925 tracing::error!(env = BREAK_GLASS_ENV, %err, "policy break-glass recovery failed");
1926 }
1927 }
1928}
1929
1930pub(crate) const BOOTSTRAP_COMPLETED_KEY: &str = "system.bootstrap.completed";
1935pub(crate) const BOOTSTRAP_PRESET_KEY: &str = "system.bootstrap.preset";
1936pub(crate) const BOOTSTRAP_FIRST_ADMIN_KEY: &str = "system.bootstrap.first_admin_id";
1937
1938pub(crate) const BOOTSTRAP_PRESET_ENV: &str = "REDDB_BOOTSTRAP_PRESET";
1941pub(crate) const PRESET_ENV: &str = "REDDB_PRESET";
1942pub(crate) const BOOTSTRAP_CERT_OUT_ENV: &str = "REDDB_BOOTSTRAP_CERT_OUT";
1945pub(crate) const PRESET_SIMPLE: &str = "simple";
1946pub(crate) const PRESET_PRODUCTION: &str = "production";
1947pub(crate) const PRESET_REGULATED: &str = "regulated";
1948pub(crate) const PRESET_CLOUD: &str = "cloud";
1949
1950pub(crate) const FIRST_ADMIN_ALLOW_ALL_POLICY: &str = "system.bootstrap.first-admin-allow-all";
1954pub(crate) const REGULATED_PROTECT_MANAGED_POLICY: &str = "system.regulated.protect-managed";
1955pub(crate) const CLOUD_PROTECT_MANAGED_POLICY: &str = "system.cloud.protect-managed";
1956pub(crate) const CLOUD_CONFIG_NAMESPACE: &str = "red.config.cloud";
1957pub(crate) const REGULATED_AUDIT_CONFIG_NAMESPACE: &str = "red.config.audit";
1958pub(crate) const REGULATED_EVIDENCE_CONFIG_NAMESPACE: &str = "red.config.evidence";
1959pub(crate) const REGULATED_QUERY_AUDIT_CONFIG_NAMESPACE: &str = "red.config.query_audit";
1960
1961pub(crate) fn apply_preset(
1971 runtime: &RedDBRuntime,
1972 auth_store: &Arc<AuthStore>,
1973) -> Result<(), String> {
1974 let bootstrap = BootstrapConfig::default();
1975 apply_preset_from_config(runtime, auth_store, &bootstrap)
1976}
1977
1978fn apply_preset_from_config(
1979 runtime: &RedDBRuntime,
1980 auth_store: &Arc<AuthStore>,
1981 bootstrap: &BootstrapConfig,
1982) -> Result<(), String> {
1983 let store = runtime.db().store();
1984
1985 if store.get_config(BOOTSTRAP_COMPLETED_KEY).is_some() {
1986 crate::cli::bootstrap_manifest::rehydrate_manifest_registry(
1987 runtime,
1988 &runtime.config_registry(),
1989 )?;
1990 tracing::info!("bootstrap state present, skipping preset application");
1991 return Ok(());
1992 }
1993
1994 let preset = bootstrap.resolved_preset();
1995
1996 if let Some(path) = bootstrap.selected_manifest() {
1997 if let Some(manifest_bootstrap) =
1998 crate::cli::bootstrap_manifest::bootstrap_config_from_manifest_file(&path)?
1999 {
2000 apply_preset_from_config(runtime, auth_store, &manifest_bootstrap)?;
2001 return Ok(());
2002 }
2003 let first_admin_id = crate::cli::bootstrap_manifest::apply_manifest_file(
2004 runtime,
2005 auth_store,
2006 &runtime.config_registry(),
2007 &path,
2008 )?;
2009 persist_bootstrap_state(runtime, "manifest", Some(&first_admin_id));
2010 tracing::info!("bootstrap manifest applied");
2011 return Ok(());
2012 }
2013
2014 let (first_admin_id, certificate) = match preset.as_str() {
2015 PRESET_SIMPLE => {
2016 (None, None)
2020 }
2021 PRESET_PRODUCTION => {
2022 let (id, cert) = apply_production_preset_from_config(auth_store, bootstrap)?;
2023 (Some(id), cert)
2024 }
2025 PRESET_CLOUD => {
2026 let (id, cert) = apply_cloud_preset(runtime, auth_store, bootstrap)?;
2027 (Some(id), cert)
2028 }
2029 PRESET_REGULATED => {
2030 apply_regulated_preset(runtime, auth_store)?;
2031 (None, None)
2032 }
2033 other => {
2034 return Err(format!(
2035 "bootstrap preset {other:?} is not recognised (expected `simple`, `production`, `regulated`, or `cloud`)"
2036 ));
2037 }
2038 };
2039
2040 if let Some(cert) = certificate.as_deref() {
2047 write_bootstrap_certificate(bootstrap, cert)?;
2048 }
2049
2050 persist_bootstrap_state(runtime, &preset, first_admin_id.as_deref());
2051 tracing::info!(preset = %preset, "bootstrap preset applied");
2052 Ok(())
2053}
2054
2055fn write_bootstrap_certificate(
2059 bootstrap: &BootstrapConfig,
2060 certificate_hex: &str,
2061) -> Result<(), String> {
2062 let Some(path) = bootstrap.selected_cert_out() else {
2063 return Ok(());
2064 };
2065 std::fs::write(&path, certificate_hex)
2066 .map_err(|err| format!("write --bootstrap-cert-out {}: {err}", path.display()))?;
2067 tracing::info!(
2068 target: "reddb::bootstrap",
2069 path = %path.display(),
2070 "first-boot self-bootstrap: wrote unseal certificate to --bootstrap-cert-out (consume via REDDB_CERTIFICATE_FILE on the next boot)"
2071 );
2072 Ok(())
2073}
2074
2075fn apply_production_preset_from_config(
2076 auth_store: &Arc<AuthStore>,
2077 bootstrap: &BootstrapConfig,
2078) -> Result<(String, Option<String>), String> {
2079 use crate::auth::store::PrincipalRef;
2080 use crate::auth::UserId;
2081
2082 let username = bootstrap.production_username().ok_or_else(|| {
2083 "production preset requires --bootstrap-admin or REDDB_USERNAME (or REDDB_USERNAME_FILE)"
2084 .to_string()
2085 })?;
2086 let password = bootstrap.production_password().ok_or_else(|| {
2087 "production preset requires --bootstrap-admin-password or REDDB_PASSWORD (or REDDB_PASSWORD_FILE)"
2088 .to_string()
2089 })?;
2090
2091 let result = auth_store
2095 .bootstrap(&username, &password)
2096 .map_err(|err| format!("bootstrap first admin: {err}"))?;
2097 let certificate = result.certificate.clone();
2098 if let Some(cert) = certificate.as_deref() {
2099 eprintln!("[reddb] CERTIFICATE: {}", cert);
2100 tracing::warn!(
2101 "vault certificate issued by REDDB_PRESET=production -- save it and update the runtime secret before restart"
2102 );
2103 }
2104 let first_admin = UserId::platform(result.user.username.clone());
2105
2106 install_allow_all_policy(auth_store)?;
2108
2109 auth_store
2111 .attach_policy(
2112 PrincipalRef::User(first_admin.clone()),
2113 FIRST_ADMIN_ALLOW_ALL_POLICY,
2114 )
2115 .map_err(|err| format!("attach allow-all policy: {err}"))?;
2116
2117 Ok((first_admin.to_string(), certificate))
2118}
2119
2120fn apply_cloud_preset(
2121 runtime: &RedDBRuntime,
2122 auth_store: &Arc<AuthStore>,
2123 bootstrap: &BootstrapConfig,
2124) -> Result<(String, Option<String>), String> {
2125 use crate::auth::store::PrincipalRef;
2126 use crate::auth::{Role, UserId};
2127
2128 let head_username = bootstrap.cloud_head_username().ok_or_else(|| {
2129 "cloud preset requires --cloud-head-admin or REDDB_CLOUD_HEAD_ADMIN (or REDDB_USERNAME)"
2130 .to_string()
2131 })?;
2132 let head_password = bootstrap.cloud_head_password().ok_or_else(|| {
2133 "cloud preset requires --cloud-head-admin-password or REDDB_CLOUD_HEAD_ADMIN_PASSWORD (or REDDB_PASSWORD)"
2134 .to_string()
2135 })?;
2136 let customer_username = bootstrap.customer_username().ok_or_else(|| {
2137 "cloud preset requires --customer-admin or REDDB_CUSTOMER_ADMIN".to_string()
2138 })?;
2139 let customer_password = bootstrap.customer_password().ok_or_else(|| {
2140 "cloud preset requires --customer-admin-password or REDDB_CUSTOMER_ADMIN_PASSWORD"
2141 .to_string()
2142 })?;
2143 if head_username == customer_username {
2144 return Err("cloud preset requires distinct head and customer admin usernames".to_string());
2145 }
2146
2147 let head = auth_store
2148 .bootstrap_system_admin(&head_username, &head_password)
2149 .map_err(|err| format!("bootstrap cloud head admin: {err}"))?;
2150 let certificate = head.certificate.clone();
2151 let head_id = UserId::platform(head.user.username.clone());
2152
2153 auth_store
2154 .create_user(&customer_username, &customer_password, Role::Admin)
2155 .map_err(|err| format!("create cloud customer admin: {err}"))?;
2156 let customer_id = UserId::platform(customer_username.clone());
2157
2158 install_allow_all_policy(auth_store)?;
2159 for user_id in [&head_id, &customer_id] {
2160 auth_store
2161 .attach_policy(
2162 PrincipalRef::User(user_id.clone()),
2163 FIRST_ADMIN_ALLOW_ALL_POLICY,
2164 )
2165 .map_err(|err| format!("attach allow-all policy: {err}"))?;
2166 }
2167 install_cloud_guardrails(runtime, auth_store, &head_id)?;
2168 auth_store
2169 .attach_policy(
2170 PrincipalRef::User(customer_id),
2171 CLOUD_PROTECT_MANAGED_POLICY,
2172 )
2173 .map_err(|err| format!("attach cloud guardrail policy: {err}"))?;
2174
2175 Ok((head_id.to_string(), certificate))
2176}
2177
2178fn install_allow_all_policy(auth_store: &Arc<AuthStore>) -> Result<(), String> {
2179 use crate::auth::policies::Policy;
2180
2181 let policy = Policy::from_json_str(&format!(
2182 r#"{{
2183 "id": "{id}",
2184 "version": 1,
2185 "statements": [{{
2186 "effect": "allow",
2187 "actions": ["*"],
2188 "resources": ["*"]
2189 }}]
2190 }}"#,
2191 id = FIRST_ADMIN_ALLOW_ALL_POLICY
2192 ))
2193 .map_err(|err| format!("compile allow-all policy: {err}"))?;
2194 auth_store
2195 .put_policy(policy)
2196 .map_err(|err| format!("install allow-all policy: {err}"))
2197}
2198
2199fn install_cloud_guardrails(
2200 runtime: &RedDBRuntime,
2201 auth_store: &Arc<AuthStore>,
2202 head_admin: &crate::auth::UserId,
2203) -> Result<(), String> {
2204 use crate::auth::policies::Policy;
2205 use crate::auth::registry::EvidenceRequirement;
2206
2207 let policy = Policy::from_json_str(&format!(
2208 r#"{{
2209 "id": "{id}",
2210 "version": 1,
2211 "statements": [
2212 {{
2213 "effect": "deny",
2214 "actions": ["policy:put", "policy:drop", "policy:attach", "policy:detach"],
2215 "resources": ["policy:{id}"]
2216 }},
2217 {{
2218 "effect": "deny",
2219 "actions": [
2220 "user:delete",
2221 "user:disable",
2222 "user:password:change",
2223 "user:role:update"
2224 ],
2225 "resources": ["user:{head_admin}"]
2226 }},
2227 {{
2228 "effect": "deny",
2229 "actions": ["config:write"],
2230 "resources": ["config:{cloud}.*"]
2231 }}
2232 ]
2233 }}"#,
2234 id = CLOUD_PROTECT_MANAGED_POLICY,
2235 head_admin = head_admin,
2236 cloud = CLOUD_CONFIG_NAMESPACE,
2237 ))
2238 .map_err(|err| format!("compile cloud guardrail policy: {err}"))?;
2239 auth_store
2240 .put_policy(policy)
2241 .map_err(|err| format!("install cloud guardrail policy: {err}"))?;
2242
2243 let now_ms = crate::utils::now_unix_millis() as u128;
2244 let entries = vec![
2245 regulated_registry_entry(
2246 CLOUD_PROTECT_MANAGED_POLICY,
2247 crate::auth::managed_policy::RESOURCE_TYPE_POLICY,
2248 "policy",
2249 "policy:*",
2250 &format!("policy:{CLOUD_PROTECT_MANAGED_POLICY}"),
2251 EvidenceRequirement::Metadata,
2252 now_ms,
2253 ),
2254 regulated_registry_entry(
2255 CLOUD_CONFIG_NAMESPACE,
2256 crate::auth::managed_config::RESOURCE_TYPE_CONFIG_NAMESPACE,
2257 "config_namespace",
2258 "config:write",
2259 &format!("config:{CLOUD_CONFIG_NAMESPACE}.*"),
2260 EvidenceRequirement::Metadata,
2261 now_ms,
2262 ),
2263 ];
2264 for entry in entries.iter().cloned() {
2265 runtime
2266 .config_registry()
2267 .restore_bootstrap_entry(entry)
2268 .map_err(|err| format!("install cloud registry entry: {err}"))?;
2269 }
2270 crate::cli::bootstrap_manifest::persist_registry_state(runtime, &entries)?;
2271 Ok(())
2272}
2273
2274fn apply_regulated_preset(
2275 runtime: &RedDBRuntime,
2276 auth_store: &Arc<AuthStore>,
2277) -> Result<(), String> {
2278 use crate::auth::policies::Policy;
2279 use crate::auth::registry::EvidenceRequirement;
2280 use crate::auth::store::{PrincipalRef, PUBLIC_IAM_GROUP};
2281
2282 runtime.query_audit().enable_infrastructure();
2283
2284 let policy = Policy::from_json_str(&format!(
2285 r#"{{
2286 "id": "{id}",
2287 "version": 1,
2288 "statements": [
2289 {{
2290 "effect": "deny",
2291 "actions": ["policy:put", "policy:drop", "policy:attach", "policy:detach"],
2292 "resources": ["policy:{id}"]
2293 }},
2294 {{
2295 "effect": "deny",
2296 "actions": ["config:write"],
2297 "resources": [
2298 "config:{audit}.*",
2299 "config:{evidence}.*",
2300 "config:{query_audit}.*"
2301 ]
2302 }}
2303 ]
2304 }}"#,
2305 id = REGULATED_PROTECT_MANAGED_POLICY,
2306 audit = REGULATED_AUDIT_CONFIG_NAMESPACE,
2307 evidence = REGULATED_EVIDENCE_CONFIG_NAMESPACE,
2308 query_audit = REGULATED_QUERY_AUDIT_CONFIG_NAMESPACE,
2309 ))
2310 .map_err(|err| format!("compile regulated guardrail policy: {err}"))?;
2311 auth_store
2312 .put_policy(policy)
2313 .map_err(|err| format!("install regulated guardrail policy: {err}"))?;
2314 auth_store
2315 .attach_policy(
2316 PrincipalRef::Group(PUBLIC_IAM_GROUP.to_string()),
2317 REGULATED_PROTECT_MANAGED_POLICY,
2318 )
2319 .map_err(|err| format!("attach regulated guardrail policy: {err}"))?;
2320
2321 let now_ms = crate::utils::now_unix_millis() as u128;
2322 let entries = vec![
2323 regulated_registry_entry(
2324 REGULATED_PROTECT_MANAGED_POLICY,
2325 crate::auth::managed_policy::RESOURCE_TYPE_POLICY,
2326 "iam_policy",
2327 "policy:*",
2328 &format!("policy:{REGULATED_PROTECT_MANAGED_POLICY}"),
2329 EvidenceRequirement::Metadata,
2330 now_ms,
2331 ),
2332 regulated_registry_entry(
2333 REGULATED_AUDIT_CONFIG_NAMESPACE,
2334 crate::auth::managed_config::RESOURCE_TYPE_CONFIG_NAMESPACE,
2335 "config_namespace",
2336 "config:write",
2337 &format!("config:{REGULATED_AUDIT_CONFIG_NAMESPACE}.*"),
2338 EvidenceRequirement::Metadata,
2339 now_ms,
2340 ),
2341 regulated_registry_entry(
2342 REGULATED_EVIDENCE_CONFIG_NAMESPACE,
2343 crate::auth::managed_config::RESOURCE_TYPE_CONFIG_NAMESPACE,
2344 "config_namespace",
2345 "config:write",
2346 &format!("config:{REGULATED_EVIDENCE_CONFIG_NAMESPACE}.*"),
2347 EvidenceRequirement::Metadata,
2348 now_ms,
2349 ),
2350 regulated_registry_entry(
2351 REGULATED_QUERY_AUDIT_CONFIG_NAMESPACE,
2352 crate::auth::managed_config::RESOURCE_TYPE_CONFIG_NAMESPACE,
2353 "config_namespace",
2354 "config:write",
2355 &format!("config:{REGULATED_QUERY_AUDIT_CONFIG_NAMESPACE}.*"),
2356 EvidenceRequirement::Metadata,
2357 now_ms,
2358 ),
2359 ];
2360
2361 for entry in entries.iter().cloned() {
2362 runtime
2363 .config_registry()
2364 .restore_bootstrap_entry(entry)
2365 .map_err(|err| format!("install regulated registry entry: {err}"))?;
2366 }
2367 crate::cli::bootstrap_manifest::persist_registry_state(runtime, &entries)?;
2368 Ok(())
2369}
2370
2371fn regulated_registry_entry(
2372 id: &str,
2373 resource_type: &str,
2374 schema: &str,
2375 required_action: &str,
2376 required_resource: &str,
2377 evidence_requirement: crate::auth::registry::EvidenceRequirement,
2378 updated_at_ms: u128,
2379) -> crate::auth::registry::ConfigRegistryEntry {
2380 crate::auth::registry::ConfigRegistryEntry {
2381 id: id.to_string(),
2382 version: 1,
2383 resource_type: resource_type.to_string(),
2384 schema: schema.to_string(),
2385 mutability: crate::auth::registry::Mutability::Immutable,
2386 sensitivity: crate::auth::registry::Sensitivity::Internal,
2387 managed: true,
2388 required_action: required_action.to_string(),
2389 required_resource: required_resource.to_string(),
2390 evidence_requirement,
2391 updated_by: "system:regulated-preset".to_string(),
2392 updated_at_ms,
2393 }
2394}
2395
2396fn persist_bootstrap_state(runtime: &RedDBRuntime, preset: &str, first_admin_id: Option<&str>) {
2397 let store = runtime.db().store();
2398 let mut tree = crate::serde_json::Map::new();
2399 tree.insert(
2400 BOOTSTRAP_COMPLETED_KEY.to_string(),
2401 crate::serde_json::Value::Bool(true),
2402 );
2403 tree.insert(
2404 BOOTSTRAP_PRESET_KEY.to_string(),
2405 crate::serde_json::Value::String(preset.to_string()),
2406 );
2407 if let Some(id) = first_admin_id {
2408 tree.insert(
2409 BOOTSTRAP_FIRST_ADMIN_KEY.to_string(),
2410 crate::serde_json::Value::String(id.to_string()),
2411 );
2412 }
2413 let json = crate::serde_json::Value::Object(tree);
2414 store.set_config_tree("", &json);
2415}
2416
2417fn merge_telemetry_with_config(
2428 mut cli: crate::telemetry::TelemetryConfig,
2429 runtime: &RedDBRuntime,
2430) -> crate::telemetry::TelemetryConfig {
2431 use reddb_types::Value;
2432
2433 let store = runtime.db().store();
2434
2435 if !cli.level_explicit {
2436 if let Some(Value::Text(v)) = store.get_config("red.logging.level") {
2437 cli.level_filter = v.to_string();
2438 }
2439 }
2440 if !cli.format_explicit {
2441 if let Some(Value::Text(v)) = store.get_config("red.logging.format") {
2442 if let Some(parsed) = crate::telemetry::LogFormat::parse(&v) {
2443 cli.format = parsed;
2444 }
2445 }
2446 }
2447 if !cli.rotation_keep_days_explicit {
2448 match store.get_config("red.logging.keep_days") {
2449 Some(Value::Integer(n)) if n >= 0 && n <= u16::MAX as i64 => {
2450 cli.rotation_keep_days = n as u16
2451 }
2452 Some(Value::UnsignedInteger(n)) if n <= u16::MAX as u64 => {
2453 cli.rotation_keep_days = n as u16
2454 }
2455 Some(Value::Text(v)) => {
2456 if let Ok(n) = v.parse::<u16>() {
2457 cli.rotation_keep_days = n;
2458 }
2459 }
2460 _ => {}
2461 }
2462 }
2463 if !cli.file_prefix_explicit {
2464 if let Some(Value::Text(v)) = store.get_config("red.logging.file_prefix") {
2465 if !v.is_empty() {
2466 cli.file_prefix = v.to_string();
2467 }
2468 }
2469 }
2470 if !cli.log_dir_explicit && !cli.log_file_disabled {
2473 if let Some(Value::Text(v)) = store.get_config("red.logging.dir") {
2474 if !v.is_empty() {
2475 cli.log_dir = Some(std::path::PathBuf::from(v.as_ref()));
2476 }
2477 }
2478 }
2479
2480 cli
2481}
2482
2483#[cfg(test)]
2484mod telemetry_merge_tests {
2485 use super::*;
2486 use crate::telemetry::{LogFormat, TelemetryConfig};
2487
2488 fn fresh_runtime() -> RedDBRuntime {
2489 RedDBRuntime::with_options(RedDBOptions::in_memory()).expect("runtime")
2490 }
2491
2492 fn set_str(runtime: &RedDBRuntime, key: &str, value: &str) {
2493 runtime
2494 .db()
2495 .store()
2496 .set_config_tree(key, &crate::serde_json::Value::String(value.to_string()));
2497 }
2498
2499 fn cli_base() -> TelemetryConfig {
2500 TelemetryConfig {
2503 log_dir: Some(std::path::PathBuf::from("/tmp/reddb-default/logs")),
2504 format: LogFormat::Json,
2505 ..Default::default()
2506 }
2507 }
2508
2509 #[test]
2510 fn config_log_dir_promoted_when_flag_absent() {
2511 let runtime = fresh_runtime();
2512 set_str(&runtime, "red.logging.dir", "/var/log/reddb");
2513 let merged = merge_telemetry_with_config(cli_base(), &runtime);
2514 assert_eq!(
2515 merged.log_dir.as_deref(),
2516 Some(std::path::Path::new("/var/log/reddb"))
2517 );
2518 }
2519
2520 #[test]
2521 fn explicit_log_dir_wins_over_config() {
2522 let runtime = fresh_runtime();
2523 set_str(&runtime, "red.logging.dir", "/var/log/reddb");
2524 let mut cli = cli_base();
2525 cli.log_dir = Some(std::path::PathBuf::from("/custom/dir"));
2526 cli.log_dir_explicit = true;
2527 let merged = merge_telemetry_with_config(cli, &runtime);
2528 assert_eq!(
2529 merged.log_dir.as_deref(),
2530 Some(std::path::Path::new("/custom/dir"))
2531 );
2532 }
2533
2534 #[test]
2535 fn no_log_file_beats_config_log_dir() {
2536 let runtime = fresh_runtime();
2537 set_str(&runtime, "red.logging.dir", "/var/log/reddb");
2538 let mut cli = cli_base();
2539 cli.log_dir = None;
2540 cli.log_file_disabled = true;
2541 let merged = merge_telemetry_with_config(cli, &runtime);
2542 assert!(
2543 merged.log_dir.is_none(),
2544 "--no-log-file must veto config dir"
2545 );
2546 }
2547
2548 #[test]
2549 fn config_format_promoted_on_non_tty_default() {
2550 let runtime = fresh_runtime();
2554 set_str(&runtime, "red.logging.format", "pretty");
2555 let merged = merge_telemetry_with_config(cli_base(), &runtime);
2556 assert_eq!(merged.format, LogFormat::Pretty);
2557 }
2558
2559 #[test]
2560 fn explicit_format_wins_over_config() {
2561 let runtime = fresh_runtime();
2562 set_str(&runtime, "red.logging.format", "pretty");
2563 let mut cli = cli_base();
2564 cli.format = LogFormat::Json;
2565 cli.format_explicit = true;
2566 let merged = merge_telemetry_with_config(cli, &runtime);
2567 assert_eq!(merged.format, LogFormat::Json);
2568 }
2569}
2570
2571#[inline(never)]
2572fn build_http_server(
2573 runtime: RedDBRuntime,
2574 auth_store: Arc<AuthStore>,
2575 bind_addr: String,
2576) -> RedDBServer {
2577 build_http_server_with_transport_readiness(
2578 runtime,
2579 auth_store,
2580 bind_addr,
2581 TransportReadiness::default(),
2582 )
2583}
2584
2585fn apply_http_limits(
2591 server: RedDBServer,
2592 config: &ServerCommandConfig,
2593 runtime: &RedDBRuntime,
2594) -> RedDBServer {
2595 let store = runtime.db().store();
2596 let resolved =
2597 crate::server::http_limits::resolve_http_limits(&config.http_limits_cli, |key| match store
2598 .get_config(key)
2599 {
2600 Some(reddb_types::Value::Text(v)) => Some(v.to_string()),
2601 Some(reddb_types::Value::Integer(n)) if n >= 0 => Some(n.to_string()),
2602 Some(reddb_types::Value::UnsignedInteger(n)) => Some(n.to_string()),
2603 _ => None,
2604 });
2605 tracing::info!(
2606 target: "reddb::http_limits",
2607 max_handlers = resolved.max_handlers,
2608 handler_timeout_ms = resolved.handler_timeout_ms,
2609 retry_after_secs = resolved.retry_after_secs,
2610 max_inflight_per_principal = resolved.max_inflight_per_principal,
2611 "http_limits resolved"
2612 );
2613 server.with_http_limits(resolved)
2614}
2615
2616fn apply_ui_bundle(
2626 server: RedDBServer,
2627 config: &ServerCommandConfig,
2628) -> Result<RedDBServer, String> {
2629 if !config.ui {
2630 return Ok(server);
2631 }
2632 let ui_dir = match &config.ui_dir {
2633 Some(dir) => dir.clone(),
2634 None => {
2635 let cache_root = crate::server::ui_bundle_resolver::reddb_user_cache_root()
2636 .unwrap_or_else(|_| std::env::temp_dir().join("reddb"));
2637 crate::server::ui_bundle_resolver::resolve_ui_bundle(
2638 &cache_root,
2639 &crate::server::ui_bundle_resolver::HttpFetcher,
2640 )
2641 .map_err(|err| format!("resolve red-ui bundle for --ui: {err}"))?
2642 }
2643 };
2644 tracing::info!(target: "reddb::ui", dir = %ui_dir.display(), "serving red-ui bundle on HTTP surface");
2645 Ok(server.with_ui_dir(ui_dir))
2646}
2647
2648#[inline(never)]
2649fn build_http_server_with_transport_readiness(
2650 runtime: RedDBRuntime,
2651 auth_store: Arc<AuthStore>,
2652 bind_addr: String,
2653 transport_readiness: TransportReadiness,
2654) -> RedDBServer {
2655 RedDBServer::with_options(
2656 runtime,
2657 ServerOptions {
2658 bind_addr,
2659 transport_readiness,
2660 ..ServerOptions::default()
2661 },
2662 )
2663 .with_auth(auth_store)
2664}
2665
2666#[inline(never)]
2670fn build_admin_only_server(
2671 runtime: RedDBRuntime,
2672 auth_store: Arc<AuthStore>,
2673 bind_addr: String,
2674) -> RedDBServer {
2675 RedDBServer::with_options(
2676 runtime,
2677 ServerOptions {
2678 bind_addr,
2679 surface: crate::server::ServerSurface::AdminOnly,
2680 ..ServerOptions::default()
2681 },
2682 )
2683 .with_auth(auth_store)
2684}
2685
2686#[inline(never)]
2690fn build_metrics_only_server(
2691 runtime: RedDBRuntime,
2692 auth_store: Arc<AuthStore>,
2693 bind_addr: String,
2694) -> RedDBServer {
2695 RedDBServer::with_options(
2696 runtime,
2697 ServerOptions {
2698 bind_addr,
2699 surface: crate::server::ServerSurface::MetricsOnly,
2700 ..ServerOptions::default()
2701 },
2702 )
2703 .with_auth(auth_store)
2704}
2705
2706fn spawn_admin_metrics_listeners(runtime: &RedDBRuntime, auth_store: &Arc<AuthStore>) {
2710 if let Some(addr) = env_nonempty("RED_ADMIN_BIND") {
2711 let server = build_admin_only_server(runtime.clone(), auth_store.clone(), addr.clone());
2712 let _ = server.serve_in_background();
2713 tracing::info!(transport = "http", surface = "admin", bind = %addr, "listener online");
2714 }
2715 if let Some(addr) = env_nonempty("RED_METRICS_BIND") {
2716 let server = build_metrics_only_server(runtime.clone(), auth_store.clone(), addr.clone());
2717 let _ = server.serve_in_background();
2718 tracing::info!(transport = "http", surface = "metrics", bind = %addr, "listener online");
2719 }
2720}
2721
2722fn resolve_http_tls_config(
2724 config: &ServerCommandConfig,
2725) -> Result<crate::server::tls::HttpTlsConfig, String> {
2726 match (&config.http_tls_cert, &config.http_tls_key) {
2727 (Some(cert), Some(key)) => Ok(crate::server::tls::HttpTlsConfig {
2728 cert_path: cert.clone(),
2729 key_path: key.clone(),
2730 client_ca_path: config.http_tls_client_ca.clone(),
2731 }),
2732 (None, None) => {
2733 let dir = config
2735 .path
2736 .as_ref()
2737 .and_then(|p| p.parent().map(std::path::PathBuf::from))
2738 .unwrap_or_else(|| std::path::PathBuf::from("."));
2739 let auto = crate::server::tls::auto_generate_dev_cert(&dir)
2740 .map_err(|err| format!("HTTP TLS dev: {err}"))?;
2741 Ok(crate::server::tls::HttpTlsConfig {
2742 cert_path: auto.cert_path,
2743 key_path: auto.key_path,
2744 client_ca_path: config.http_tls_client_ca.clone(),
2745 })
2746 }
2747 _ => Err("HTTP TLS requires both --http-tls-cert and --http-tls-key (or neither, with RED_HTTP_TLS_DEV=1)".to_string()),
2748 }
2749}
2750
2751#[cfg(test)]
2752mod tests {
2753 use super::*;
2754
2755 #[test]
2756 fn render_systemd_unit_contains_expected_execstart() {
2757 let config = SystemdServiceConfig {
2758 service_name: "reddb".to_string(),
2759 binary_path: PathBuf::from("/usr/local/bin/red"),
2760 run_user: "reddb".to_string(),
2761 run_group: "reddb".to_string(),
2762 data_path: reddb_file::default_service_database_path(),
2763 router_bind_addr: None,
2764 grpc_bind_addr: Some("0.0.0.0:55055".to_string()),
2765 http_bind_addr: None,
2766 };
2767
2768 let unit = render_systemd_unit(&config);
2769 assert!(unit.contains("ExecStart=/usr/local/bin/red server --path /var/lib/reddb/data.rdb --grpc-bind 0.0.0.0:55055"));
2770 assert!(unit.contains("ReadWritePaths=/var/lib/reddb"));
2771 }
2772
2773 #[test]
2774 fn systemd_service_config_derives_paths() {
2775 let config = SystemdServiceConfig {
2776 service_name: "reddb-api".to_string(),
2777 binary_path: PathBuf::from("/usr/local/bin/red"),
2778 run_user: "reddb".to_string(),
2779 run_group: "reddb".to_string(),
2780 data_path: PathBuf::from("/srv/reddb/live/data.rdb"),
2781 router_bind_addr: None,
2782 grpc_bind_addr: None,
2783 http_bind_addr: Some("127.0.0.1:5000".to_string()),
2784 };
2785
2786 assert_eq!(config.data_dir(), PathBuf::from("/srv/reddb/live"));
2787 assert_eq!(
2788 config.unit_path(),
2789 PathBuf::from("/etc/systemd/system/reddb-api.service")
2790 );
2791 }
2792
2793 #[test]
2794 fn render_systemd_unit_supports_dual_transport() {
2795 let config = SystemdServiceConfig {
2796 service_name: "reddb".to_string(),
2797 binary_path: PathBuf::from("/usr/local/bin/red"),
2798 run_user: "reddb".to_string(),
2799 run_group: "reddb".to_string(),
2800 data_path: reddb_file::default_service_database_path(),
2801 router_bind_addr: None,
2802 grpc_bind_addr: Some("0.0.0.0:55055".to_string()),
2803 http_bind_addr: Some("0.0.0.0:5000".to_string()),
2804 };
2805
2806 let unit = render_systemd_unit(&config);
2807 assert!(unit.contains("--grpc-bind 0.0.0.0:55055"));
2808 assert!(unit.contains("--http-bind 0.0.0.0:5000"));
2809 }
2810
2811 #[test]
2812 fn render_systemd_unit_supports_router_mode() {
2813 let config = SystemdServiceConfig {
2814 service_name: "reddb".to_string(),
2815 binary_path: PathBuf::from("/usr/local/bin/red"),
2816 run_user: "reddb".to_string(),
2817 run_group: "reddb".to_string(),
2818 data_path: reddb_file::default_service_database_path(),
2819 router_bind_addr: Some(DEFAULT_ROUTER_BIND_ADDR.to_string()),
2820 grpc_bind_addr: None,
2821 http_bind_addr: None,
2822 };
2823
2824 let unit = render_systemd_unit(&config);
2825 assert!(unit.contains("--bind 127.0.0.1:5050"));
2826 assert!(!unit.contains("--grpc-bind"));
2827 assert!(!unit.contains("--http-bind"));
2828 }
2829
2830 #[test]
2831 fn explicit_bind_collision_is_fatal() {
2832 let held = TcpListener::bind("127.0.0.1:0").expect("hold test port");
2833 let addr = held.local_addr().expect("held addr").to_string();
2834 let mut readiness = TransportReadiness::default();
2835
2836 let error = bind_listener_for_startup(&mut readiness, "http", &addr, true).unwrap_err();
2837
2838 assert!(error.contains("explicit http listener bind"));
2839 assert_eq!(readiness.active.len(), 0);
2840 assert_eq!(readiness.failed.len(), 1);
2841 assert!(readiness.failed[0].explicit);
2842 assert_eq!(readiness.failed[0].bind_addr, addr);
2843 }
2844
2845 fn no_auth_env_lock() -> &'static std::sync::Mutex<()> {
2852 static LOCK: std::sync::OnceLock<std::sync::Mutex<()>> = std::sync::OnceLock::new();
2853 LOCK.get_or_init(|| std::sync::Mutex::new(()))
2854 }
2855
2856 fn no_auth_test_config(no_auth: bool) -> ServerCommandConfig {
2857 ServerCommandConfig {
2858 path: None,
2859 router_bind_addr: Some(DEFAULT_ROUTER_BIND_ADDR.to_string()),
2860 router_bind_explicit: false,
2861 grpc_bind_addr: None,
2862 grpc_bind_explicit: false,
2863 grpc_tls_bind_addr: None,
2864 grpc_tls_cert: None,
2865 grpc_tls_key: None,
2866 grpc_tls_client_ca: None,
2867 http_bind_addr: None,
2868 http_bind_explicit: false,
2869 http_tls_bind_addr: None,
2870 http_tls_cert: None,
2871 http_tls_key: None,
2872 http_tls_client_ca: None,
2873 wire_bind_addr: None,
2874 wire_bind_explicit: false,
2875 wire_tls_bind_addr: None,
2876 wire_tls_cert: None,
2877 wire_tls_key: None,
2878 pg_bind_addr: None,
2879 create_if_missing: true,
2880 read_only: false,
2881 role: "standalone".to_string(),
2882 primary_addr: None,
2883 storage_profile: StorageProfileSelection::embedded_single_file(),
2884 auth: false,
2885 require_auth: false,
2886 vault: true,
2889 no_auth,
2890 workers: None,
2891 telemetry: None,
2892 http_limits_cli: crate::server::HttpLimitsCliInput::default(),
2893 ui: false,
2894 ui_dir: None,
2895 bootstrap: BootstrapConfig::default(),
2896 }
2897 }
2898
2899 #[test]
2900 fn no_auth_flag_disables_every_auth_knob_and_stamps_metadata() {
2901 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
2902 unsafe {
2907 std::env::set_var("REDDB_USERNAME", "admin");
2908 std::env::set_var("REDDB_PASSWORD", "hunter2");
2909 }
2910 let config = no_auth_test_config(true);
2911 let options = config.to_db_options().expect("to_db_options");
2912
2913 assert!(no_auth_active(&options), "metadata should be stamped");
2914 assert!(!options.auth.enabled, "auth.enabled must be forced off");
2915 assert!(
2916 !options.auth.require_auth,
2917 "require_auth must be forced off"
2918 );
2919 assert!(
2920 !options.auth.vault_enabled,
2921 "vault_enabled must be forced off (overrides --vault)"
2922 );
2923 assert_eq!(
2924 options.metadata.get(NO_AUTH_META).map(String::as_str),
2925 Some("true"),
2926 );
2927
2928 unsafe {
2930 std::env::remove_var("REDDB_USERNAME");
2931 std::env::remove_var("REDDB_PASSWORD");
2932 }
2933 }
2934
2935 #[test]
2936 fn default_behaviour_without_no_auth_flag_is_unchanged() {
2937 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
2938 let config = no_auth_test_config(false);
2939 let options = config.to_db_options().expect("to_db_options");
2940
2941 assert!(
2942 !no_auth_active(&options),
2943 "default boot must not be marked no-auth"
2944 );
2945 assert!(
2946 options.metadata.get(NO_AUTH_META).is_none(),
2947 "metadata key must be absent when flag is off"
2948 );
2949 assert!(options.auth.vault_enabled);
2951 }
2952
2953 #[test]
2954 fn no_auth_active_blocks_bootstrap_from_env() {
2955 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
2956 unsafe {
2961 std::env::set_var("REDDB_USERNAME", "admin");
2962 std::env::set_var("REDDB_PASSWORD", "hunter2");
2963 }
2964
2965 let options = no_auth_test_config(true)
2966 .to_db_options()
2967 .expect("to_db_options");
2968
2969 let auth_store = AuthStore::new(options.auth.clone());
2973 if !no_auth_active(&options) {
2974 auth_store.bootstrap_from_env();
2975 }
2976
2977 assert!(
2978 auth_store.needs_bootstrap(),
2979 "no admin user must be bootstrapped under --no-auth even with REDDB_USERNAME/PASSWORD set"
2980 );
2981
2982 unsafe {
2984 std::env::remove_var("REDDB_USERNAME");
2985 std::env::remove_var("REDDB_PASSWORD");
2986 }
2987 }
2988
2989 fn clear_preset_env() {
2996 unsafe {
2998 std::env::remove_var(BOOTSTRAP_PRESET_ENV);
2999 std::env::remove_var(PRESET_ENV);
3000 std::env::remove_var("REDDB_BOOTSTRAP_MANIFEST");
3001 std::env::remove_var("REDDB_AUTH");
3002 std::env::remove_var("REDDB_REQUIRE_AUTH");
3003 std::env::remove_var("REDDB_NO_AUTH");
3004 std::env::remove_var("REDDB_DEV");
3005 std::env::remove_var("REDDB_VAULT");
3006 std::env::remove_var("REDDB_USERNAME");
3007 std::env::remove_var("REDDB_PASSWORD");
3008 std::env::remove_var("REDDB_USERNAME_FILE");
3009 std::env::remove_var("REDDB_PASSWORD_FILE");
3010 std::env::remove_var("REDDB_CLOUD_HEAD_ADMIN");
3011 std::env::remove_var("REDDB_CLOUD_HEAD_ADMIN_PASSWORD");
3012 std::env::remove_var("REDDB_CLOUD_HEAD_ADMIN_PASSWORD_FILE");
3013 std::env::remove_var("REDDB_CUSTOMER_ADMIN");
3014 std::env::remove_var("REDDB_CUSTOMER_ADMIN_PASSWORD");
3015 std::env::remove_var("REDDB_CUSTOMER_ADMIN_PASSWORD_FILE");
3016 }
3017 }
3018
3019 fn clear_backup_env() {
3020 unsafe {
3022 std::env::remove_var("REDDB_BACKUP_S3_ENDPOINT");
3023 std::env::remove_var("REDDB_BACKUP_S3_BUCKET");
3024 std::env::remove_var("REDDB_BACKUP_S3_PREFIX");
3025 std::env::remove_var("REDDB_BACKUP_S3_ACCESS_KEY_ID");
3026 std::env::remove_var("REDDB_BACKUP_S3_SECRET_ACCESS_KEY");
3027 std::env::remove_var("REDDB_BACKUP_S3_REGION");
3028 std::env::remove_var("REDDB_BACKUP_CHECKPOINT_INTERVAL_SECS");
3029 std::env::remove_var("REDDB_BACKUP_WAL_FLUSH_INTERVAL_SECS");
3030 std::env::remove_var("REDDB_BACKUP_PAUSE_ON_LAG_SECS");
3031 }
3032 }
3033
3034 fn fresh_runtime_and_store() -> (RedDBRuntime, Arc<AuthStore>) {
3035 let runtime = RedDBRuntime::with_options(RedDBOptions::in_memory()).expect("runtime");
3036 let auth_store = Arc::new(AuthStore::new(crate::auth::AuthConfig::default()));
3037 (runtime, auth_store)
3038 }
3039
3040 #[test]
3041 fn auth_env_knobs_enable_auth_require_auth_and_vault() {
3042 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3043 clear_preset_env();
3044 unsafe {
3046 std::env::set_var("REDDB_AUTH", "true");
3047 std::env::set_var("REDDB_REQUIRE_AUTH", "true");
3048 std::env::set_var("REDDB_VAULT", "true");
3049 }
3050
3051 let mut config = no_auth_test_config(false);
3052 config.vault = false;
3053 let options = config.to_db_options().expect("to_db_options");
3054
3055 assert!(options.auth.enabled);
3056 assert!(options.auth.require_auth);
3057 assert!(options.auth.vault_enabled);
3058
3059 clear_preset_env();
3060 }
3061
3062 #[test]
3063 fn production_and_cloud_presets_force_auth_require_auth_and_vault() {
3064 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3065 clear_preset_env();
3066
3067 for preset in [PRESET_PRODUCTION, PRESET_CLOUD] {
3068 let mut config = no_auth_test_config(false);
3069 config.vault = false;
3070 config.bootstrap.preset = Some(preset.to_string());
3071
3072 let options = config.to_db_options().expect("to_db_options");
3073 assert!(options.auth.enabled, "{preset} should enable auth");
3074 assert!(
3075 options.auth.require_auth,
3076 "{preset} should require authenticated requests"
3077 );
3078 assert!(options.auth.vault_enabled, "{preset} should enable vault");
3079 assert!(!no_auth_active(&options));
3080 }
3081
3082 clear_preset_env();
3083 }
3084
3085 #[test]
3086 fn no_auth_env_overrides_preset_forced_auth() {
3087 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3088 clear_preset_env();
3089 unsafe {
3091 std::env::set_var("REDDB_NO_AUTH", "true");
3092 std::env::set_var(BOOTSTRAP_PRESET_ENV, PRESET_CLOUD);
3093 }
3094
3095 let mut config = no_auth_test_config(false);
3096 config.auth = true;
3097 config.require_auth = true;
3098 let options = config.to_db_options().expect("to_db_options");
3099
3100 assert!(no_auth_active(&options));
3101 assert!(!options.auth.enabled);
3102 assert!(!options.auth.require_auth);
3103 assert!(!options.auth.vault_enabled);
3104
3105 clear_preset_env();
3106 }
3107
3108 #[test]
3109 fn simple_preset_is_default_and_persists_state() {
3110 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3111 clear_preset_env();
3112
3113 let (runtime, auth_store) = fresh_runtime_and_store();
3114 apply_preset(&runtime, &auth_store).expect("simple preset applies cleanly");
3115
3116 assert!(
3118 auth_store.needs_bootstrap(),
3119 "simple preset must not create an admin"
3120 );
3121
3122 let store = runtime.db().store();
3124 let completed = store
3125 .get_config(BOOTSTRAP_COMPLETED_KEY)
3126 .expect("completed key persisted");
3127 assert!(matches!(completed, reddb_types::Value::Boolean(true)));
3128 let preset = store
3129 .get_config(BOOTSTRAP_PRESET_KEY)
3130 .expect("preset key persisted");
3131 match preset {
3132 reddb_types::Value::Text(s) => assert_eq!(s.as_ref(), PRESET_SIMPLE),
3133 other => panic!("expected Text(simple), got {other:?}"),
3134 }
3135 assert!(
3136 store.get_config(BOOTSTRAP_FIRST_ADMIN_KEY).is_none(),
3137 "simple preset must not record a first admin"
3138 );
3139
3140 clear_preset_env();
3141 }
3142
3143 #[test]
3144 fn production_preset_creates_first_admin_with_allow_all_policy() {
3145 use crate::auth::policies::{EvalContext, ResourceRef};
3146 use crate::auth::UserId;
3147
3148 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3149 clear_preset_env();
3150 unsafe {
3152 std::env::set_var(PRESET_ENV, PRESET_PRODUCTION);
3153 std::env::set_var("REDDB_USERNAME", "ops");
3154 std::env::set_var("REDDB_PASSWORD", "hunter2");
3155 }
3156
3157 let (runtime, auth_store) = fresh_runtime_and_store();
3158 apply_preset(&runtime, &auth_store).expect("production preset applies cleanly");
3159
3160 assert!(
3162 !auth_store.needs_bootstrap(),
3163 "production preset must seal bootstrap"
3164 );
3165 let users = auth_store.list_users();
3166 assert_eq!(users.len(), 1);
3167 let admin = &users[0];
3168 assert_eq!(admin.username, "ops");
3169 assert!(
3170 admin.tenant_id.is_none(),
3171 "first admin must be platform-scoped (tenant=None)"
3172 );
3173
3174 let policy = auth_store
3176 .get_policy(FIRST_ADMIN_ALLOW_ALL_POLICY)
3177 .expect("allow-all policy installed");
3178 assert!(!policy.statements.is_empty());
3179
3180 let actor = UserId::platform("ops");
3183 let ctx = EvalContext {
3184 principal_tenant: None,
3185 current_tenant: None,
3186 peer_ip: None,
3187 mfa_present: false,
3188 now_ms: 1_700_000_000_000,
3189 principal_is_admin_role: true,
3190 principal_is_platform_scoped: true,
3191 };
3192 let arbitrary_resource = ResourceRef::new("config", "red.config.audit.enabled");
3193 assert!(
3194 auth_store.check_policy_authz(&actor, "config:write", &arbitrary_resource, &ctx),
3195 "allow-all policy must grant arbitrary actions via the evaluator"
3196 );
3197
3198 let store = runtime.db().store();
3200 match store
3201 .get_config(BOOTSTRAP_FIRST_ADMIN_KEY)
3202 .expect("first_admin_id persisted")
3203 {
3204 reddb_types::Value::Text(s) => assert_eq!(s.as_ref(), "ops"),
3205 other => panic!("expected Text(ops), got {other:?}"),
3206 }
3207 match store.get_config(BOOTSTRAP_PRESET_KEY).unwrap() {
3208 reddb_types::Value::Text(s) => assert_eq!(s.as_ref(), PRESET_PRODUCTION),
3209 other => panic!("expected Text(production), got {other:?}"),
3210 }
3211
3212 clear_preset_env();
3213 }
3214
3215 #[test]
3216 fn bootstrap_preset_env_takes_precedence_over_legacy_preset_env() {
3217 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3218 clear_preset_env();
3219 unsafe {
3221 std::env::set_var(BOOTSTRAP_PRESET_ENV, PRESET_REGULATED);
3222 std::env::set_var(PRESET_ENV, PRESET_SIMPLE);
3223 }
3224
3225 let options = no_auth_test_config(false)
3226 .to_db_options()
3227 .expect("regulated options");
3228 assert!(
3229 options.control_events.compliance_mode,
3230 "canonical REDDB_BOOTSTRAP_PRESET should win over REDDB_PRESET"
3231 );
3232 assert!(options.query_audit.enabled);
3233
3234 clear_preset_env();
3235 }
3236
3237 #[test]
3238 fn cloud_preset_creates_system_head_and_customer_admins() {
3239 use crate::auth::policies::{EvalContext, ResourceRef};
3240 use crate::auth::UserId;
3241
3242 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3243 clear_preset_env();
3244 unsafe {
3246 std::env::set_var(BOOTSTRAP_PRESET_ENV, PRESET_CLOUD);
3247 std::env::set_var("REDDB_CLOUD_HEAD_ADMIN", "head");
3248 std::env::set_var("REDDB_CLOUD_HEAD_ADMIN_PASSWORD", "head-pass");
3249 std::env::set_var("REDDB_CUSTOMER_ADMIN", "customer");
3250 std::env::set_var("REDDB_CUSTOMER_ADMIN_PASSWORD", "customer-pass");
3251 }
3252
3253 let (runtime, auth_store) = fresh_runtime_and_store();
3254 apply_preset(&runtime, &auth_store).expect("cloud preset applies cleanly");
3255
3256 let head = auth_store
3257 .get_user(None, "head")
3258 .expect("head admin should exist");
3259 assert_eq!(head.tenant_id, None);
3260 let customer = auth_store
3261 .get_user(None, "customer")
3262 .expect("customer admin should exist");
3263 assert_eq!(customer.tenant_id, None);
3264
3265 let ctx = EvalContext {
3266 principal_tenant: None,
3267 current_tenant: None,
3268 peer_ip: None,
3269 mfa_present: false,
3270 now_ms: 1_700_000_000_000,
3271 principal_is_admin_role: true,
3272 principal_is_platform_scoped: true,
3273 };
3274 assert!(auth_store.check_policy_authz(
3275 &UserId::platform("customer"),
3276 "config:write",
3277 &ResourceRef::new("config", "red.config.customer.enabled"),
3278 &ctx,
3279 ));
3280 assert!(
3281 auth_store.check_policy_authz(
3282 &UserId::platform("head"),
3283 "config:write",
3284 &ResourceRef::new("config", "red.config.cloud.enabled"),
3285 &ctx,
3286 ),
3287 "cloud head keeps allow-all authority over managed cloud config"
3288 );
3289 assert!(
3290 !auth_store.check_policy_authz(
3291 &UserId::platform("customer"),
3292 "config:write",
3293 &ResourceRef::new("config", "red.config.cloud.enabled"),
3294 &ctx,
3295 ),
3296 "cloud customer must be denied managed cloud config writes"
3297 );
3298 assert!(
3299 !auth_store.check_policy_authz(
3300 &UserId::platform("customer"),
3301 "policy:drop",
3302 &ResourceRef::new("policy", CLOUD_PROTECT_MANAGED_POLICY),
3303 &ctx,
3304 ),
3305 "cloud customer must be denied mutations of the managed guardrail policy"
3306 );
3307
3308 assert!(auth_store.get_user(None, "head").is_some());
3309 assert!(auth_store
3310 .get_policy(CLOUD_PROTECT_MANAGED_POLICY)
3311 .is_some());
3312 assert!(runtime
3313 .config_registry()
3314 .get_active(CLOUD_CONFIG_NAMESPACE)
3315 .is_some());
3316
3317 let store = runtime.db().store();
3318 match store
3319 .get_config(BOOTSTRAP_FIRST_ADMIN_KEY)
3320 .expect("head admin id persisted")
3321 {
3322 reddb_types::Value::Text(s) => assert_eq!(s.as_ref(), "head"),
3323 other => panic!("expected Text(head), got {other:?}"),
3324 }
3325 match store.get_config(BOOTSTRAP_PRESET_KEY).unwrap() {
3326 reddb_types::Value::Text(s) => assert_eq!(s.as_ref(), PRESET_CLOUD),
3327 other => panic!("expected Text(cloud), got {other:?}"),
3328 }
3329
3330 clear_preset_env();
3331 }
3332
3333 #[test]
3334 fn cloud_preset_customer_cannot_disable_head_admin_or_drop_guardrail_policy() {
3335 use crate::auth::Role;
3336 use crate::runtime::mvcc::{clear_current_auth_identity, set_current_auth_identity};
3337
3338 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3339 clear_preset_env();
3340
3341 let bootstrap = BootstrapConfig {
3342 preset: Some(PRESET_CLOUD.to_string()),
3343 cloud_head_admin: Some("red_admin".to_string()),
3344 cloud_head_admin_password: Some("head-pass".to_string()),
3345 customer_admin: Some("admin".to_string()),
3346 customer_admin_password: Some("customer-pass".to_string()),
3347 ..BootstrapConfig::default()
3348 };
3349 let (runtime, auth_store) = fresh_runtime_and_store();
3350 apply_preset_from_config(&runtime, &auth_store, &bootstrap)
3351 .expect("cloud preset applies cleanly");
3352 runtime.set_auth_store(Arc::clone(&auth_store));
3353
3354 set_current_auth_identity("admin".to_string(), Role::Admin);
3355 let disable_head = runtime.execute_query("ALTER USER red_admin DISABLE");
3356 let drop_guardrail =
3357 runtime.execute_query(&format!("DROP POLICY '{}'", CLOUD_PROTECT_MANAGED_POLICY));
3358 let create_user = runtime.execute_query("CREATE USER app_user WITH PASSWORD 'p' ROLE read");
3359 let disable_user = runtime.execute_query("ALTER USER app_user DISABLE");
3360 clear_current_auth_identity();
3361
3362 let disable_head_err = disable_head.expect_err("customer admin must not disable red_admin");
3363 assert!(
3364 disable_head_err.to_string().contains("user:disable"),
3365 "error should name the denied lifecycle action: {disable_head_err}"
3366 );
3367 assert!(
3368 auth_store
3369 .get_user(None, "red_admin")
3370 .expect("red_admin should still exist")
3371 .enabled,
3372 "denied mutation must leave red_admin enabled"
3373 );
3374
3375 let drop_guardrail_err =
3376 drop_guardrail.expect_err("customer admin must not drop cloud guardrail policy");
3377 assert!(
3378 drop_guardrail_err.to_string().contains("managed policy"),
3379 "error should name the managed policy guardrail: {drop_guardrail_err}"
3380 );
3381 assert!(auth_store
3382 .get_policy(CLOUD_PROTECT_MANAGED_POLICY)
3383 .is_some());
3384
3385 create_user.expect("customer admin should retain normal create-user authority");
3386 disable_user.expect("customer admin should retain normal user-disable authority");
3387 assert!(
3388 !auth_store
3389 .get_user(None, "app_user")
3390 .expect("app_user should exist")
3391 .enabled,
3392 "normal capability should still apply outside the reserved head admin"
3393 );
3394
3395 clear_preset_env();
3396 }
3397
3398 #[test]
3399 fn cloud_preset_cli_admin_alias_wins_over_cloud_env_password() {
3400 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3401 clear_preset_env();
3402 unsafe {
3404 std::env::set_var("REDDB_CLOUD_HEAD_ADMIN_PASSWORD", "env-head-pass");
3405 }
3406
3407 let bootstrap = BootstrapConfig {
3408 preset: Some(PRESET_CLOUD.to_string()),
3409 admin_username: Some("head".to_string()),
3410 admin_password: Some("cli-head-pass".to_string()),
3411 customer_admin: Some("customer".to_string()),
3412 customer_admin_password: Some("customer-pass".to_string()),
3413 ..BootstrapConfig::default()
3414 };
3415 let (runtime, auth_store) = fresh_runtime_and_store();
3416 apply_preset_from_config(&runtime, &auth_store, &bootstrap)
3417 .expect("cloud preset applies cleanly");
3418
3419 auth_store
3420 .authenticate("head", "cli-head-pass")
3421 .expect("CLI alias password should win");
3422 assert!(
3423 auth_store.authenticate("head", "env-head-pass").is_err(),
3424 "cloud-specific env password must not beat CLI alias"
3425 );
3426
3427 clear_preset_env();
3428 }
3429
3430 #[test]
3431 fn bootstrap_intent_manifest_matches_cloud_flag_path() {
3432 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3433 clear_preset_env();
3434
3435 let manifest_dir = std::env::current_dir()
3436 .expect("current dir")
3437 .join(".red/tmp/bootstrap-manifest-tests");
3438 std::fs::create_dir_all(&manifest_dir).expect("create manifest test dir");
3439 let unique = format!(
3440 "{}-{}",
3441 std::process::id(),
3442 std::time::SystemTime::now()
3443 .duration_since(std::time::UNIX_EPOCH)
3444 .unwrap_or_default()
3445 .as_millis()
3446 );
3447 let head_password = manifest_dir.join(format!("head-{unique}.secret"));
3448 let customer_password = manifest_dir.join(format!("customer-{unique}.secret"));
3449 let manifest_path = manifest_dir.join(format!("intent-{unique}.json"));
3450 std::fs::write(&head_password, "head-pass\n").expect("write head secret");
3451 std::fs::write(&customer_password, "customer-pass\n").expect("write customer secret");
3452 std::fs::write(
3453 &manifest_path,
3454 format!(
3455 r#"{{
3456 "bootstrap": {{
3457 "preset": "cloud",
3458 "cloud_head_admin": {{
3459 "username": "head",
3460 "password_file": "{}"
3461 }},
3462 "customer_admin": {{
3463 "username": "customer",
3464 "password_file": "{}"
3465 }}
3466 }}
3467 }}"#,
3468 head_password.display(),
3469 customer_password.display()
3470 ),
3471 )
3472 .expect("write manifest");
3473
3474 let mut config = no_auth_test_config(false);
3475 config.bootstrap.manifest = Some(manifest_path.clone());
3476 let options = config.to_db_options().expect("manifest options");
3477 assert!(options.auth.enabled);
3478 assert!(options.auth.require_auth);
3479 assert!(options.auth.vault_enabled);
3480
3481 let (runtime, auth_store) = fresh_runtime_and_store();
3482 apply_preset_from_config(&runtime, &auth_store, &config.bootstrap)
3483 .expect("manifest intent applies cleanly");
3484
3485 auth_store
3486 .authenticate("head", "head-pass")
3487 .expect("head password comes from file");
3488 auth_store
3489 .authenticate("customer", "customer-pass")
3490 .expect("customer password comes from file");
3491 assert!(auth_store.get_user(None, "head").is_some());
3492 assert!(auth_store.get_user(None, "customer").is_some());
3493 assert!(auth_store
3494 .get_policy(CLOUD_PROTECT_MANAGED_POLICY)
3495 .is_some());
3496 match runtime
3497 .db()
3498 .store()
3499 .get_config(BOOTSTRAP_PRESET_KEY)
3500 .expect("preset key persisted")
3501 {
3502 reddb_types::Value::Text(s) => assert_eq!(s.as_ref(), PRESET_CLOUD),
3503 other => panic!("expected Text(cloud), got {other:?}"),
3504 }
3505
3506 apply_preset_from_config(&runtime, &auth_store, &config.bootstrap)
3507 .expect("second boot is idempotent");
3508
3509 let _ = std::fs::remove_file(&manifest_path);
3510 let _ = std::fs::remove_file(&head_password);
3511 let _ = std::fs::remove_file(&customer_password);
3512 clear_preset_env();
3513 }
3514
3515 #[test]
3516 fn regulated_preset_enables_query_audit_infrastructure_without_rules() {
3517 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3518 clear_preset_env();
3519 unsafe {
3521 std::env::set_var(PRESET_ENV, PRESET_REGULATED);
3522 }
3523
3524 let (runtime, auth_store) = fresh_runtime_and_store();
3525 apply_preset(&runtime, &auth_store).expect("regulated preset applies cleanly");
3526
3527 assert!(runtime.query_audit().is_enabled());
3528 assert!(runtime.query_audit().rules().is_empty());
3529 assert!(
3530 runtime
3531 .db()
3532 .store()
3533 .get_collection(crate::runtime::query_audit::QUERY_AUDIT_COLLECTION)
3534 .is_some(),
3535 "regulated preset should create the query-audit stream"
3536 );
3537
3538 runtime
3539 .execute_query("CREATE TABLE docs (id INT)")
3540 .expect("create table");
3541 runtime
3542 .execute_query("INSERT INTO docs (id) VALUES (1)")
3543 .expect("insert");
3544 runtime.execute_query("SELECT * FROM docs").expect("select");
3545 let rows = runtime
3546 .db()
3547 .store()
3548 .get_collection(crate::runtime::query_audit::QUERY_AUDIT_COLLECTION)
3549 .expect("query audit collection")
3550 .query_all(|_| true);
3551 assert!(
3552 rows.is_empty(),
3553 "regulated preset must not globally audit every query"
3554 );
3555
3556 clear_preset_env();
3557 }
3558
3559 #[test]
3560 fn managed_backup_env_rejects_primary_replica_single_file_storage() {
3561 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3562 clear_backup_env();
3563 unsafe {
3565 std::env::set_var("REDDB_BACKUP_S3_ENDPOINT", "https://s3.example.test");
3566 std::env::set_var("REDDB_BACKUP_S3_BUCKET", "reddb");
3567 std::env::set_var("REDDB_BACKUP_S3_PREFIX", "clusters/prod");
3568 std::env::set_var("REDDB_BACKUP_S3_ACCESS_KEY_ID", "AK");
3569 std::env::set_var("REDDB_BACKUP_S3_SECRET_ACCESS_KEY", "SK");
3570 }
3571
3572 let mut config = no_auth_test_config(false);
3573 config.role = "primary".to_string();
3574 config.storage_profile = crate::storage::StorageDeployPreset::PrimaryReplicaDev.selection();
3575
3576 let err = config.to_db_options().unwrap_err();
3577 assert!(err.contains("managed backup"), "got: {err}");
3578 assert!(err.contains("operational-directory"), "got: {err}");
3579
3580 clear_backup_env();
3581 }
3582
3583 #[test]
3584 fn regulated_preset_installs_managed_evidence_guardrails_end_to_end() {
3585 use crate::auth::policies::{EvalContext, Policy, ResourceRef};
3586 use crate::auth::store::PrincipalRef;
3587 use crate::auth::{Role, UserId};
3588 use crate::runtime::mvcc::{clear_current_auth_identity, set_current_auth_identity};
3589 use reddb_types::Value;
3590
3591 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3592 clear_preset_env();
3593 unsafe {
3595 std::env::set_var(PRESET_ENV, PRESET_REGULATED);
3596 }
3597
3598 let options = no_auth_test_config(false)
3599 .to_db_options()
3600 .expect("regulated options");
3601 assert!(
3602 options.control_events.compliance_mode,
3603 "regulated preset must enable fail-closed control evidence before runtime boot"
3604 );
3605 assert!(
3606 options.query_audit.enabled && options.query_audit.rules.is_empty(),
3607 "regulated preset must enable query-audit infrastructure without global rules"
3608 );
3609
3610 let runtime = RedDBRuntime::with_options(options).expect("runtime");
3611 let auth_store = Arc::new(AuthStore::new(crate::auth::AuthConfig::default()));
3612 apply_preset(&runtime, &auth_store).expect("regulated preset applies cleanly");
3613 runtime.set_auth_store(Arc::clone(&auth_store));
3614
3615 assert!(runtime.control_events_require_persistence());
3616 assert!(runtime.query_audit().is_enabled());
3617 assert!(runtime.query_audit().rules().is_empty());
3618 assert!(auth_store
3619 .get_policy(REGULATED_PROTECT_MANAGED_POLICY)
3620 .is_some());
3621
3622 let managed_policy = runtime
3623 .config_registry()
3624 .get_active(REGULATED_PROTECT_MANAGED_POLICY)
3625 .expect("regulated managed policy registry entry");
3626 assert!(managed_policy.managed);
3627 assert_eq!(managed_policy.resource_type, "policy");
3628 assert!(
3629 runtime
3630 .config_registry()
3631 .get_active(REGULATED_AUDIT_CONFIG_NAMESPACE)
3632 .expect("regulated audit config namespace")
3633 .managed
3634 );
3635
3636 let registry_rows = runtime
3637 .execute_query(&format!(
3638 "SELECT id, managed FROM red.registry WHERE id = '{}'",
3639 REGULATED_PROTECT_MANAGED_POLICY
3640 ))
3641 .expect("red.registry query");
3642 assert_eq!(registry_rows.result.records.len(), 1);
3643 assert_eq!(
3644 registry_rows.result.records[0].get("managed"),
3645 Some(&Value::Boolean(true))
3646 );
3647
3648 let managed_policy_rows = runtime
3649 .execute_query(&format!(
3650 "SELECT policy_id FROM red.managed_policies WHERE policy_id = '{}'",
3651 REGULATED_PROTECT_MANAGED_POLICY
3652 ))
3653 .expect("red.managed_policies query");
3654 assert_eq!(managed_policy_rows.result.records.len(), 1);
3655
3656 let capability_rows = runtime
3657 .execute_query(
3658 "SELECT action FROM red.control_capabilities WHERE action = 'evidence:export'",
3659 )
3660 .expect("red.control_capabilities query");
3661 assert_eq!(capability_rows.result.records.len(), 1);
3662
3663 auth_store
3664 .create_user("alice", "p", Role::Admin)
3665 .expect("create ordinary admin");
3666 let allow_all = Policy::from_json_str(
3667 r#"{
3668 "id": "alice-allow-all",
3669 "version": 1,
3670 "statements": [{
3671 "effect": "allow",
3672 "actions": ["*"],
3673 "resources": ["*"]
3674 }]
3675 }"#,
3676 )
3677 .expect("allow-all policy");
3678 auth_store.put_policy(allow_all).expect("install allow-all");
3679 auth_store
3680 .attach_policy(
3681 PrincipalRef::User(UserId::platform("alice")),
3682 "alice-allow-all",
3683 )
3684 .expect("attach allow-all");
3685 let ctx = EvalContext {
3686 principal_tenant: None,
3687 current_tenant: None,
3688 peer_ip: None,
3689 mfa_present: false,
3690 now_ms: 1_700_000_000_000,
3691 principal_is_admin_role: true,
3692 principal_is_platform_scoped: true,
3693 };
3694 assert!(
3695 !auth_store.check_policy_authz(
3696 &UserId::platform("alice"),
3697 "policy:drop",
3698 &ResourceRef::new("policy", REGULATED_PROTECT_MANAGED_POLICY),
3699 &ctx,
3700 ),
3701 "managed guardrail deny policy must be effective for ordinary admins"
3702 );
3703
3704 set_current_auth_identity("alice".to_string(), Role::Admin);
3705 let denied = runtime.execute_query(&format!(
3706 "DROP POLICY '{}'",
3707 REGULATED_PROTECT_MANAGED_POLICY
3708 ));
3709 clear_current_auth_identity();
3710 let err = denied.expect_err("managed policy guardrail must deny ordinary admin");
3711 assert!(
3712 err.to_string().contains("managed policy"),
3713 "error should name the managed guardrail: {err}"
3714 );
3715 assert!(
3716 auth_store
3717 .get_policy(REGULATED_PROTECT_MANAGED_POLICY)
3718 .is_some(),
3719 "denied mutation must leave managed policy installed"
3720 );
3721
3722 let denied_events = runtime
3723 .execute_query(&format!(
3724 "SELECT action, resource, outcome FROM red.control_events \
3725 WHERE action = 'policy:drop' AND resource = 'policy:{}'",
3726 REGULATED_PROTECT_MANAGED_POLICY
3727 ))
3728 .expect("red.control_events denied policy drop");
3729 assert_eq!(denied_events.result.records.len(), 1);
3730 assert_eq!(
3731 denied_events.result.records[0].get("outcome"),
3732 Some(&Value::text("denied"))
3733 );
3734
3735 set_current_auth_identity("alice".to_string(), Role::Admin);
3736 let config_denied = runtime.execute_query("SET CONFIG red.config.audit.enabled = true");
3737 clear_current_auth_identity();
3738 let err = config_denied.expect_err("managed config guardrail must deny ordinary admin");
3739 assert!(
3740 err.to_string().contains("managed config"),
3741 "error should name the managed config guardrail: {err}"
3742 );
3743
3744 let denied_config_events = runtime
3745 .execute_query(
3746 "SELECT action, resource, outcome FROM red.control_events \
3747 WHERE action = 'config:write' AND resource = 'config:red.config.audit.enabled'",
3748 )
3749 .expect("red.control_events denied config write");
3750 assert_eq!(denied_config_events.result.records.len(), 1);
3751 assert_eq!(
3752 denied_config_events.result.records[0].get("outcome"),
3753 Some(&Value::text("denied"))
3754 );
3755
3756 runtime
3757 .execute_query("CREATE TABLE regulated_docs (id INT)")
3758 .expect("create user table");
3759 runtime
3760 .execute_query("SELECT * FROM regulated_docs")
3761 .expect("select user table");
3762 let audit_rows = runtime
3763 .db()
3764 .store()
3765 .get_collection(crate::runtime::query_audit::QUERY_AUDIT_COLLECTION)
3766 .expect("query audit collection")
3767 .query_all(|_| true);
3768 assert!(
3769 audit_rows.is_empty(),
3770 "regulated preset must not globally audit data-plane queries"
3771 );
3772
3773 clear_preset_env();
3774 }
3775
3776 #[test]
3777 fn bootstrap_manifest_installs_initial_users_policies_guardrails_and_config() {
3778 use crate::auth::policies::{EvalContext, ResourceRef};
3779 use crate::auth::UserId;
3780 use reddb_types::Value;
3781
3782 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3783 clear_preset_env();
3784
3785 let manifest_dir = std::env::current_dir()
3786 .expect("current dir")
3787 .join(".red/tmp/bootstrap-manifest-tests");
3788 std::fs::create_dir_all(&manifest_dir).expect("create manifest test dir");
3789 let manifest_path = manifest_dir.join(format!(
3790 "reddb-bootstrap-manifest-{}-{}.json",
3791 std::process::id(),
3792 std::time::SystemTime::now()
3793 .duration_since(std::time::UNIX_EPOCH)
3794 .unwrap_or_default()
3795 .as_millis()
3796 ));
3797 std::fs::write(
3798 &manifest_path,
3799 r#"{
3800 "users": [
3801 {
3802 "username": "ops",
3803 "password": "hunter2",
3804 "role": "admin"
3805 }
3806 ],
3807 "policies": [
3808 {
3809 "id": "bootstrap-registry-admin",
3810 "version": 1,
3811 "statements": [
3812 {
3813 "effect": "allow",
3814 "actions": ["red.registry:*", "policy:*", "config:write", "select"],
3815 "resources": ["registry:*", "policy:*", "config:*", "collection:docs"]
3816 }
3817 ]
3818 }
3819 ],
3820 "managed_policies": [
3821 {
3822 "id": "managed-deny-drop",
3823 "version": 1,
3824 "statements": [
3825 {
3826 "effect": "deny",
3827 "actions": ["policy:drop"],
3828 "resources": ["policy:managed-deny-drop"]
3829 }
3830 ],
3831 "required_resource": "policy:managed-deny-drop",
3832 "evidence": "full"
3833 }
3834 ],
3835 "attachments": [
3836 {"user": "ops", "policy": "bootstrap-registry-admin"}
3837 ],
3838 "managed_config_namespaces": [
3839 {
3840 "id": "red.ai",
3841 "required_action": "config:write",
3842 "required_resource": "config:red.ai.*",
3843 "evidence": "metadata"
3844 }
3845 ],
3846 "config": [
3847 {"key": "red.ai.default.provider", "value": "openai"},
3848 {
3849 "key": "red.ai.openai.default.secret_ref",
3850 "secret_ref": {"collection": "red.vault", "key": "openai"}
3851 }
3852 ],
3853 "actor": "ops"
3854 }"#,
3855 )
3856 .expect("write manifest");
3857 unsafe {
3859 std::env::set_var("REDDB_BOOTSTRAP_MANIFEST", &manifest_path);
3860 }
3861
3862 let (runtime, auth_store) = fresh_runtime_and_store();
3863 apply_preset(&runtime, &auth_store).expect("manifest applies cleanly");
3864
3865 let users = auth_store.list_users();
3866 assert_eq!(users.len(), 1);
3867 assert_eq!(users[0].username, "ops");
3868 assert!(users[0].tenant_id.is_none());
3869
3870 let actor = UserId::platform("ops");
3871 let ctx = EvalContext {
3872 principal_tenant: None,
3873 current_tenant: None,
3874 peer_ip: None,
3875 mfa_present: false,
3876 now_ms: 1_700_000_000_000,
3877 principal_is_admin_role: true,
3878 principal_is_platform_scoped: true,
3879 };
3880 assert!(auth_store.check_policy_authz(
3882 &actor,
3883 "select",
3884 &ResourceRef::new("collection", "docs"),
3885 &ctx
3886 ));
3887
3888 let managed_policy = runtime
3889 .config_registry()
3890 .get_active("managed-deny-drop")
3891 .expect("managed policy registry entry");
3892 assert!(managed_policy.managed);
3893 assert_eq!(managed_policy.resource_type, "policy");
3894 let managed_config = runtime
3895 .config_registry()
3896 .get_active("red.ai")
3897 .expect("managed config namespace registry entry");
3898 assert!(managed_config.managed);
3899 assert_eq!(managed_config.resource_type, "config_namespace");
3900
3901 let store = runtime.db().store();
3902 match store
3903 .get_config("red.ai.default.provider")
3904 .expect("plain config persisted")
3905 {
3906 Value::Text(s) => assert_eq!(s.as_ref(), "openai"),
3907 other => panic!("expected provider text, got {other:?}"),
3908 }
3909 let Value::Json(bytes) = store
3910 .get_config("red.ai.openai.default.secret_ref")
3911 .expect("secret ref config persisted")
3912 else {
3913 panic!("secret ref must be stored as structured JSON");
3914 };
3915 let reference: crate::serde_json::Value =
3916 crate::serde_json::from_slice(&bytes).expect("secret ref json");
3917 assert_eq!(
3918 reference.get("type").and_then(|v| v.as_str()),
3919 Some("secret_ref")
3920 );
3921 assert!(
3922 !String::from_utf8_lossy(&bytes).contains("hunter2"),
3923 "manifest password must not leak into secret ref config"
3924 );
3925
3926 let completed = store
3927 .get_config(BOOTSTRAP_COMPLETED_KEY)
3928 .expect("bootstrap completion persisted");
3929 assert!(matches!(completed, Value::Boolean(true)));
3930 assert!(
3931 store
3932 .get_config("system.bootstrap.manifest.registry_entries")
3933 .is_some(),
3934 "managed registry entries must be persisted internally"
3935 );
3936
3937 std::fs::remove_file(&manifest_path).expect("remove manifest after first boot");
3938 let restored_registry = Arc::new(crate::auth::registry::ConfigRegistry::new());
3939 crate::cli::bootstrap_manifest::rehydrate_manifest_registry(&runtime, &restored_registry)
3940 .expect("registry rehydrates without manifest file");
3941 assert!(restored_registry.get_active("managed-deny-drop").is_some());
3942 assert!(restored_registry.get_active("red.ai").is_some());
3943
3944 let fresh = Arc::new(AuthStore::new(crate::auth::AuthConfig::default()));
3945 apply_preset(&runtime, &fresh).expect("re-run must not need manifest file");
3946 assert!(fresh.needs_bootstrap());
3947
3948 clear_preset_env();
3949 }
3950
3951 #[test]
3952 fn production_preset_refuses_to_start_without_password() {
3953 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3954 clear_preset_env();
3955 unsafe {
3957 std::env::set_var(PRESET_ENV, PRESET_PRODUCTION);
3958 std::env::set_var("REDDB_USERNAME", "ops");
3959 }
3960
3961 let (runtime, auth_store) = fresh_runtime_and_store();
3962 let err = apply_preset(&runtime, &auth_store).expect_err("must reject missing password");
3963 assert!(
3964 err.contains("REDDB_PASSWORD"),
3965 "error must name the missing env: {err}"
3966 );
3967
3968 assert!(auth_store.needs_bootstrap());
3970 assert!(runtime
3971 .db()
3972 .store()
3973 .get_config(BOOTSTRAP_COMPLETED_KEY)
3974 .is_none());
3975
3976 clear_preset_env();
3977 }
3978
3979 #[test]
3980 fn re_running_production_after_first_boot_is_a_silent_skip() {
3981 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3982 clear_preset_env();
3983 unsafe {
3985 std::env::set_var(PRESET_ENV, PRESET_PRODUCTION);
3986 std::env::set_var("REDDB_USERNAME", "ops");
3987 std::env::set_var("REDDB_PASSWORD", "hunter2");
3988 }
3989
3990 let (runtime, auth_store) = fresh_runtime_and_store();
3991 apply_preset(&runtime, &auth_store).expect("first apply");
3992 assert_eq!(auth_store.list_users().len(), 1);
3993
3994 let fresh = Arc::new(AuthStore::new(crate::auth::AuthConfig::default()));
4001 apply_preset(&runtime, &fresh).expect("re-run is silent-skip");
4002 assert!(
4003 fresh.needs_bootstrap(),
4004 "re-run must not create a second admin"
4005 );
4006 assert!(
4007 fresh.get_policy(FIRST_ADMIN_ALLOW_ALL_POLICY).is_none(),
4008 "re-run must not re-install the allow-all policy on the fresh store"
4009 );
4010
4011 clear_preset_env();
4012 }
4013
4014 #[test]
4015 fn unrecognised_preset_value_is_rejected() {
4016 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
4017 clear_preset_env();
4018 unsafe {
4020 std::env::set_var(PRESET_ENV, "weird");
4021 }
4022
4023 let (runtime, auth_store) = fresh_runtime_and_store();
4024 let err = apply_preset(&runtime, &auth_store).expect_err("must reject unknown preset");
4025 assert!(err.contains("weird"), "error must echo the value: {err}");
4026 assert!(auth_store.needs_bootstrap());
4027
4028 clear_preset_env();
4029 }
4030
4031 #[test]
4032 fn no_auth_short_circuits_preset_entirely() {
4033 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
4034 clear_preset_env();
4035 unsafe {
4038 std::env::set_var(PRESET_ENV, PRESET_PRODUCTION);
4039 std::env::set_var("REDDB_USERNAME", "ops");
4040 std::env::set_var("REDDB_PASSWORD", "hunter2");
4041 }
4042
4043 let options = no_auth_test_config(true)
4044 .to_db_options()
4045 .expect("to_db_options");
4046 assert!(no_auth_active(&options));
4047
4048 let (runtime, auth_store) = fresh_runtime_and_store();
4051 if !no_auth_active(&options) {
4052 apply_preset(&runtime, &auth_store).expect("would apply preset");
4053 }
4054
4055 assert!(
4056 auth_store.needs_bootstrap(),
4057 "--no-auth must prevent any admin creation"
4058 );
4059 assert!(
4060 runtime
4061 .db()
4062 .store()
4063 .get_config(BOOTSTRAP_COMPLETED_KEY)
4064 .is_none(),
4065 "--no-auth must skip bootstrap-state persistence"
4066 );
4067
4068 clear_preset_env();
4069 }
4070
4071 #[test]
4072 fn implicit_bind_collision_degrades() {
4073 let held = TcpListener::bind("127.0.0.1:0").expect("hold test port");
4074 let addr = held.local_addr().expect("held addr").to_string();
4075 let mut readiness = TransportReadiness::default();
4076
4077 let listener =
4078 bind_listener_for_startup(&mut readiness, "http", &addr, false).expect("nonfatal");
4079
4080 assert!(listener.is_none());
4081 assert_eq!(readiness.active.len(), 0);
4082 assert_eq!(readiness.failed.len(), 1);
4083 assert!(!readiness.failed[0].explicit);
4084 assert_eq!(readiness.failed[0].bind_addr, addr);
4085 }
4086}