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 mut backend = crate::storage::backend::S3Backend::new(s3_cfg);
648 if let Some(path) = &cfg.credentials_file {
649 backend = backend.with_credentials_file(path);
650 }
651 let backend = Arc::new(backend);
652 options.remote_backend = Some(backend.clone());
653 options.remote_backend_atomic = Some(backend);
654 let trimmed = cfg.prefix.trim_end_matches('/');
659 options.remote_key = Some(reddb_file::remote_database_key(trimmed));
660
661 tracing::info!(
662 backend = "s3",
663 endpoint = %endpoint_host,
664 bucket = %cfg.bucket,
665 prefix = %cfg.prefix,
666 checkpoint_interval_secs = cfg.checkpoint_interval_secs,
667 wal_flush_interval_secs = cfg.wal_flush_interval_secs,
668 "backup backend configured from REDDB_BACKUP_* env"
669 );
670 }
671
672 #[cfg(not(feature = "backend-s3"))]
673 {
674 tracing::warn!(
675 backend = "s3",
676 endpoint = %endpoint_host,
677 bucket = %cfg.bucket,
678 prefix = %cfg.prefix,
679 "REDDB_BACKUP_S3_* configured but binary built without `backend-s3` feature; \
680 backend wiring skipped (archiver/checkpointer also disabled)"
681 );
682 }
683}
684
685fn endpoint_host(endpoint: &str) -> &str {
686 let after_scheme = endpoint
687 .split_once("://")
688 .map(|(_, r)| r)
689 .unwrap_or(endpoint);
690 after_scheme.split('/').next().unwrap_or(after_scheme)
691}
692
693fn spawn_backup_tasks_if_configured(
699 options: &RedDBOptions,
700 runtime: &RedDBRuntime,
701) -> Option<BackupTasksHandle> {
702 let checkpoint_secs: u64 = options
703 .metadata
704 .get(BACKUP_INTERVAL_META_CHECKPOINT)?
705 .parse()
706 .ok()?;
707 let wal_secs: u64 = options
708 .metadata
709 .get(BACKUP_INTERVAL_META_WAL_FLUSH)?
710 .parse()
711 .ok()?;
712 let pause_on_lag_secs: u64 = options
715 .metadata
716 .get(BACKUP_PAUSE_ON_LAG_META)
717 .and_then(|raw| raw.parse().ok())
718 .unwrap_or(0);
719 options.remote_backend.as_ref()?;
720
721 let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
722
723 if pause_on_lag_secs > 0 {
728 let now_ms = std::time::SystemTime::now()
729 .duration_since(std::time::UNIX_EPOCH)
730 .map(|d| d.as_millis() as u64)
731 .unwrap_or(0);
732 runtime
733 .write_gate()
734 .configure_archive_lag_pause(pause_on_lag_secs, now_ms);
735 tracing::info!(
736 pause_on_lag_secs,
737 "archive-lag pause enabled — engine will transition to read-only after threshold seconds of archiver silence"
738 );
739 }
740
741 let checkpoint_handle = {
742 let stop = Arc::clone(&stop);
743 let runtime = runtime.clone();
744 let interval = Duration::from_secs(checkpoint_secs);
745 thread::Builder::new()
746 .name("red-checkpointer".into())
747 .spawn(move || {
748 periodic_loop(stop, interval, move || {
749 if let Err(err) = runtime.checkpoint() {
750 tracing::warn!(error = %err, "periodic checkpoint failed");
751 }
752 })
753 })
754 .ok()
755 };
756
757 let archiver_handle = {
758 let stop = Arc::clone(&stop);
759 let runtime = runtime.clone();
760 let interval = Duration::from_secs(wal_secs);
761 let lag_enabled = pause_on_lag_secs > 0;
762 thread::Builder::new()
763 .name("red-wal-archiver".into())
764 .spawn(move || {
765 periodic_loop(stop, interval, move || match runtime.trigger_backup() {
766 Ok(_) if lag_enabled => {
767 let now_ms = std::time::SystemTime::now()
768 .duration_since(std::time::UNIX_EPOCH)
769 .map(|d| d.as_millis() as u64)
770 .unwrap_or(0);
771 runtime.write_gate().record_archive_success(now_ms);
772 runtime.write_gate().evaluate_archive_lag(now_ms);
776 }
777 Ok(_) => {}
778 Err(err) => {
779 tracing::warn!(error = %err, "periodic WAL archive/backup failed");
780 }
781 })
782 })
783 .ok()
784 };
785
786 let lag_monitor_handle = if pause_on_lag_secs > 0 {
791 let stop = Arc::clone(&stop);
792 let runtime = runtime.clone();
793 let interval = Duration::from_secs(5);
797 thread::Builder::new()
798 .name("red-archive-lag-monitor".into())
799 .spawn(move || {
800 periodic_loop(stop, interval, move || {
801 let now_ms = std::time::SystemTime::now()
802 .duration_since(std::time::UNIX_EPOCH)
803 .map(|d| d.as_millis() as u64)
804 .unwrap_or(0);
805 let was_paused = runtime.write_gate().is_auto_paused();
806 let now_paused = runtime.write_gate().evaluate_archive_lag(now_ms);
807 if now_paused && !was_paused {
808 tracing::warn!(
809 pause_on_lag_secs,
810 last_archive_at_ms = runtime.write_gate().last_archive_at_ms(),
811 "WAL archive lag exceeded threshold — entering graceful read-only mode (issue #519)"
812 );
813 } else if !now_paused && was_paused {
814 tracing::info!(
815 "WAL archive caught up — exiting graceful read-only mode (issue #519)"
816 );
817 }
818 })
819 })
820 .ok()
821 } else {
822 None
823 };
824
825 tracing::info!(
826 checkpoint_interval_secs = checkpoint_secs,
827 wal_flush_interval_secs = wal_secs,
828 "backup tasks spawned (checkpointer + WAL archiver)"
829 );
830
831 Some(BackupTasksHandle {
832 stop,
833 _checkpoint_handle: checkpoint_handle,
834 _archiver_handle: archiver_handle,
835 _lag_monitor_handle: lag_monitor_handle,
836 })
837}
838
839pub struct BackupTasksHandle {
842 stop: Arc<std::sync::atomic::AtomicBool>,
843 _checkpoint_handle: Option<thread::JoinHandle<()>>,
844 _archiver_handle: Option<thread::JoinHandle<()>>,
845 _lag_monitor_handle: Option<thread::JoinHandle<()>>,
848}
849
850impl Drop for BackupTasksHandle {
851 fn drop(&mut self) {
852 self.stop.store(true, std::sync::atomic::Ordering::Release);
853 }
854}
855
856fn periodic_loop<F: FnMut()>(
857 stop: Arc<std::sync::atomic::AtomicBool>,
858 interval: Duration,
859 mut tick: F,
860) {
861 let wake = Duration::from_secs(1);
864 let mut elapsed = Duration::ZERO;
865 while !stop.load(std::sync::atomic::Ordering::Acquire) {
866 thread::sleep(wake);
867 elapsed += wake;
868 if elapsed >= interval {
869 tick();
870 elapsed = Duration::ZERO;
871 }
872 }
873}
874
875fn configure_remote_backend_from_env(options: &mut RedDBOptions) {
876 let backend = env_nonempty("RED_BACKEND")
882 .or_else(|| env_nonempty("REDDB_REMOTE_BACKEND"))
883 .unwrap_or_else(|| "none".to_string())
884 .to_ascii_lowercase();
885
886 match backend.as_str() {
887 "s3" | "minio" | "r2" => {
892 #[cfg(feature = "backend-s3")]
893 {
894 if let Some(config) = s3_config_from_env() {
895 let remote_key = env_nonempty("RED_REMOTE_KEY")
896 .or_else(|| env_nonempty("REDDB_REMOTE_KEY"))
897 .unwrap_or_else(|| reddb_file::remote_database_key("clusters/dev"));
898 let backend = Arc::new(crate::storage::backend::S3Backend::new(config));
899 options.remote_backend = Some(backend.clone());
900 options.remote_backend_atomic = Some(backend);
901 options.remote_key = Some(remote_key);
902 }
903 }
904 #[cfg(not(feature = "backend-s3"))]
905 {
906 tracing::warn!(
907 backend = %backend,
908 "RED_BACKEND={backend} requested but binary was built without `backend-s3` feature"
909 );
910 }
911 }
912 "fs" | "local" => {
917 let base_path = env_nonempty("RED_FS_PATH").or_else(|| env_nonempty("REDDB_FS_PATH"));
918 let remote_key = match (
919 base_path,
920 env_nonempty("RED_REMOTE_KEY").or_else(|| env_nonempty("REDDB_REMOTE_KEY")),
921 ) {
922 (Some(base), Some(rel)) => Some(format!(
923 "{}/{}",
924 base.trim_end_matches('/'),
925 rel.trim_start_matches('/')
926 )),
927 (Some(base), None) => Some(reddb_file::remote_database_key(&format!(
928 "{}/clusters/dev",
929 base.trim_end_matches('/')
930 ))),
931 (None, Some(rel)) => Some(rel),
932 (None, None) => None,
933 };
934 if let Some(key) = remote_key {
935 let backend = Arc::new(crate::storage::backend::LocalBackend);
936 options.remote_backend = Some(backend.clone());
937 options.remote_backend_atomic = Some(backend);
938 options.remote_key = Some(key);
939 }
940 }
941 "http" => {
946 let base_url = match env_nonempty("RED_HTTP_BACKEND_URL")
947 .or_else(|| env_nonempty("REDDB_HTTP_BACKEND_URL"))
948 {
949 Some(u) => u,
950 None => {
951 tracing::warn!(
952 "RED_BACKEND=http requires RED_HTTP_BACKEND_URL — backend disabled"
953 );
954 return;
955 }
956 };
957 let prefix = env_nonempty("RED_HTTP_BACKEND_PREFIX")
958 .or_else(|| env_nonempty("REDDB_HTTP_BACKEND_PREFIX"))
959 .unwrap_or_default();
960 let auth_header = if let Some(path) = env_nonempty("RED_HTTP_BACKEND_AUTH_HEADER_FILE")
961 .or_else(|| env_nonempty("REDDB_HTTP_BACKEND_AUTH_HEADER_FILE"))
962 {
963 std::fs::read_to_string(&path)
964 .ok()
965 .map(|s| s.trim().to_string())
966 .filter(|s| !s.is_empty())
967 } else {
968 env_nonempty("RED_HTTP_BACKEND_AUTH_HEADER")
969 .or_else(|| env_nonempty("REDDB_HTTP_BACKEND_AUTH_HEADER"))
970 };
971
972 let mut config =
973 crate::storage::backend::HttpBackendConfig::new(base_url).with_prefix(prefix);
974 if let Some(auth) = auth_header {
975 config = config.with_auth_header(auth);
976 }
977 let conditional_writes = env_truthy("RED_HTTP_CONDITIONAL_WRITES")
978 || env_truthy("RED_HTTP_BACKEND_CONDITIONAL_WRITES")
979 || env_truthy("REDDB_HTTP_BACKEND_CONDITIONAL_WRITES");
980 config = config.with_conditional_writes(conditional_writes);
981 if conditional_writes {
986 match crate::storage::backend::AtomicHttpBackend::try_new(config.clone()) {
987 Ok(atomic) => {
988 let atomic_arc = Arc::new(atomic);
989 options.remote_backend = Some(atomic_arc.clone());
990 options.remote_backend_atomic = Some(atomic_arc);
991 }
992 Err(err) => {
993 tracing::warn!(error = %err, "AtomicHttpBackend init failed; falling back to plain HTTP (no CAS)");
994 options.remote_backend =
995 Some(Arc::new(crate::storage::backend::HttpBackend::new(config)));
996 }
997 }
998 } else {
999 options.remote_backend =
1000 Some(Arc::new(crate::storage::backend::HttpBackend::new(config)));
1001 }
1002 options.remote_key = env_nonempty("RED_REMOTE_KEY")
1003 .or_else(|| env_nonempty("REDDB_REMOTE_KEY"))
1004 .or_else(|| Some(reddb_file::remote_database_key("clusters/dev")));
1005 }
1006 "none" | "" => {}
1009 other => {
1010 tracing::warn!(
1011 backend = %other,
1012 "unknown RED_BACKEND value — supported: s3 | fs | http | none"
1013 );
1014 }
1015 }
1016}
1017
1018#[cfg(feature = "backend-s3")]
1023fn env_s3(suffix: &str) -> Option<String> {
1024 env_nonempty(&format!("RED_S3_{suffix}"))
1025 .or_else(|| env_nonempty(&format!("REDDB_S3_{suffix}")))
1026}
1027
1028#[cfg(feature = "backend-s3")]
1034fn env_s3_secret(suffix: &str) -> Option<String> {
1035 let file_key_red = format!("RED_S3_{suffix}_FILE");
1036 let file_key_legacy = format!("REDDB_S3_{suffix}_FILE");
1037 if let Some(path) = env_nonempty(&file_key_red).or_else(|| env_nonempty(&file_key_legacy)) {
1038 return std::fs::read_to_string(&path)
1039 .ok()
1040 .map(|s| s.trim().to_string())
1041 .filter(|s| !s.is_empty());
1042 }
1043 env_s3(suffix)
1044}
1045
1046#[cfg(feature = "backend-s3")]
1047fn s3_config_from_env() -> Option<crate::storage::backend::S3Config> {
1048 let endpoint = env_s3("ENDPOINT")?;
1049 let bucket = env_s3("BUCKET")?;
1050 let access_key = env_s3_secret("ACCESS_KEY")?;
1051 let secret_key = env_s3_secret("SECRET_KEY")?;
1052 let region = env_s3("REGION").unwrap_or_else(|| "us-east-1".to_string());
1053 let key_prefix = env_s3("KEY_PREFIX")
1054 .or_else(|| env_s3("PREFIX"))
1055 .unwrap_or_default();
1056 let path_style = env_s3("PATH_STYLE")
1057 .map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on"))
1058 .unwrap_or(true);
1059 Some(crate::storage::backend::S3Config {
1060 endpoint,
1061 bucket,
1062 key_prefix,
1063 access_key,
1064 secret_key,
1065 region,
1066 path_style,
1067 })
1068}
1069
1070pub fn render_systemd_unit(config: &SystemdServiceConfig) -> String {
1071 let data_dir = config.data_dir();
1072 let exec_start = render_systemd_exec_start(config);
1073 format!(
1074 "[Unit]\n\
1075Description=RedDB unified database service\n\
1076After=network-online.target\n\
1077Wants=network-online.target\n\
1078\n\
1079[Service]\n\
1080Type=simple\n\
1081User={user}\n\
1082Group={group}\n\
1083WorkingDirectory={workdir}\n\
1084ExecStart={exec_start}\n\
1085Restart=always\n\
1086RestartSec=2\n\
1087LimitSTACK=16M\n\
1088NoNewPrivileges=true\n\
1089PrivateTmp=true\n\
1090ProtectSystem=strict\n\
1091ProtectHome=true\n\
1092ProtectControlGroups=true\n\
1093ProtectKernelTunables=true\n\
1094ProtectKernelModules=true\n\
1095RestrictNamespaces=true\n\
1096LockPersonality=true\n\
1097MemoryDenyWriteExecute=true\n\
1098ReadWritePaths={workdir}\n\
1099\n\
1100[Install]\n\
1101WantedBy=multi-user.target\n",
1102 user = config.run_user,
1103 group = config.run_group,
1104 workdir = data_dir.display(),
1105 exec_start = exec_start,
1106 )
1107}
1108
1109#[cfg(target_os = "linux")]
1118pub fn install_systemd_service(config: &SystemdServiceConfig) -> Result<(), String> {
1119 ensure_root()?;
1120 ensure_command_available("systemctl")?;
1121 ensure_command_available("getent")?;
1122 ensure_command_available("groupadd")?;
1123 ensure_command_available("useradd")?;
1124 ensure_command_available("install")?;
1125 ensure_executable(&config.binary_path)?;
1126
1127 if !command_success("getent", ["group", config.run_group.as_str()])? {
1128 run_command("groupadd", ["--system", config.run_group.as_str()])?;
1129 }
1130
1131 if !command_success("id", ["-u", config.run_user.as_str()])? {
1132 let data_dir = config.data_dir();
1133 run_command(
1134 "useradd",
1135 [
1136 "--system",
1137 "--gid",
1138 config.run_group.as_str(),
1139 "--home-dir",
1140 data_dir.to_string_lossy().as_ref(),
1141 "--shell",
1142 "/usr/sbin/nologin",
1143 config.run_user.as_str(),
1144 ],
1145 )?;
1146 }
1147
1148 let data_dir = config.data_dir();
1149 run_command(
1150 "install",
1151 [
1152 "-d",
1153 "-o",
1154 config.run_user.as_str(),
1155 "-g",
1156 config.run_group.as_str(),
1157 "-m",
1158 "0750",
1159 data_dir.to_string_lossy().as_ref(),
1160 ],
1161 )?;
1162
1163 std::fs::write(config.unit_path(), render_systemd_unit(config))
1164 .map_err(|err| format!("failed to write systemd unit: {err}"))?;
1165
1166 run_command("systemctl", ["daemon-reload"])?;
1167 run_command(
1168 "systemctl",
1169 [
1170 "enable",
1171 "--now",
1172 format!("{}.service", config.service_name).as_str(),
1173 ],
1174 )?;
1175
1176 Ok(())
1177}
1178
1179#[cfg(not(target_os = "linux"))]
1184pub fn install_systemd_service(_config: &SystemdServiceConfig) -> Result<(), String> {
1185 Err("systemd install is Linux-only — use sc.exe (Windows) or \
1186 launchd (macOS) to install the service manually using the \
1187 unit printed by `red service print-unit`"
1188 .to_string())
1189}
1190
1191#[cfg(target_os = "linux")]
1192fn ensure_root() -> Result<(), String> {
1193 let output = Command::new("id")
1194 .arg("-u")
1195 .output()
1196 .map_err(|err| format!("failed to determine current uid: {err}"))?;
1197 if !output.status.success() {
1198 return Err("failed to determine current uid".to_string());
1199 }
1200 let uid = String::from_utf8_lossy(&output.stdout);
1201 if uid.trim() != "0" {
1202 return Err("run this command as root (sudo)".to_string());
1203 }
1204 Ok(())
1205}
1206
1207#[cfg(target_os = "linux")]
1208fn ensure_command_available(command: &str) -> Result<(), String> {
1209 let status = Command::new("sh")
1210 .args(["-lc", &format!("command -v {command} >/dev/null 2>&1")])
1211 .status()
1212 .map_err(|err| format!("failed to check command '{command}': {err}"))?;
1213 if status.success() {
1214 Ok(())
1215 } else {
1216 Err(format!("required command not found: {command}"))
1217 }
1218}
1219
1220#[cfg(target_os = "linux")]
1221fn ensure_executable(path: &std::path::Path) -> Result<(), String> {
1222 let metadata = std::fs::metadata(path)
1223 .map_err(|err| format!("binary not found '{}': {err}", path.display()))?;
1224 #[cfg(unix)]
1225 {
1226 use std::os::unix::fs::PermissionsExt;
1227 if metadata.permissions().mode() & 0o111 == 0 {
1228 return Err(format!("binary is not executable: {}", path.display()));
1229 }
1230 }
1231 #[cfg(not(unix))]
1232 {
1233 if !metadata.is_file() {
1234 return Err(format!("binary is not a file: {}", path.display()));
1235 }
1236 }
1237 Ok(())
1238}
1239
1240#[cfg(target_os = "linux")]
1241fn command_success<const N: usize>(program: &str, args: [&str; N]) -> Result<bool, String> {
1242 Command::new(program)
1243 .args(args)
1244 .status()
1245 .map(|status| status.success())
1246 .map_err(|err| format!("failed to run {program}: {err}"))
1247}
1248
1249#[cfg(target_os = "linux")]
1250fn run_command<const N: usize>(program: &str, args: [&str; N]) -> Result<(), String> {
1251 let output = Command::new(program)
1252 .args(args)
1253 .output()
1254 .map_err(|err| format!("failed to run {program}: {err}"))?;
1255 if output.status.success() {
1256 return Ok(());
1257 }
1258
1259 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
1260 let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
1261 let detail = if !stderr.is_empty() {
1262 stderr
1263 } else if !stdout.is_empty() {
1264 stdout
1265 } else {
1266 format!("exit status {}", output.status)
1267 };
1268 Err(format!("{program} failed: {detail}"))
1269}
1270
1271fn render_systemd_exec_start(config: &SystemdServiceConfig) -> String {
1272 let mut parts = vec![
1273 config.binary_path.display().to_string(),
1274 "server".to_string(),
1275 "--path".to_string(),
1276 config.data_path.display().to_string(),
1277 ];
1278
1279 if let Some(bind_addr) = &config.router_bind_addr {
1280 parts.push("--bind".to_string());
1281 parts.push(bind_addr.clone());
1282 } else if let Some(bind_addr) = &config.grpc_bind_addr {
1283 parts.push("--grpc-bind".to_string());
1284 parts.push(bind_addr.clone());
1285 }
1286 if let Some(bind_addr) = &config.http_bind_addr {
1287 parts.push("--http-bind".to_string());
1288 parts.push(bind_addr.clone());
1289 }
1290
1291 parts.join(" ")
1292}
1293
1294pub fn probe_listener(target: &str, timeout: Duration) -> bool {
1295 let addresses: Vec<SocketAddr> = match target.to_socket_addrs() {
1296 Ok(addresses) => addresses.collect(),
1297 Err(_) => return false,
1298 };
1299
1300 addresses
1301 .into_iter()
1302 .any(|address| TcpStream::connect_timeout(&address, timeout).is_ok())
1303}
1304
1305pub fn bind_listener_for_startup(
1323 readiness: &mut TransportReadiness,
1324 transport: &str,
1325 bind_addr: &str,
1326 explicit: bool,
1327) -> Result<Option<TcpListener>, String> {
1328 match TcpListener::bind(bind_addr) {
1329 Ok(listener) => {
1330 readiness.active(transport, bind_addr, explicit);
1331 Ok(Some(listener))
1332 }
1333 Err(err) => {
1334 let reason = format!("{transport} listener bind {bind_addr}: {err}");
1335 readiness.failed(transport, bind_addr, explicit, reason.clone());
1336 if explicit {
1337 tracing::error!(
1338 transport,
1339 bind = %bind_addr,
1340 error = %err,
1341 "fatal explicit bind failure"
1342 );
1343 Err(format!("explicit {reason}"))
1344 } else {
1345 tracing::warn!(
1346 transport,
1347 bind = %bind_addr,
1348 error = %err,
1349 "non-fatal implicit bind failure; listener degraded"
1350 );
1351 Ok(None)
1352 }
1353 }
1354 }
1355}
1356
1357async fn spawn_lifecycle_signal_handler(runtime: RedDBRuntime) {
1380 let backup_on_shutdown = std::env::var("RED_BACKUP_ON_SHUTDOWN")
1381 .ok()
1382 .map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on"))
1383 .unwrap_or(true);
1384
1385 #[cfg(unix)]
1386 {
1387 use tokio::signal::unix::{signal, SignalKind};
1388
1389 let mut sigterm = match signal(SignalKind::terminate()) {
1390 Ok(s) => s,
1391 Err(err) => {
1392 tracing::warn!(
1393 error = %err,
1394 "could not install SIGTERM handler; orchestrator graceful shutdown will fall back to SIGKILL"
1395 );
1396 return;
1397 }
1398 };
1399 let mut sigint = match signal(SignalKind::interrupt()) {
1400 Ok(s) => s,
1401 Err(err) => {
1402 tracing::warn!(error = %err, "could not install SIGINT handler");
1403 return;
1404 }
1405 };
1406 let mut sighup = match signal(SignalKind::hangup()) {
1412 Ok(s) => Some(s),
1413 Err(err) => {
1414 tracing::warn!(error = %err, "could not install SIGHUP handler; secret reload via signal disabled");
1415 None
1416 }
1417 };
1418
1419 let reload_runtime = runtime.clone();
1420 tokio::spawn(async move {
1421 loop {
1422 let signal_name = match &mut sighup {
1423 Some(hup) => tokio::select! {
1424 _ = sigterm.recv() => "SIGTERM",
1425 _ = sigint.recv() => "SIGINT",
1426 _ = hup.recv() => "SIGHUP",
1427 },
1428 None => tokio::select! {
1429 _ = sigterm.recv() => "SIGTERM",
1430 _ = sigint.recv() => "SIGINT",
1431 },
1432 };
1433
1434 if signal_name == "SIGHUP" {
1435 handle_sighup_reload(&reload_runtime);
1436 continue; }
1438
1439 tracing::info!(
1440 signal = signal_name,
1441 "lifecycle signal received; shutting down"
1442 );
1443 match runtime.graceful_shutdown(backup_on_shutdown) {
1444 Ok(report) => {
1445 tracing::info!(
1446 duration_ms = report.duration_ms,
1447 flushed_wal = report.flushed_wal,
1448 final_checkpoint = report.final_checkpoint,
1449 backup_uploaded = report.backup_uploaded,
1450 "graceful shutdown complete"
1451 );
1452 }
1453 Err(err) => {
1454 tracing::error!(error = %err, "graceful shutdown failed");
1455 crate::telemetry::operator_event::OperatorEvent::ShutdownForced {
1461 reason: format!("graceful shutdown failed: {err}"),
1462 }
1463 .emit_global();
1464 }
1465 }
1466 std::process::exit(0);
1467 }
1468 });
1469 }
1470
1471 #[cfg(not(unix))]
1472 {
1473 tokio::spawn(async move {
1474 let interrupted = tokio::signal::ctrl_c().await;
1475 if let Err(err) = interrupted {
1476 tracing::warn!(error = %err, "could not install Ctrl+C handler");
1477 return;
1478 }
1479
1480 tracing::info!(
1481 signal = "Ctrl+C",
1482 "lifecycle signal received; shutting down"
1483 );
1484 match runtime.graceful_shutdown(backup_on_shutdown) {
1485 Ok(report) => {
1486 tracing::info!(
1487 duration_ms = report.duration_ms,
1488 flushed_wal = report.flushed_wal,
1489 final_checkpoint = report.final_checkpoint,
1490 backup_uploaded = report.backup_uploaded,
1491 "graceful shutdown complete"
1492 );
1493 }
1494 Err(err) => {
1495 tracing::error!(error = %err, "graceful shutdown failed");
1496 }
1497 }
1498 std::process::exit(0);
1499 });
1500 }
1501}
1502
1503fn handle_sighup_reload(runtime: &RedDBRuntime) {
1512 let now_ms = std::time::SystemTime::now()
1513 .duration_since(std::time::UNIX_EPOCH)
1514 .map(|d| d.as_millis() as u64)
1515 .unwrap_or(0);
1516 tracing::info!(
1517 target: "reddb::secrets",
1518 ts_unix_ms = now_ms,
1519 "SIGHUP received; secrets will be re-read from *_FILE on next access"
1520 );
1521 use crate::runtime::audit_log::{AuditAuthSource, AuditEvent, AuditFieldEscaper, Outcome};
1526 runtime.audit_log().record_event(
1527 AuditEvent::builder("config/sighup_reload")
1528 .source(AuditAuthSource::System)
1529 .resource("secrets")
1530 .outcome(Outcome::Success)
1531 .field(AuditFieldEscaper::field("ts_unix_ms", now_ms))
1532 .build(),
1533 );
1534}
1535
1536fn resolve_pg_wire_tls(config: &ServerCommandConfig) -> Option<crate::wire::WireTlsConfig> {
1547 let tls_configured = config.wire_tls_bind_addr.is_some()
1548 || (config.wire_tls_cert.is_some() && config.wire_tls_key.is_some());
1549 if !tls_configured {
1550 return None;
1551 }
1552 match resolve_wire_tls_config(config) {
1553 Ok(cfg) => Some(cfg),
1554 Err(e) => {
1555 tracing::error!(
1556 err = %e,
1557 "pg wire TLS config error; listener will run plaintext"
1558 );
1559 None
1560 }
1561 }
1562}
1563
1564fn resolve_grpc_tls_options(config: &ServerCommandConfig) -> Result<crate::GrpcTlsOptions, String> {
1578 let (cert_pem, key_pem) = match (&config.grpc_tls_cert, &config.grpc_tls_key) {
1579 (Some(cert), Some(key)) => {
1580 let cert_pem = std::fs::read(cert)
1581 .map_err(|e| format!("read grpc cert {}: {e}", cert.display()))?;
1582 let key_pem =
1583 std::fs::read(key).map_err(|e| format!("read grpc key {}: {e}", key.display()))?;
1584 (cert_pem, key_pem)
1585 }
1586 _ => {
1587 let dev = std::env::var("RED_GRPC_TLS_DEV")
1589 .ok()
1590 .map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on"))
1591 .unwrap_or(false);
1592 if !dev {
1593 return Err("gRPC TLS configured but no cert/key supplied — set \
1594 REDDB_GRPC_TLS_CERT / REDDB_GRPC_TLS_KEY (or \
1595 RED_GRPC_TLS_DEV=1 to auto-generate a self-signed cert)"
1596 .to_string());
1597 }
1598 let dir = config
1599 .path
1600 .as_ref()
1601 .and_then(|p| p.parent())
1602 .map(PathBuf::from)
1603 .unwrap_or_else(|| PathBuf::from("."));
1604 let (cert_pem_str, key_pem_str) =
1605 crate::wire::tls::generate_self_signed_cert("localhost")
1606 .map_err(|e| format!("auto-generate dev grpc cert: {e}"))?;
1607
1608 let fp = sha256_pem_fingerprint(cert_pem_str.as_bytes());
1613 tracing::warn!(
1614 target: "reddb::security",
1615 transport = "grpc",
1616 cert_sha256 = %fp,
1617 "RED_GRPC_TLS_DEV=1: using auto-generated self-signed cert; \
1618 DO NOT use in production"
1619 );
1620 let cert_path = dir.join("grpc-tls-cert.pem");
1622 let key_path = dir.join("grpc-tls-key.pem");
1623 if !cert_path.exists() || !key_path.exists() {
1624 let _ = std::fs::create_dir_all(&dir);
1625 std::fs::write(&cert_path, cert_pem_str.as_bytes())
1626 .map_err(|e| format!("write grpc dev cert: {e}"))?;
1627 std::fs::write(&key_path, key_pem_str.as_bytes())
1628 .map_err(|e| format!("write grpc dev key: {e}"))?;
1629 #[cfg(unix)]
1630 {
1631 use std::os::unix::fs::PermissionsExt;
1632 let _ =
1633 std::fs::set_permissions(&key_path, std::fs::Permissions::from_mode(0o600));
1634 }
1635 }
1636 (cert_pem_str.into_bytes(), key_pem_str.into_bytes())
1637 }
1638 };
1639
1640 let client_ca_pem = match &config.grpc_tls_client_ca {
1641 Some(path) => Some(
1642 std::fs::read(path)
1643 .map_err(|e| format!("read grpc client CA {}: {e}", path.display()))?,
1644 ),
1645 None => None,
1646 };
1647
1648 Ok(crate::GrpcTlsOptions {
1649 cert_pem,
1650 key_pem,
1651 client_ca_pem,
1652 })
1653}
1654
1655fn sha256_pem_fingerprint(pem: &[u8]) -> String {
1658 use sha2::{Digest, Sha256};
1659 let mut h = Sha256::new();
1660 h.update(pem);
1661 let d = h.finalize();
1662 let mut buf = String::with_capacity(64);
1663 for b in d.iter() {
1664 buf.push_str(&format!("{b:02x}"));
1665 }
1666 buf
1667}
1668
1669fn resolve_wire_tls_config(
1671 config: &ServerCommandConfig,
1672) -> Result<crate::wire::WireTlsConfig, String> {
1673 match (&config.wire_tls_cert, &config.wire_tls_key) {
1674 (Some(cert), Some(key)) => Ok(crate::wire::WireTlsConfig {
1675 cert_path: cert.clone(),
1676 key_path: key.clone(),
1677 }),
1678 _ => {
1679 let dir = config
1681 .path
1682 .as_ref()
1683 .and_then(|p| p.parent())
1684 .map(PathBuf::from)
1685 .unwrap_or_else(|| PathBuf::from("."));
1686 crate::wire::tls::auto_generate_cert(&dir).map_err(|e| e.to_string())
1687 }
1688 }
1689}
1690
1691#[inline(never)]
1692fn build_runtime_and_auth_store(
1693 config: &ServerCommandConfig,
1694 db_options: RedDBOptions,
1695) -> Result<
1696 (
1697 RedDBRuntime,
1698 Arc<AuthStore>,
1699 Option<crate::telemetry::TelemetryGuard>,
1700 ),
1701 String,
1702> {
1703 build_runtime_with_bootstrap(db_options, config.telemetry.clone(), &config.bootstrap)
1710}
1711
1712pub(crate) fn build_runtime_with_telemetry(
1722 db_options: RedDBOptions,
1723 cli_telemetry: Option<crate::telemetry::TelemetryConfig>,
1724) -> Result<
1725 (
1726 RedDBRuntime,
1727 Arc<AuthStore>,
1728 Option<crate::telemetry::TelemetryGuard>,
1729 ),
1730 String,
1731> {
1732 let bootstrap = BootstrapConfig::default();
1733 build_runtime_with_bootstrap(db_options, cli_telemetry, &bootstrap)
1734}
1735
1736fn build_runtime_with_bootstrap(
1737 db_options: RedDBOptions,
1738 cli_telemetry: Option<crate::telemetry::TelemetryConfig>,
1739 bootstrap: &BootstrapConfig,
1740) -> Result<
1741 (
1742 RedDBRuntime,
1743 Arc<AuthStore>,
1744 Option<crate::telemetry::TelemetryGuard>,
1745 ),
1746 String,
1747> {
1748 let runtime = RedDBRuntime::with_options(db_options.clone()).map_err(|err| {
1749 let msg = err.to_string();
1755 crate::telemetry::operator_event::OperatorEvent::StartupFailed {
1756 phase: "runtime_construction".to_string(),
1757 error: msg.clone(),
1758 }
1759 .emit_global();
1760 msg
1761 })?;
1762
1763 crate::runtime::lease_loop::start_lease_loop_if_required(&runtime).map_err(|err| {
1768 let msg = err.to_string();
1769 crate::telemetry::operator_event::OperatorEvent::StartupFailed {
1770 phase: "lease_loop".to_string(),
1771 error: msg.clone(),
1772 }
1773 .emit_global();
1774 msg
1775 })?;
1776
1777 if let Some(data_path) = db_options.data_path.as_deref() {
1781 let watch_dir = data_path.parent().unwrap_or(data_path);
1782 crate::runtime::disk_space_monitor::DiskSpaceMonitor::new(watch_dir, 90).spawn();
1783 }
1784
1785 {
1789 let config_path = crate::runtime::config_overlay::config_file_path();
1790 let store = runtime.db().store();
1791 crate::runtime::config_watcher::ConfigWatcher::new(config_path, store).spawn();
1792 }
1793
1794 let merged = merge_telemetry_with_config(
1797 cli_telemetry
1798 .unwrap_or_else(|| default_telemetry_for_path(db_options.data_path.as_deref())),
1799 &runtime,
1800 );
1801 let telemetry_guard = crate::telemetry::init(merged);
1802
1803 let no_auth = no_auth_active(&db_options);
1804 let auth_store =
1805 if db_options.auth.vault_enabled {
1806 let pager =
1807 runtime.db().store().pager().cloned().ok_or_else(|| {
1808 "vault requires a paged database (persistent mode)".to_string()
1809 })?;
1810 let store = AuthStore::with_vault(db_options.auth.clone(), pager)
1811 .map_err(|err| err.to_string())?;
1812 Arc::new(store)
1813 } else {
1814 Arc::new(AuthStore::new(db_options.auth.clone()))
1815 };
1816 auth_store.configure_control_events(
1817 runtime.control_event_ledger(),
1818 runtime.control_event_config(),
1819 );
1820 let bootstrap_already_completed = runtime
1837 .db()
1838 .store()
1839 .get_config(BOOTSTRAP_COMPLETED_KEY)
1840 .is_some();
1841 let disposition = crate::cluster::authorize_cluster_bootstrap(
1842 db_options.storage_profile.deploy_profile,
1843 no_auth,
1844 bootstrap.auth_bootstrap_input(),
1845 bootstrap_already_completed,
1846 )?;
1847 let vault_plan = crate::cluster::plan_vault_bootstrap(disposition);
1856 match disposition {
1857 crate::cluster::BootstrapDisposition::SkipDevBypass => {
1858 debug_assert_eq!(vault_plan, crate::cluster::VaultBootstrapPlan::SkipNoVault);
1859 eprintln!("{NO_AUTH_WARNING}");
1860 tracing::warn!("{NO_AUTH_WARNING}");
1861 }
1862 crate::cluster::BootstrapDisposition::AlreadyComplete => {
1863 debug_assert_eq!(
1870 vault_plan,
1871 crate::cluster::VaultBootstrapPlan::OpenClusterGlobalStore {
1872 consume_secret_inputs: false,
1873 }
1874 );
1875 apply_preset_from_config(&runtime, &auth_store, bootstrap)?;
1876 tracing::info!("bootstrap already completed, unsealing existing cluster auth store");
1877 }
1878 crate::cluster::BootstrapDisposition::ProceedLocal => {
1879 debug_assert_eq!(
1882 vault_plan,
1883 crate::cluster::VaultBootstrapPlan::OpenClusterGlobalStore {
1884 consume_secret_inputs: true,
1885 }
1886 );
1887 apply_preset_from_config(&runtime, &auth_store, bootstrap)?;
1888 maybe_apply_policy_break_glass(&auth_store);
1889 }
1890 }
1891
1892 {
1894 let store = Arc::clone(&auth_store);
1895 std::thread::Builder::new()
1896 .name("reddb-session-purge".into())
1897 .spawn(move || loop {
1898 std::thread::sleep(std::time::Duration::from_secs(300));
1899 store.purge_expired_sessions();
1900 })
1901 .ok();
1902 }
1903
1904 Ok((runtime, auth_store, telemetry_guard))
1905}
1906
1907fn maybe_apply_policy_break_glass(auth_store: &Arc<AuthStore>) {
1911 use crate::auth::self_lock_guard::BREAK_GLASS_ENV;
1912
1913 let enabled = std::env::var(BREAK_GLASS_ENV)
1914 .ok()
1915 .map(|v| {
1916 let trimmed = v.trim().to_ascii_lowercase();
1917 matches!(trimmed.as_str(), "1" | "true" | "yes")
1918 })
1919 .unwrap_or(false);
1920 if !enabled {
1921 return;
1922 }
1923 let now = crate::utils::now_unix_millis() as u128;
1924 match auth_store.apply_policy_break_glass(now) {
1925 Ok(()) => {
1926 tracing::warn!(env = BREAK_GLASS_ENV, "policy break-glass recovery applied");
1927 }
1928 Err(err) => {
1929 tracing::error!(env = BREAK_GLASS_ENV, %err, "policy break-glass recovery failed");
1930 }
1931 }
1932}
1933
1934pub(crate) const BOOTSTRAP_COMPLETED_KEY: &str = "system.bootstrap.completed";
1939pub(crate) const BOOTSTRAP_PRESET_KEY: &str = "system.bootstrap.preset";
1940pub(crate) const BOOTSTRAP_FIRST_ADMIN_KEY: &str = "system.bootstrap.first_admin_id";
1941
1942pub(crate) const BOOTSTRAP_PRESET_ENV: &str = "REDDB_BOOTSTRAP_PRESET";
1945pub(crate) const PRESET_ENV: &str = "REDDB_PRESET";
1946pub(crate) const BOOTSTRAP_CERT_OUT_ENV: &str = "REDDB_BOOTSTRAP_CERT_OUT";
1949pub(crate) const PRESET_SIMPLE: &str = "simple";
1950pub(crate) const PRESET_PRODUCTION: &str = "production";
1951pub(crate) const PRESET_REGULATED: &str = "regulated";
1952pub(crate) const PRESET_CLOUD: &str = "cloud";
1953
1954pub(crate) const FIRST_ADMIN_ALLOW_ALL_POLICY: &str = "system.bootstrap.first-admin-allow-all";
1958pub(crate) const REGULATED_PROTECT_MANAGED_POLICY: &str = "system.regulated.protect-managed";
1959pub(crate) const CLOUD_PROTECT_MANAGED_POLICY: &str = "system.cloud.protect-managed";
1960pub(crate) const CLOUD_CONFIG_NAMESPACE: &str = "red.config.cloud";
1961pub(crate) const REGULATED_AUDIT_CONFIG_NAMESPACE: &str = "red.config.audit";
1962pub(crate) const REGULATED_EVIDENCE_CONFIG_NAMESPACE: &str = "red.config.evidence";
1963pub(crate) const REGULATED_QUERY_AUDIT_CONFIG_NAMESPACE: &str = "red.config.query_audit";
1964
1965pub(crate) fn apply_preset(
1975 runtime: &RedDBRuntime,
1976 auth_store: &Arc<AuthStore>,
1977) -> Result<(), String> {
1978 let bootstrap = BootstrapConfig::default();
1979 apply_preset_from_config(runtime, auth_store, &bootstrap)
1980}
1981
1982fn apply_preset_from_config(
1983 runtime: &RedDBRuntime,
1984 auth_store: &Arc<AuthStore>,
1985 bootstrap: &BootstrapConfig,
1986) -> Result<(), String> {
1987 let store = runtime.db().store();
1988
1989 if store.get_config(BOOTSTRAP_COMPLETED_KEY).is_some() {
1990 crate::cli::bootstrap_manifest::rehydrate_manifest_registry(
1991 runtime,
1992 &runtime.config_registry(),
1993 )?;
1994 tracing::info!("bootstrap state present, skipping preset application");
1995 return Ok(());
1996 }
1997
1998 let preset = bootstrap.resolved_preset();
1999
2000 if let Some(path) = bootstrap.selected_manifest() {
2001 if let Some(manifest_bootstrap) =
2002 crate::cli::bootstrap_manifest::bootstrap_config_from_manifest_file(&path)?
2003 {
2004 apply_preset_from_config(runtime, auth_store, &manifest_bootstrap)?;
2005 return Ok(());
2006 }
2007 let first_admin_id = crate::cli::bootstrap_manifest::apply_manifest_file(
2008 runtime,
2009 auth_store,
2010 &runtime.config_registry(),
2011 &path,
2012 )?;
2013 persist_bootstrap_state(runtime, "manifest", Some(&first_admin_id));
2014 tracing::info!("bootstrap manifest applied");
2015 return Ok(());
2016 }
2017
2018 let (first_admin_id, certificate) = match preset.as_str() {
2019 PRESET_SIMPLE => {
2020 (None, None)
2024 }
2025 PRESET_PRODUCTION => {
2026 let (id, cert) = apply_production_preset_from_config(auth_store, bootstrap)?;
2027 (Some(id), cert)
2028 }
2029 PRESET_CLOUD => {
2030 let (id, cert) = apply_cloud_preset(runtime, auth_store, bootstrap)?;
2031 (Some(id), cert)
2032 }
2033 PRESET_REGULATED => {
2034 apply_regulated_preset(runtime, auth_store)?;
2035 (None, None)
2036 }
2037 other => {
2038 return Err(format!(
2039 "bootstrap preset {other:?} is not recognised (expected `simple`, `production`, `regulated`, or `cloud`)"
2040 ));
2041 }
2042 };
2043
2044 if let Some(cert) = certificate.as_deref() {
2051 write_bootstrap_certificate(bootstrap, cert)?;
2052 }
2053
2054 persist_bootstrap_state(runtime, &preset, first_admin_id.as_deref());
2055 tracing::info!(preset = %preset, "bootstrap preset applied");
2056 Ok(())
2057}
2058
2059fn write_bootstrap_certificate(
2063 bootstrap: &BootstrapConfig,
2064 certificate_hex: &str,
2065) -> Result<(), String> {
2066 let Some(path) = bootstrap.selected_cert_out() else {
2067 return Ok(());
2068 };
2069 std::fs::write(&path, certificate_hex)
2070 .map_err(|err| format!("write --bootstrap-cert-out {}: {err}", path.display()))?;
2071 tracing::info!(
2072 target: "reddb::bootstrap",
2073 path = %path.display(),
2074 "first-boot self-bootstrap: wrote unseal certificate to --bootstrap-cert-out (consume via REDDB_CERTIFICATE_FILE on the next boot)"
2075 );
2076 Ok(())
2077}
2078
2079fn apply_production_preset_from_config(
2080 auth_store: &Arc<AuthStore>,
2081 bootstrap: &BootstrapConfig,
2082) -> Result<(String, Option<String>), String> {
2083 use crate::auth::store::PrincipalRef;
2084 use crate::auth::UserId;
2085
2086 let username = bootstrap.production_username().ok_or_else(|| {
2087 "production preset requires --bootstrap-admin or REDDB_USERNAME (or REDDB_USERNAME_FILE)"
2088 .to_string()
2089 })?;
2090 let password = bootstrap.production_password().ok_or_else(|| {
2091 "production preset requires --bootstrap-admin-password or REDDB_PASSWORD (or REDDB_PASSWORD_FILE)"
2092 .to_string()
2093 })?;
2094
2095 let result = auth_store
2099 .bootstrap(&username, &password)
2100 .map_err(|err| format!("bootstrap first admin: {err}"))?;
2101 let certificate = result.certificate.clone();
2102 if let Some(cert) = certificate.as_deref() {
2103 eprintln!("[reddb] CERTIFICATE: {}", cert);
2104 tracing::warn!(
2105 "vault certificate issued by REDDB_PRESET=production -- save it and update the runtime secret before restart"
2106 );
2107 }
2108 let first_admin = UserId::platform(result.user.username.clone());
2109
2110 install_allow_all_policy(auth_store)?;
2112
2113 auth_store
2115 .attach_policy(
2116 PrincipalRef::User(first_admin.clone()),
2117 FIRST_ADMIN_ALLOW_ALL_POLICY,
2118 )
2119 .map_err(|err| format!("attach allow-all policy: {err}"))?;
2120
2121 Ok((first_admin.to_string(), certificate))
2122}
2123
2124fn apply_cloud_preset(
2125 runtime: &RedDBRuntime,
2126 auth_store: &Arc<AuthStore>,
2127 bootstrap: &BootstrapConfig,
2128) -> Result<(String, Option<String>), String> {
2129 use crate::auth::store::PrincipalRef;
2130 use crate::auth::{Role, UserId};
2131
2132 let head_username = bootstrap.cloud_head_username().ok_or_else(|| {
2133 "cloud preset requires --cloud-head-admin or REDDB_CLOUD_HEAD_ADMIN (or REDDB_USERNAME)"
2134 .to_string()
2135 })?;
2136 let head_password = bootstrap.cloud_head_password().ok_or_else(|| {
2137 "cloud preset requires --cloud-head-admin-password or REDDB_CLOUD_HEAD_ADMIN_PASSWORD (or REDDB_PASSWORD)"
2138 .to_string()
2139 })?;
2140 let customer_username = bootstrap.customer_username().ok_or_else(|| {
2141 "cloud preset requires --customer-admin or REDDB_CUSTOMER_ADMIN".to_string()
2142 })?;
2143 let customer_password = bootstrap.customer_password().ok_or_else(|| {
2144 "cloud preset requires --customer-admin-password or REDDB_CUSTOMER_ADMIN_PASSWORD"
2145 .to_string()
2146 })?;
2147 if head_username == customer_username {
2148 return Err("cloud preset requires distinct head and customer admin usernames".to_string());
2149 }
2150
2151 let head = auth_store
2152 .bootstrap_system_admin(&head_username, &head_password)
2153 .map_err(|err| format!("bootstrap cloud head admin: {err}"))?;
2154 let certificate = head.certificate.clone();
2155 let head_id = UserId::platform(head.user.username.clone());
2156
2157 auth_store
2158 .create_user(&customer_username, &customer_password, Role::Admin)
2159 .map_err(|err| format!("create cloud customer admin: {err}"))?;
2160 let customer_id = UserId::platform(customer_username.clone());
2161
2162 install_allow_all_policy(auth_store)?;
2163 for user_id in [&head_id, &customer_id] {
2164 auth_store
2165 .attach_policy(
2166 PrincipalRef::User(user_id.clone()),
2167 FIRST_ADMIN_ALLOW_ALL_POLICY,
2168 )
2169 .map_err(|err| format!("attach allow-all policy: {err}"))?;
2170 }
2171 install_cloud_guardrails(runtime, auth_store, &head_id)?;
2172 auth_store
2173 .attach_policy(
2174 PrincipalRef::User(customer_id),
2175 CLOUD_PROTECT_MANAGED_POLICY,
2176 )
2177 .map_err(|err| format!("attach cloud guardrail policy: {err}"))?;
2178
2179 Ok((head_id.to_string(), certificate))
2180}
2181
2182pub(crate) fn install_allow_all_policy(auth_store: &AuthStore) -> Result<(), String> {
2183 use crate::auth::policies::Policy;
2184
2185 let policy = Policy::from_json_str(&format!(
2186 r#"{{
2187 "id": "{id}",
2188 "version": 1,
2189 "statements": [{{
2190 "effect": "allow",
2191 "actions": ["*"],
2192 "resources": ["*"]
2193 }}]
2194 }}"#,
2195 id = FIRST_ADMIN_ALLOW_ALL_POLICY
2196 ))
2197 .map_err(|err| format!("compile allow-all policy: {err}"))?;
2198 auth_store
2199 .put_policy(policy)
2200 .map_err(|err| format!("install allow-all policy: {err}"))
2201}
2202
2203fn install_cloud_guardrails(
2204 runtime: &RedDBRuntime,
2205 auth_store: &Arc<AuthStore>,
2206 head_admin: &crate::auth::UserId,
2207) -> Result<(), String> {
2208 use crate::auth::policies::Policy;
2209 use crate::auth::registry::EvidenceRequirement;
2210
2211 let policy = Policy::from_json_str(&format!(
2212 r#"{{
2213 "id": "{id}",
2214 "version": 1,
2215 "statements": [
2216 {{
2217 "effect": "deny",
2218 "actions": ["policy:put", "policy:drop", "policy:attach", "policy:detach"],
2219 "resources": ["policy:{id}"]
2220 }},
2221 {{
2222 "effect": "deny",
2223 "actions": [
2224 "user:delete",
2225 "user:disable",
2226 "user:password:change",
2227 "user:role:update"
2228 ],
2229 "resources": ["user:{head_admin}"]
2230 }},
2231 {{
2232 "effect": "deny",
2233 "actions": ["config:write"],
2234 "resources": ["config:{cloud}.*"]
2235 }}
2236 ]
2237 }}"#,
2238 id = CLOUD_PROTECT_MANAGED_POLICY,
2239 head_admin = head_admin,
2240 cloud = CLOUD_CONFIG_NAMESPACE,
2241 ))
2242 .map_err(|err| format!("compile cloud guardrail policy: {err}"))?;
2243 auth_store
2244 .put_policy(policy)
2245 .map_err(|err| format!("install cloud guardrail policy: {err}"))?;
2246
2247 let now_ms = crate::utils::now_unix_millis() as u128;
2248 let entries = vec![
2249 regulated_registry_entry(
2250 CLOUD_PROTECT_MANAGED_POLICY,
2251 crate::auth::managed_policy::RESOURCE_TYPE_POLICY,
2252 "policy",
2253 "policy:*",
2254 &format!("policy:{CLOUD_PROTECT_MANAGED_POLICY}"),
2255 EvidenceRequirement::Metadata,
2256 now_ms,
2257 ),
2258 regulated_registry_entry(
2259 CLOUD_CONFIG_NAMESPACE,
2260 crate::auth::managed_config::RESOURCE_TYPE_CONFIG_NAMESPACE,
2261 "config_namespace",
2262 "config:write",
2263 &format!("config:{CLOUD_CONFIG_NAMESPACE}.*"),
2264 EvidenceRequirement::Metadata,
2265 now_ms,
2266 ),
2267 ];
2268 for entry in entries.iter().cloned() {
2269 runtime
2270 .config_registry()
2271 .restore_bootstrap_entry(entry)
2272 .map_err(|err| format!("install cloud registry entry: {err}"))?;
2273 }
2274 crate::cli::bootstrap_manifest::persist_registry_state(runtime, &entries)?;
2275 Ok(())
2276}
2277
2278fn apply_regulated_preset(
2279 runtime: &RedDBRuntime,
2280 auth_store: &Arc<AuthStore>,
2281) -> Result<(), String> {
2282 use crate::auth::policies::Policy;
2283 use crate::auth::registry::EvidenceRequirement;
2284 use crate::auth::store::{PrincipalRef, PUBLIC_IAM_GROUP};
2285
2286 runtime.query_audit().enable_infrastructure();
2287
2288 let policy = Policy::from_json_str(&format!(
2289 r#"{{
2290 "id": "{id}",
2291 "version": 1,
2292 "statements": [
2293 {{
2294 "effect": "deny",
2295 "actions": ["policy:put", "policy:drop", "policy:attach", "policy:detach"],
2296 "resources": ["policy:{id}"]
2297 }},
2298 {{
2299 "effect": "deny",
2300 "actions": ["config:write"],
2301 "resources": [
2302 "config:{audit}.*",
2303 "config:{evidence}.*",
2304 "config:{query_audit}.*"
2305 ]
2306 }}
2307 ]
2308 }}"#,
2309 id = REGULATED_PROTECT_MANAGED_POLICY,
2310 audit = REGULATED_AUDIT_CONFIG_NAMESPACE,
2311 evidence = REGULATED_EVIDENCE_CONFIG_NAMESPACE,
2312 query_audit = REGULATED_QUERY_AUDIT_CONFIG_NAMESPACE,
2313 ))
2314 .map_err(|err| format!("compile regulated guardrail policy: {err}"))?;
2315 auth_store
2316 .put_policy(policy)
2317 .map_err(|err| format!("install regulated guardrail policy: {err}"))?;
2318 auth_store
2319 .attach_policy(
2320 PrincipalRef::Group(PUBLIC_IAM_GROUP.to_string()),
2321 REGULATED_PROTECT_MANAGED_POLICY,
2322 )
2323 .map_err(|err| format!("attach regulated guardrail policy: {err}"))?;
2324
2325 let now_ms = crate::utils::now_unix_millis() as u128;
2326 let entries = vec![
2327 regulated_registry_entry(
2328 REGULATED_PROTECT_MANAGED_POLICY,
2329 crate::auth::managed_policy::RESOURCE_TYPE_POLICY,
2330 "iam_policy",
2331 "policy:*",
2332 &format!("policy:{REGULATED_PROTECT_MANAGED_POLICY}"),
2333 EvidenceRequirement::Metadata,
2334 now_ms,
2335 ),
2336 regulated_registry_entry(
2337 REGULATED_AUDIT_CONFIG_NAMESPACE,
2338 crate::auth::managed_config::RESOURCE_TYPE_CONFIG_NAMESPACE,
2339 "config_namespace",
2340 "config:write",
2341 &format!("config:{REGULATED_AUDIT_CONFIG_NAMESPACE}.*"),
2342 EvidenceRequirement::Metadata,
2343 now_ms,
2344 ),
2345 regulated_registry_entry(
2346 REGULATED_EVIDENCE_CONFIG_NAMESPACE,
2347 crate::auth::managed_config::RESOURCE_TYPE_CONFIG_NAMESPACE,
2348 "config_namespace",
2349 "config:write",
2350 &format!("config:{REGULATED_EVIDENCE_CONFIG_NAMESPACE}.*"),
2351 EvidenceRequirement::Metadata,
2352 now_ms,
2353 ),
2354 regulated_registry_entry(
2355 REGULATED_QUERY_AUDIT_CONFIG_NAMESPACE,
2356 crate::auth::managed_config::RESOURCE_TYPE_CONFIG_NAMESPACE,
2357 "config_namespace",
2358 "config:write",
2359 &format!("config:{REGULATED_QUERY_AUDIT_CONFIG_NAMESPACE}.*"),
2360 EvidenceRequirement::Metadata,
2361 now_ms,
2362 ),
2363 ];
2364
2365 for entry in entries.iter().cloned() {
2366 runtime
2367 .config_registry()
2368 .restore_bootstrap_entry(entry)
2369 .map_err(|err| format!("install regulated registry entry: {err}"))?;
2370 }
2371 crate::cli::bootstrap_manifest::persist_registry_state(runtime, &entries)?;
2372 Ok(())
2373}
2374
2375fn regulated_registry_entry(
2376 id: &str,
2377 resource_type: &str,
2378 schema: &str,
2379 required_action: &str,
2380 required_resource: &str,
2381 evidence_requirement: crate::auth::registry::EvidenceRequirement,
2382 updated_at_ms: u128,
2383) -> crate::auth::registry::ConfigRegistryEntry {
2384 crate::auth::registry::ConfigRegistryEntry {
2385 id: id.to_string(),
2386 version: 1,
2387 resource_type: resource_type.to_string(),
2388 schema: schema.to_string(),
2389 mutability: crate::auth::registry::Mutability::Immutable,
2390 sensitivity: crate::auth::registry::Sensitivity::Internal,
2391 managed: true,
2392 required_action: required_action.to_string(),
2393 required_resource: required_resource.to_string(),
2394 evidence_requirement,
2395 updated_by: "system:regulated-preset".to_string(),
2396 updated_at_ms,
2397 }
2398}
2399
2400fn persist_bootstrap_state(runtime: &RedDBRuntime, preset: &str, first_admin_id: Option<&str>) {
2401 let store = runtime.db().store();
2402 let mut tree = crate::serde_json::Map::new();
2403 tree.insert(
2404 BOOTSTRAP_COMPLETED_KEY.to_string(),
2405 crate::serde_json::Value::Bool(true),
2406 );
2407 tree.insert(
2408 BOOTSTRAP_PRESET_KEY.to_string(),
2409 crate::serde_json::Value::String(preset.to_string()),
2410 );
2411 if let Some(id) = first_admin_id {
2412 tree.insert(
2413 BOOTSTRAP_FIRST_ADMIN_KEY.to_string(),
2414 crate::serde_json::Value::String(id.to_string()),
2415 );
2416 }
2417 let json = crate::serde_json::Value::Object(tree);
2418 store.set_config_tree("", &json);
2419}
2420
2421fn merge_telemetry_with_config(
2432 mut cli: crate::telemetry::TelemetryConfig,
2433 runtime: &RedDBRuntime,
2434) -> crate::telemetry::TelemetryConfig {
2435 use reddb_types::Value;
2436
2437 let store = runtime.db().store();
2438
2439 if !cli.level_explicit {
2440 if let Some(Value::Text(v)) = store.get_config("red.logging.level") {
2441 cli.level_filter = v.to_string();
2442 }
2443 }
2444 if !cli.format_explicit {
2445 if let Some(Value::Text(v)) = store.get_config("red.logging.format") {
2446 if let Some(parsed) = crate::telemetry::LogFormat::parse(&v) {
2447 cli.format = parsed;
2448 }
2449 }
2450 }
2451 if !cli.rotation_keep_days_explicit {
2452 match store.get_config("red.logging.keep_days") {
2453 Some(Value::Integer(n)) if n >= 0 && n <= u16::MAX as i64 => {
2454 cli.rotation_keep_days = n as u16
2455 }
2456 Some(Value::UnsignedInteger(n)) if n <= u16::MAX as u64 => {
2457 cli.rotation_keep_days = n as u16
2458 }
2459 Some(Value::Text(v)) => {
2460 if let Ok(n) = v.parse::<u16>() {
2461 cli.rotation_keep_days = n;
2462 }
2463 }
2464 _ => {}
2465 }
2466 }
2467 if !cli.file_prefix_explicit {
2468 if let Some(Value::Text(v)) = store.get_config("red.logging.file_prefix") {
2469 if !v.is_empty() {
2470 cli.file_prefix = v.to_string();
2471 }
2472 }
2473 }
2474 if !cli.log_dir_explicit && !cli.log_file_disabled {
2477 if let Some(Value::Text(v)) = store.get_config("red.logging.dir") {
2478 if !v.is_empty() {
2479 cli.log_dir = Some(std::path::PathBuf::from(v.as_ref()));
2480 }
2481 }
2482 }
2483
2484 cli
2485}
2486
2487#[cfg(test)]
2488mod telemetry_merge_tests {
2489 use super::*;
2490 use crate::telemetry::{LogFormat, TelemetryConfig};
2491
2492 fn fresh_runtime() -> RedDBRuntime {
2493 RedDBRuntime::with_options(RedDBOptions::in_memory()).expect("runtime")
2494 }
2495
2496 fn set_str(runtime: &RedDBRuntime, key: &str, value: &str) {
2497 runtime
2498 .db()
2499 .store()
2500 .set_config_tree(key, &crate::serde_json::Value::String(value.to_string()));
2501 }
2502
2503 fn cli_base() -> TelemetryConfig {
2504 TelemetryConfig {
2507 log_dir: Some(std::path::PathBuf::from("/tmp/reddb-default/logs")),
2508 format: LogFormat::Json,
2509 ..Default::default()
2510 }
2511 }
2512
2513 #[test]
2514 fn config_log_dir_promoted_when_flag_absent() {
2515 let runtime = fresh_runtime();
2516 set_str(&runtime, "red.logging.dir", "/var/log/reddb");
2517 let merged = merge_telemetry_with_config(cli_base(), &runtime);
2518 assert_eq!(
2519 merged.log_dir.as_deref(),
2520 Some(std::path::Path::new("/var/log/reddb"))
2521 );
2522 }
2523
2524 #[test]
2525 fn explicit_log_dir_wins_over_config() {
2526 let runtime = fresh_runtime();
2527 set_str(&runtime, "red.logging.dir", "/var/log/reddb");
2528 let mut cli = cli_base();
2529 cli.log_dir = Some(std::path::PathBuf::from("/custom/dir"));
2530 cli.log_dir_explicit = true;
2531 let merged = merge_telemetry_with_config(cli, &runtime);
2532 assert_eq!(
2533 merged.log_dir.as_deref(),
2534 Some(std::path::Path::new("/custom/dir"))
2535 );
2536 }
2537
2538 #[test]
2539 fn no_log_file_beats_config_log_dir() {
2540 let runtime = fresh_runtime();
2541 set_str(&runtime, "red.logging.dir", "/var/log/reddb");
2542 let mut cli = cli_base();
2543 cli.log_dir = None;
2544 cli.log_file_disabled = true;
2545 let merged = merge_telemetry_with_config(cli, &runtime);
2546 assert!(
2547 merged.log_dir.is_none(),
2548 "--no-log-file must veto config dir"
2549 );
2550 }
2551
2552 #[test]
2553 fn config_format_promoted_on_non_tty_default() {
2554 let runtime = fresh_runtime();
2558 set_str(&runtime, "red.logging.format", "pretty");
2559 let merged = merge_telemetry_with_config(cli_base(), &runtime);
2560 assert_eq!(merged.format, LogFormat::Pretty);
2561 }
2562
2563 #[test]
2564 fn explicit_format_wins_over_config() {
2565 let runtime = fresh_runtime();
2566 set_str(&runtime, "red.logging.format", "pretty");
2567 let mut cli = cli_base();
2568 cli.format = LogFormat::Json;
2569 cli.format_explicit = true;
2570 let merged = merge_telemetry_with_config(cli, &runtime);
2571 assert_eq!(merged.format, LogFormat::Json);
2572 }
2573}
2574
2575#[inline(never)]
2576fn build_http_server(
2577 runtime: RedDBRuntime,
2578 auth_store: Arc<AuthStore>,
2579 bind_addr: String,
2580) -> RedDBServer {
2581 build_http_server_with_transport_readiness(
2582 runtime,
2583 auth_store,
2584 bind_addr,
2585 TransportReadiness::default(),
2586 )
2587}
2588
2589fn apply_http_limits(
2595 server: RedDBServer,
2596 config: &ServerCommandConfig,
2597 runtime: &RedDBRuntime,
2598) -> RedDBServer {
2599 let store = runtime.db().store();
2600 let resolved =
2601 crate::server::http_limits::resolve_http_limits(&config.http_limits_cli, |key| match store
2602 .get_config(key)
2603 {
2604 Some(reddb_types::Value::Text(v)) => Some(v.to_string()),
2605 Some(reddb_types::Value::Integer(n)) if n >= 0 => Some(n.to_string()),
2606 Some(reddb_types::Value::UnsignedInteger(n)) => Some(n.to_string()),
2607 _ => None,
2608 });
2609 tracing::info!(
2610 target: "reddb::http_limits",
2611 max_handlers = resolved.max_handlers,
2612 handler_timeout_ms = resolved.handler_timeout_ms,
2613 retry_after_secs = resolved.retry_after_secs,
2614 max_inflight_per_principal = resolved.max_inflight_per_principal,
2615 "http_limits resolved"
2616 );
2617 server.with_http_limits(resolved)
2618}
2619
2620fn apply_ui_bundle(
2630 server: RedDBServer,
2631 config: &ServerCommandConfig,
2632) -> Result<RedDBServer, String> {
2633 if !config.ui {
2634 return Ok(server);
2635 }
2636 let ui_dir = match &config.ui_dir {
2637 Some(dir) => dir.clone(),
2638 None => {
2639 let cache_root = crate::server::ui_bundle_resolver::reddb_user_cache_root()
2640 .unwrap_or_else(|_| std::env::temp_dir().join("reddb"));
2641 crate::server::ui_bundle_resolver::resolve_ui_bundle(
2642 &cache_root,
2643 &crate::server::ui_bundle_resolver::HttpFetcher,
2644 )
2645 .map_err(|err| format!("resolve red-ui bundle for --ui: {err}"))?
2646 }
2647 };
2648 tracing::info!(target: "reddb::ui", dir = %ui_dir.display(), "serving red-ui bundle on HTTP surface");
2649 Ok(server.with_ui_dir(ui_dir))
2650}
2651
2652#[inline(never)]
2653fn build_http_server_with_transport_readiness(
2654 runtime: RedDBRuntime,
2655 auth_store: Arc<AuthStore>,
2656 bind_addr: String,
2657 transport_readiness: TransportReadiness,
2658) -> RedDBServer {
2659 RedDBServer::with_options(
2660 runtime,
2661 ServerOptions {
2662 bind_addr,
2663 transport_readiness,
2664 ..ServerOptions::default()
2665 },
2666 )
2667 .with_auth(auth_store)
2668}
2669
2670#[inline(never)]
2674fn build_admin_only_server(
2675 runtime: RedDBRuntime,
2676 auth_store: Arc<AuthStore>,
2677 bind_addr: String,
2678) -> RedDBServer {
2679 RedDBServer::with_options(
2680 runtime,
2681 ServerOptions {
2682 bind_addr,
2683 surface: crate::server::ServerSurface::AdminOnly,
2684 ..ServerOptions::default()
2685 },
2686 )
2687 .with_auth(auth_store)
2688}
2689
2690#[inline(never)]
2694fn build_metrics_only_server(
2695 runtime: RedDBRuntime,
2696 auth_store: Arc<AuthStore>,
2697 bind_addr: String,
2698) -> RedDBServer {
2699 RedDBServer::with_options(
2700 runtime,
2701 ServerOptions {
2702 bind_addr,
2703 surface: crate::server::ServerSurface::MetricsOnly,
2704 ..ServerOptions::default()
2705 },
2706 )
2707 .with_auth(auth_store)
2708}
2709
2710fn spawn_admin_metrics_listeners(runtime: &RedDBRuntime, auth_store: &Arc<AuthStore>) {
2714 if let Some(addr) = env_nonempty("RED_ADMIN_BIND") {
2715 let server = build_admin_only_server(runtime.clone(), auth_store.clone(), addr.clone());
2716 let _ = server.serve_in_background();
2717 tracing::info!(transport = "http", surface = "admin", bind = %addr, "listener online");
2718 }
2719 if let Some(addr) = env_nonempty("RED_METRICS_BIND") {
2720 let server = build_metrics_only_server(runtime.clone(), auth_store.clone(), addr.clone());
2721 let _ = server.serve_in_background();
2722 tracing::info!(transport = "http", surface = "metrics", bind = %addr, "listener online");
2723 }
2724}
2725
2726fn resolve_http_tls_config(
2728 config: &ServerCommandConfig,
2729) -> Result<crate::server::tls::HttpTlsConfig, String> {
2730 match (&config.http_tls_cert, &config.http_tls_key) {
2731 (Some(cert), Some(key)) => Ok(crate::server::tls::HttpTlsConfig {
2732 cert_path: cert.clone(),
2733 key_path: key.clone(),
2734 client_ca_path: config.http_tls_client_ca.clone(),
2735 }),
2736 (None, None) => {
2737 let dir = config
2739 .path
2740 .as_ref()
2741 .and_then(|p| p.parent().map(std::path::PathBuf::from))
2742 .unwrap_or_else(|| std::path::PathBuf::from("."));
2743 let auto = crate::server::tls::auto_generate_dev_cert(&dir)
2744 .map_err(|err| format!("HTTP TLS dev: {err}"))?;
2745 Ok(crate::server::tls::HttpTlsConfig {
2746 cert_path: auto.cert_path,
2747 key_path: auto.key_path,
2748 client_ca_path: config.http_tls_client_ca.clone(),
2749 })
2750 }
2751 _ => Err("HTTP TLS requires both --http-tls-cert and --http-tls-key (or neither, with RED_HTTP_TLS_DEV=1)".to_string()),
2752 }
2753}
2754
2755#[cfg(test)]
2756mod tests {
2757 use super::*;
2758
2759 #[test]
2760 fn render_systemd_unit_contains_expected_execstart() {
2761 let config = SystemdServiceConfig {
2762 service_name: "reddb".to_string(),
2763 binary_path: PathBuf::from("/usr/local/bin/red"),
2764 run_user: "reddb".to_string(),
2765 run_group: "reddb".to_string(),
2766 data_path: reddb_file::default_service_database_path(),
2767 router_bind_addr: None,
2768 grpc_bind_addr: Some("0.0.0.0:55055".to_string()),
2769 http_bind_addr: None,
2770 };
2771
2772 let unit = render_systemd_unit(&config);
2773 assert!(unit.contains("ExecStart=/usr/local/bin/red server --path /var/lib/reddb/data.rdb --grpc-bind 0.0.0.0:55055"));
2774 assert!(unit.contains("ReadWritePaths=/var/lib/reddb"));
2775 }
2776
2777 #[test]
2778 fn systemd_service_config_derives_paths() {
2779 let config = SystemdServiceConfig {
2780 service_name: "reddb-api".to_string(),
2781 binary_path: PathBuf::from("/usr/local/bin/red"),
2782 run_user: "reddb".to_string(),
2783 run_group: "reddb".to_string(),
2784 data_path: PathBuf::from("/srv/reddb/live/data.rdb"),
2785 router_bind_addr: None,
2786 grpc_bind_addr: None,
2787 http_bind_addr: Some("127.0.0.1:5000".to_string()),
2788 };
2789
2790 assert_eq!(config.data_dir(), PathBuf::from("/srv/reddb/live"));
2791 assert_eq!(
2792 config.unit_path(),
2793 PathBuf::from("/etc/systemd/system/reddb-api.service")
2794 );
2795 }
2796
2797 #[test]
2798 fn render_systemd_unit_supports_dual_transport() {
2799 let config = SystemdServiceConfig {
2800 service_name: "reddb".to_string(),
2801 binary_path: PathBuf::from("/usr/local/bin/red"),
2802 run_user: "reddb".to_string(),
2803 run_group: "reddb".to_string(),
2804 data_path: reddb_file::default_service_database_path(),
2805 router_bind_addr: None,
2806 grpc_bind_addr: Some("0.0.0.0:55055".to_string()),
2807 http_bind_addr: Some("0.0.0.0:5000".to_string()),
2808 };
2809
2810 let unit = render_systemd_unit(&config);
2811 assert!(unit.contains("--grpc-bind 0.0.0.0:55055"));
2812 assert!(unit.contains("--http-bind 0.0.0.0:5000"));
2813 }
2814
2815 #[test]
2816 fn render_systemd_unit_supports_router_mode() {
2817 let config = SystemdServiceConfig {
2818 service_name: "reddb".to_string(),
2819 binary_path: PathBuf::from("/usr/local/bin/red"),
2820 run_user: "reddb".to_string(),
2821 run_group: "reddb".to_string(),
2822 data_path: reddb_file::default_service_database_path(),
2823 router_bind_addr: Some(DEFAULT_ROUTER_BIND_ADDR.to_string()),
2824 grpc_bind_addr: None,
2825 http_bind_addr: None,
2826 };
2827
2828 let unit = render_systemd_unit(&config);
2829 assert!(unit.contains("--bind 127.0.0.1:5050"));
2830 assert!(!unit.contains("--grpc-bind"));
2831 assert!(!unit.contains("--http-bind"));
2832 }
2833
2834 #[test]
2835 fn explicit_bind_collision_is_fatal() {
2836 let held = TcpListener::bind("127.0.0.1:0").expect("hold test port");
2837 let addr = held.local_addr().expect("held addr").to_string();
2838 let mut readiness = TransportReadiness::default();
2839
2840 let error = bind_listener_for_startup(&mut readiness, "http", &addr, true).unwrap_err();
2841
2842 assert!(error.contains("explicit http listener bind"));
2843 assert_eq!(readiness.active.len(), 0);
2844 assert_eq!(readiness.failed.len(), 1);
2845 assert!(readiness.failed[0].explicit);
2846 assert_eq!(readiness.failed[0].bind_addr, addr);
2847 }
2848
2849 fn no_auth_env_lock() -> &'static std::sync::Mutex<()> {
2856 static LOCK: std::sync::OnceLock<std::sync::Mutex<()>> = std::sync::OnceLock::new();
2857 LOCK.get_or_init(|| std::sync::Mutex::new(()))
2858 }
2859
2860 fn no_auth_test_config(no_auth: bool) -> ServerCommandConfig {
2861 ServerCommandConfig {
2862 path: None,
2863 router_bind_addr: Some(DEFAULT_ROUTER_BIND_ADDR.to_string()),
2864 router_bind_explicit: false,
2865 grpc_bind_addr: None,
2866 grpc_bind_explicit: false,
2867 grpc_tls_bind_addr: None,
2868 grpc_tls_cert: None,
2869 grpc_tls_key: None,
2870 grpc_tls_client_ca: None,
2871 http_bind_addr: None,
2872 http_bind_explicit: false,
2873 http_tls_bind_addr: None,
2874 http_tls_cert: None,
2875 http_tls_key: None,
2876 http_tls_client_ca: None,
2877 wire_bind_addr: None,
2878 wire_bind_explicit: false,
2879 wire_tls_bind_addr: None,
2880 wire_tls_cert: None,
2881 wire_tls_key: None,
2882 pg_bind_addr: None,
2883 create_if_missing: true,
2884 read_only: false,
2885 role: "standalone".to_string(),
2886 primary_addr: None,
2887 storage_profile: StorageProfileSelection::embedded_single_file(),
2888 auth: false,
2889 require_auth: false,
2890 vault: true,
2893 no_auth,
2894 workers: None,
2895 telemetry: None,
2896 http_limits_cli: crate::server::HttpLimitsCliInput::default(),
2897 ui: false,
2898 ui_dir: None,
2899 bootstrap: BootstrapConfig::default(),
2900 }
2901 }
2902
2903 #[test]
2904 fn no_auth_flag_disables_every_auth_knob_and_stamps_metadata() {
2905 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
2906 unsafe {
2911 std::env::set_var("REDDB_USERNAME", "admin");
2912 std::env::set_var("REDDB_PASSWORD", "hunter2");
2913 }
2914 let config = no_auth_test_config(true);
2915 let options = config.to_db_options().expect("to_db_options");
2916
2917 assert!(no_auth_active(&options), "metadata should be stamped");
2918 assert!(!options.auth.enabled, "auth.enabled must be forced off");
2919 assert!(
2920 !options.auth.require_auth,
2921 "require_auth must be forced off"
2922 );
2923 assert!(
2924 !options.auth.vault_enabled,
2925 "vault_enabled must be forced off (overrides --vault)"
2926 );
2927 assert_eq!(
2928 options.metadata.get(NO_AUTH_META).map(String::as_str),
2929 Some("true"),
2930 );
2931
2932 unsafe {
2934 std::env::remove_var("REDDB_USERNAME");
2935 std::env::remove_var("REDDB_PASSWORD");
2936 }
2937 }
2938
2939 #[test]
2940 fn default_behaviour_without_no_auth_flag_is_unchanged() {
2941 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
2942 let config = no_auth_test_config(false);
2943 let options = config.to_db_options().expect("to_db_options");
2944
2945 assert!(
2946 !no_auth_active(&options),
2947 "default boot must not be marked no-auth"
2948 );
2949 assert!(
2950 options.metadata.get(NO_AUTH_META).is_none(),
2951 "metadata key must be absent when flag is off"
2952 );
2953 assert!(options.auth.vault_enabled);
2955 }
2956
2957 #[test]
2958 fn no_auth_active_blocks_bootstrap_from_env() {
2959 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
2960 unsafe {
2965 std::env::set_var("REDDB_USERNAME", "admin");
2966 std::env::set_var("REDDB_PASSWORD", "hunter2");
2967 }
2968
2969 let options = no_auth_test_config(true)
2970 .to_db_options()
2971 .expect("to_db_options");
2972
2973 let auth_store = AuthStore::new(options.auth.clone());
2977 if !no_auth_active(&options) {
2978 auth_store.bootstrap_from_env();
2979 }
2980
2981 assert!(
2982 auth_store.needs_bootstrap(),
2983 "no admin user must be bootstrapped under --no-auth even with REDDB_USERNAME/PASSWORD set"
2984 );
2985
2986 unsafe {
2988 std::env::remove_var("REDDB_USERNAME");
2989 std::env::remove_var("REDDB_PASSWORD");
2990 }
2991 }
2992
2993 fn clear_preset_env() {
3000 unsafe {
3002 std::env::remove_var(BOOTSTRAP_PRESET_ENV);
3003 std::env::remove_var(PRESET_ENV);
3004 std::env::remove_var("REDDB_BOOTSTRAP_MANIFEST");
3005 std::env::remove_var("REDDB_AUTH");
3006 std::env::remove_var("REDDB_REQUIRE_AUTH");
3007 std::env::remove_var("REDDB_NO_AUTH");
3008 std::env::remove_var("REDDB_DEV");
3009 std::env::remove_var("REDDB_VAULT");
3010 std::env::remove_var("REDDB_USERNAME");
3011 std::env::remove_var("REDDB_PASSWORD");
3012 std::env::remove_var("REDDB_USERNAME_FILE");
3013 std::env::remove_var("REDDB_PASSWORD_FILE");
3014 std::env::remove_var("REDDB_CLOUD_HEAD_ADMIN");
3015 std::env::remove_var("REDDB_CLOUD_HEAD_ADMIN_PASSWORD");
3016 std::env::remove_var("REDDB_CLOUD_HEAD_ADMIN_PASSWORD_FILE");
3017 std::env::remove_var("REDDB_CUSTOMER_ADMIN");
3018 std::env::remove_var("REDDB_CUSTOMER_ADMIN_PASSWORD");
3019 std::env::remove_var("REDDB_CUSTOMER_ADMIN_PASSWORD_FILE");
3020 }
3021 }
3022
3023 fn clear_backup_env() {
3024 unsafe {
3026 std::env::remove_var("REDDB_BACKUP_S3_ENDPOINT");
3027 std::env::remove_var("REDDB_BACKUP_S3_BUCKET");
3028 std::env::remove_var("REDDB_BACKUP_S3_PREFIX");
3029 std::env::remove_var("REDDB_BACKUP_S3_ACCESS_KEY_ID");
3030 std::env::remove_var("REDDB_BACKUP_S3_SECRET_ACCESS_KEY");
3031 std::env::remove_var("REDDB_BACKUP_S3_REGION");
3032 std::env::remove_var("REDDB_BACKUP_CHECKPOINT_INTERVAL_SECS");
3033 std::env::remove_var("REDDB_BACKUP_WAL_FLUSH_INTERVAL_SECS");
3034 std::env::remove_var("REDDB_BACKUP_PAUSE_ON_LAG_SECS");
3035 }
3036 }
3037
3038 fn fresh_runtime_and_store() -> (RedDBRuntime, Arc<AuthStore>) {
3039 let runtime = RedDBRuntime::with_options(RedDBOptions::in_memory()).expect("runtime");
3040 let auth_store = Arc::new(AuthStore::new(crate::auth::AuthConfig::default()));
3041 (runtime, auth_store)
3042 }
3043
3044 #[test]
3045 fn auth_env_knobs_enable_auth_require_auth_and_vault() {
3046 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3047 clear_preset_env();
3048 unsafe {
3050 std::env::set_var("REDDB_AUTH", "true");
3051 std::env::set_var("REDDB_REQUIRE_AUTH", "true");
3052 std::env::set_var("REDDB_VAULT", "true");
3053 }
3054
3055 let mut config = no_auth_test_config(false);
3056 config.vault = false;
3057 let options = config.to_db_options().expect("to_db_options");
3058
3059 assert!(options.auth.enabled);
3060 assert!(options.auth.require_auth);
3061 assert!(options.auth.vault_enabled);
3062
3063 clear_preset_env();
3064 }
3065
3066 #[test]
3067 fn production_and_cloud_presets_force_auth_require_auth_and_vault() {
3068 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3069 clear_preset_env();
3070
3071 for preset in [PRESET_PRODUCTION, PRESET_CLOUD] {
3072 let mut config = no_auth_test_config(false);
3073 config.vault = false;
3074 config.bootstrap.preset = Some(preset.to_string());
3075
3076 let options = config.to_db_options().expect("to_db_options");
3077 assert!(options.auth.enabled, "{preset} should enable auth");
3078 assert!(
3079 options.auth.require_auth,
3080 "{preset} should require authenticated requests"
3081 );
3082 assert!(options.auth.vault_enabled, "{preset} should enable vault");
3083 assert!(!no_auth_active(&options));
3084 }
3085
3086 clear_preset_env();
3087 }
3088
3089 #[test]
3090 fn no_auth_env_overrides_preset_forced_auth() {
3091 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3092 clear_preset_env();
3093 unsafe {
3095 std::env::set_var("REDDB_NO_AUTH", "true");
3096 std::env::set_var(BOOTSTRAP_PRESET_ENV, PRESET_CLOUD);
3097 }
3098
3099 let mut config = no_auth_test_config(false);
3100 config.auth = true;
3101 config.require_auth = true;
3102 let options = config.to_db_options().expect("to_db_options");
3103
3104 assert!(no_auth_active(&options));
3105 assert!(!options.auth.enabled);
3106 assert!(!options.auth.require_auth);
3107 assert!(!options.auth.vault_enabled);
3108
3109 clear_preset_env();
3110 }
3111
3112 #[test]
3113 fn simple_preset_is_default_and_persists_state() {
3114 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3115 clear_preset_env();
3116
3117 let (runtime, auth_store) = fresh_runtime_and_store();
3118 apply_preset(&runtime, &auth_store).expect("simple preset applies cleanly");
3119
3120 assert!(
3122 auth_store.needs_bootstrap(),
3123 "simple preset must not create an admin"
3124 );
3125
3126 let store = runtime.db().store();
3128 let completed = store
3129 .get_config(BOOTSTRAP_COMPLETED_KEY)
3130 .expect("completed key persisted");
3131 assert!(matches!(completed, reddb_types::Value::Boolean(true)));
3132 let preset = store
3133 .get_config(BOOTSTRAP_PRESET_KEY)
3134 .expect("preset key persisted");
3135 match preset {
3136 reddb_types::Value::Text(s) => assert_eq!(s.as_ref(), PRESET_SIMPLE),
3137 other => panic!("expected Text(simple), got {other:?}"),
3138 }
3139 assert!(
3140 store.get_config(BOOTSTRAP_FIRST_ADMIN_KEY).is_none(),
3141 "simple preset must not record a first admin"
3142 );
3143
3144 clear_preset_env();
3145 }
3146
3147 #[test]
3148 fn production_preset_creates_first_admin_with_allow_all_policy() {
3149 use crate::auth::policies::{EvalContext, ResourceRef};
3150 use crate::auth::UserId;
3151
3152 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3153 clear_preset_env();
3154 unsafe {
3156 std::env::set_var(PRESET_ENV, PRESET_PRODUCTION);
3157 std::env::set_var("REDDB_USERNAME", "ops");
3158 std::env::set_var("REDDB_PASSWORD", "hunter2");
3159 }
3160
3161 let (runtime, auth_store) = fresh_runtime_and_store();
3162 apply_preset(&runtime, &auth_store).expect("production preset applies cleanly");
3163
3164 assert!(
3166 !auth_store.needs_bootstrap(),
3167 "production preset must seal bootstrap"
3168 );
3169 let users = auth_store.list_users();
3170 assert_eq!(users.len(), 1);
3171 let admin = &users[0];
3172 assert_eq!(admin.username, "ops");
3173 assert!(
3174 admin.tenant_id.is_none(),
3175 "first admin must be platform-scoped (tenant=None)"
3176 );
3177
3178 let policy = auth_store
3180 .get_policy(FIRST_ADMIN_ALLOW_ALL_POLICY)
3181 .expect("allow-all policy installed");
3182 assert!(!policy.statements.is_empty());
3183
3184 let actor = UserId::platform("ops");
3187 let ctx = EvalContext {
3188 principal_tenant: None,
3189 current_tenant: None,
3190 peer_ip: None,
3191 mfa_present: false,
3192 now_ms: 1_700_000_000_000,
3193 principal_is_admin_role: true,
3194 principal_is_platform_scoped: true,
3195 };
3196 let arbitrary_resource = ResourceRef::new("config", "red.config.audit.enabled");
3197 assert!(
3198 auth_store.check_policy_authz(&actor, "config:write", &arbitrary_resource, &ctx),
3199 "allow-all policy must grant arbitrary actions via the evaluator"
3200 );
3201
3202 let store = runtime.db().store();
3204 match store
3205 .get_config(BOOTSTRAP_FIRST_ADMIN_KEY)
3206 .expect("first_admin_id persisted")
3207 {
3208 reddb_types::Value::Text(s) => assert_eq!(s.as_ref(), "ops"),
3209 other => panic!("expected Text(ops), got {other:?}"),
3210 }
3211 match store.get_config(BOOTSTRAP_PRESET_KEY).unwrap() {
3212 reddb_types::Value::Text(s) => assert_eq!(s.as_ref(), PRESET_PRODUCTION),
3213 other => panic!("expected Text(production), got {other:?}"),
3214 }
3215
3216 clear_preset_env();
3217 }
3218
3219 #[test]
3220 fn bootstrap_preset_env_takes_precedence_over_legacy_preset_env() {
3221 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3222 clear_preset_env();
3223 unsafe {
3225 std::env::set_var(BOOTSTRAP_PRESET_ENV, PRESET_REGULATED);
3226 std::env::set_var(PRESET_ENV, PRESET_SIMPLE);
3227 }
3228
3229 let options = no_auth_test_config(false)
3230 .to_db_options()
3231 .expect("regulated options");
3232 assert!(
3233 options.control_events.compliance_mode,
3234 "canonical REDDB_BOOTSTRAP_PRESET should win over REDDB_PRESET"
3235 );
3236 assert!(options.query_audit.enabled);
3237
3238 clear_preset_env();
3239 }
3240
3241 #[test]
3242 fn cloud_preset_creates_system_head_and_customer_admins() {
3243 use crate::auth::policies::{EvalContext, ResourceRef};
3244 use crate::auth::UserId;
3245
3246 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3247 clear_preset_env();
3248 unsafe {
3250 std::env::set_var(BOOTSTRAP_PRESET_ENV, PRESET_CLOUD);
3251 std::env::set_var("REDDB_CLOUD_HEAD_ADMIN", "head");
3252 std::env::set_var("REDDB_CLOUD_HEAD_ADMIN_PASSWORD", "head-pass");
3253 std::env::set_var("REDDB_CUSTOMER_ADMIN", "customer");
3254 std::env::set_var("REDDB_CUSTOMER_ADMIN_PASSWORD", "customer-pass");
3255 }
3256
3257 let (runtime, auth_store) = fresh_runtime_and_store();
3258 apply_preset(&runtime, &auth_store).expect("cloud preset applies cleanly");
3259
3260 let head = auth_store
3261 .get_user(None, "head")
3262 .expect("head admin should exist");
3263 assert_eq!(head.tenant_id, None);
3264 let customer = auth_store
3265 .get_user(None, "customer")
3266 .expect("customer admin should exist");
3267 assert_eq!(customer.tenant_id, None);
3268
3269 let ctx = EvalContext {
3270 principal_tenant: None,
3271 current_tenant: None,
3272 peer_ip: None,
3273 mfa_present: false,
3274 now_ms: 1_700_000_000_000,
3275 principal_is_admin_role: true,
3276 principal_is_platform_scoped: true,
3277 };
3278 assert!(auth_store.check_policy_authz(
3279 &UserId::platform("customer"),
3280 "config:write",
3281 &ResourceRef::new("config", "red.config.customer.enabled"),
3282 &ctx,
3283 ));
3284 assert!(
3285 auth_store.check_policy_authz(
3286 &UserId::platform("head"),
3287 "config:write",
3288 &ResourceRef::new("config", "red.config.cloud.enabled"),
3289 &ctx,
3290 ),
3291 "cloud head keeps allow-all authority over managed cloud config"
3292 );
3293 assert!(
3294 !auth_store.check_policy_authz(
3295 &UserId::platform("customer"),
3296 "config:write",
3297 &ResourceRef::new("config", "red.config.cloud.enabled"),
3298 &ctx,
3299 ),
3300 "cloud customer must be denied managed cloud config writes"
3301 );
3302 assert!(
3303 !auth_store.check_policy_authz(
3304 &UserId::platform("customer"),
3305 "policy:drop",
3306 &ResourceRef::new("policy", CLOUD_PROTECT_MANAGED_POLICY),
3307 &ctx,
3308 ),
3309 "cloud customer must be denied mutations of the managed guardrail policy"
3310 );
3311
3312 assert!(auth_store.get_user(None, "head").is_some());
3313 assert!(auth_store
3314 .get_policy(CLOUD_PROTECT_MANAGED_POLICY)
3315 .is_some());
3316 assert!(runtime
3317 .config_registry()
3318 .get_active(CLOUD_CONFIG_NAMESPACE)
3319 .is_some());
3320
3321 let store = runtime.db().store();
3322 match store
3323 .get_config(BOOTSTRAP_FIRST_ADMIN_KEY)
3324 .expect("head admin id persisted")
3325 {
3326 reddb_types::Value::Text(s) => assert_eq!(s.as_ref(), "head"),
3327 other => panic!("expected Text(head), got {other:?}"),
3328 }
3329 match store.get_config(BOOTSTRAP_PRESET_KEY).unwrap() {
3330 reddb_types::Value::Text(s) => assert_eq!(s.as_ref(), PRESET_CLOUD),
3331 other => panic!("expected Text(cloud), got {other:?}"),
3332 }
3333
3334 clear_preset_env();
3335 }
3336
3337 #[test]
3338 fn cloud_preset_customer_cannot_disable_head_admin_or_drop_guardrail_policy() {
3339 use crate::auth::Role;
3340 use crate::runtime::mvcc::{clear_current_auth_identity, set_current_auth_identity};
3341
3342 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3343 clear_preset_env();
3344
3345 let bootstrap = BootstrapConfig {
3346 preset: Some(PRESET_CLOUD.to_string()),
3347 cloud_head_admin: Some("red_admin".to_string()),
3348 cloud_head_admin_password: Some("head-pass".to_string()),
3349 customer_admin: Some("admin".to_string()),
3350 customer_admin_password: Some("customer-pass".to_string()),
3351 ..BootstrapConfig::default()
3352 };
3353 let (runtime, auth_store) = fresh_runtime_and_store();
3354 apply_preset_from_config(&runtime, &auth_store, &bootstrap)
3355 .expect("cloud preset applies cleanly");
3356 runtime.set_auth_store(Arc::clone(&auth_store));
3357
3358 set_current_auth_identity("admin".to_string(), Role::Admin);
3359 let disable_head = runtime.execute_query("ALTER USER red_admin DISABLE");
3360 let drop_guardrail =
3361 runtime.execute_query(&format!("DROP POLICY '{}'", CLOUD_PROTECT_MANAGED_POLICY));
3362 let create_user = runtime.execute_query("CREATE USER app_user WITH PASSWORD 'p' ROLE read");
3363 let disable_user = runtime.execute_query("ALTER USER app_user DISABLE");
3364 clear_current_auth_identity();
3365
3366 let disable_head_err = disable_head.expect_err("customer admin must not disable red_admin");
3367 assert!(
3368 disable_head_err.to_string().contains("user:disable"),
3369 "error should name the denied lifecycle action: {disable_head_err}"
3370 );
3371 assert!(
3372 auth_store
3373 .get_user(None, "red_admin")
3374 .expect("red_admin should still exist")
3375 .enabled,
3376 "denied mutation must leave red_admin enabled"
3377 );
3378
3379 let drop_guardrail_err =
3380 drop_guardrail.expect_err("customer admin must not drop cloud guardrail policy");
3381 assert!(
3382 drop_guardrail_err.to_string().contains("managed policy"),
3383 "error should name the managed policy guardrail: {drop_guardrail_err}"
3384 );
3385 assert!(auth_store
3386 .get_policy(CLOUD_PROTECT_MANAGED_POLICY)
3387 .is_some());
3388
3389 create_user.expect("customer admin should retain normal create-user authority");
3390 disable_user.expect("customer admin should retain normal user-disable authority");
3391 assert!(
3392 !auth_store
3393 .get_user(None, "app_user")
3394 .expect("app_user should exist")
3395 .enabled,
3396 "normal capability should still apply outside the reserved head admin"
3397 );
3398
3399 clear_preset_env();
3400 }
3401
3402 #[test]
3403 fn cloud_preset_cli_admin_alias_wins_over_cloud_env_password() {
3404 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3405 clear_preset_env();
3406 unsafe {
3408 std::env::set_var("REDDB_CLOUD_HEAD_ADMIN_PASSWORD", "env-head-pass");
3409 }
3410
3411 let bootstrap = BootstrapConfig {
3412 preset: Some(PRESET_CLOUD.to_string()),
3413 admin_username: Some("head".to_string()),
3414 admin_password: Some("cli-head-pass".to_string()),
3415 customer_admin: Some("customer".to_string()),
3416 customer_admin_password: Some("customer-pass".to_string()),
3417 ..BootstrapConfig::default()
3418 };
3419 let (runtime, auth_store) = fresh_runtime_and_store();
3420 apply_preset_from_config(&runtime, &auth_store, &bootstrap)
3421 .expect("cloud preset applies cleanly");
3422
3423 auth_store
3424 .authenticate("head", "cli-head-pass")
3425 .expect("CLI alias password should win");
3426 assert!(
3427 auth_store.authenticate("head", "env-head-pass").is_err(),
3428 "cloud-specific env password must not beat CLI alias"
3429 );
3430
3431 clear_preset_env();
3432 }
3433
3434 #[test]
3435 fn bootstrap_intent_manifest_matches_cloud_flag_path() {
3436 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3437 clear_preset_env();
3438
3439 let manifest_dir = std::env::current_dir()
3440 .expect("current dir")
3441 .join(".red/tmp/bootstrap-manifest-tests");
3442 std::fs::create_dir_all(&manifest_dir).expect("create manifest test dir");
3443 let unique = format!(
3444 "{}-{}",
3445 std::process::id(),
3446 std::time::SystemTime::now()
3447 .duration_since(std::time::UNIX_EPOCH)
3448 .unwrap_or_default()
3449 .as_millis()
3450 );
3451 let head_password = manifest_dir.join(format!("head-{unique}.secret"));
3452 let customer_password = manifest_dir.join(format!("customer-{unique}.secret"));
3453 let manifest_path = manifest_dir.join(format!("intent-{unique}.json"));
3454 std::fs::write(&head_password, "head-pass\n").expect("write head secret");
3455 std::fs::write(&customer_password, "customer-pass\n").expect("write customer secret");
3456 std::fs::write(
3457 &manifest_path,
3458 format!(
3459 r#"{{
3460 "bootstrap": {{
3461 "preset": "cloud",
3462 "cloud_head_admin": {{
3463 "username": "head",
3464 "password_file": "{}"
3465 }},
3466 "customer_admin": {{
3467 "username": "customer",
3468 "password_file": "{}"
3469 }}
3470 }}
3471 }}"#,
3472 head_password.display(),
3473 customer_password.display()
3474 ),
3475 )
3476 .expect("write manifest");
3477
3478 let mut config = no_auth_test_config(false);
3479 config.bootstrap.manifest = Some(manifest_path.clone());
3480 let options = config.to_db_options().expect("manifest options");
3481 assert!(options.auth.enabled);
3482 assert!(options.auth.require_auth);
3483 assert!(options.auth.vault_enabled);
3484
3485 let (runtime, auth_store) = fresh_runtime_and_store();
3486 apply_preset_from_config(&runtime, &auth_store, &config.bootstrap)
3487 .expect("manifest intent applies cleanly");
3488
3489 auth_store
3490 .authenticate("head", "head-pass")
3491 .expect("head password comes from file");
3492 auth_store
3493 .authenticate("customer", "customer-pass")
3494 .expect("customer password comes from file");
3495 assert!(auth_store.get_user(None, "head").is_some());
3496 assert!(auth_store.get_user(None, "customer").is_some());
3497 assert!(auth_store
3498 .get_policy(CLOUD_PROTECT_MANAGED_POLICY)
3499 .is_some());
3500 match runtime
3501 .db()
3502 .store()
3503 .get_config(BOOTSTRAP_PRESET_KEY)
3504 .expect("preset key persisted")
3505 {
3506 reddb_types::Value::Text(s) => assert_eq!(s.as_ref(), PRESET_CLOUD),
3507 other => panic!("expected Text(cloud), got {other:?}"),
3508 }
3509
3510 apply_preset_from_config(&runtime, &auth_store, &config.bootstrap)
3511 .expect("second boot is idempotent");
3512
3513 let _ = std::fs::remove_file(&manifest_path);
3514 let _ = std::fs::remove_file(&head_password);
3515 let _ = std::fs::remove_file(&customer_password);
3516 clear_preset_env();
3517 }
3518
3519 #[test]
3520 fn regulated_preset_enables_query_audit_infrastructure_without_rules() {
3521 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3522 clear_preset_env();
3523 unsafe {
3525 std::env::set_var(PRESET_ENV, PRESET_REGULATED);
3526 }
3527
3528 let (runtime, auth_store) = fresh_runtime_and_store();
3529 apply_preset(&runtime, &auth_store).expect("regulated preset applies cleanly");
3530
3531 assert!(runtime.query_audit().is_enabled());
3532 assert!(runtime.query_audit().rules().is_empty());
3533 assert!(
3534 runtime
3535 .db()
3536 .store()
3537 .get_collection(crate::runtime::query_audit::QUERY_AUDIT_COLLECTION)
3538 .is_some(),
3539 "regulated preset should create the query-audit stream"
3540 );
3541
3542 runtime
3543 .execute_query("CREATE TABLE docs (id INT)")
3544 .expect("create table");
3545 runtime
3546 .execute_query("INSERT INTO docs (id) VALUES (1)")
3547 .expect("insert");
3548 runtime.execute_query("SELECT * FROM docs").expect("select");
3549 let rows = runtime
3550 .db()
3551 .store()
3552 .get_collection(crate::runtime::query_audit::QUERY_AUDIT_COLLECTION)
3553 .expect("query audit collection")
3554 .query_all(|_| true);
3555 assert!(
3556 rows.is_empty(),
3557 "regulated preset must not globally audit every query"
3558 );
3559
3560 clear_preset_env();
3561 }
3562
3563 #[test]
3564 fn managed_backup_env_rejects_primary_replica_single_file_storage() {
3565 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3566 clear_backup_env();
3567 unsafe {
3569 std::env::set_var("REDDB_BACKUP_S3_ENDPOINT", "https://s3.example.test");
3570 std::env::set_var("REDDB_BACKUP_S3_BUCKET", "reddb");
3571 std::env::set_var("REDDB_BACKUP_S3_PREFIX", "clusters/prod");
3572 std::env::set_var("REDDB_BACKUP_S3_ACCESS_KEY_ID", "AK");
3573 std::env::set_var("REDDB_BACKUP_S3_SECRET_ACCESS_KEY", "SK");
3574 }
3575
3576 let mut config = no_auth_test_config(false);
3577 config.role = "primary".to_string();
3578 config.storage_profile = crate::storage::StorageDeployPreset::PrimaryReplicaDev.selection();
3579
3580 let err = config.to_db_options().unwrap_err();
3581 assert!(err.contains("managed backup"), "got: {err}");
3582 assert!(err.contains("operational-directory"), "got: {err}");
3583
3584 clear_backup_env();
3585 }
3586
3587 #[test]
3588 fn regulated_preset_installs_managed_evidence_guardrails_end_to_end() {
3589 use crate::auth::policies::{EvalContext, Policy, ResourceRef};
3590 use crate::auth::store::PrincipalRef;
3591 use crate::auth::{Role, UserId};
3592 use crate::runtime::mvcc::{clear_current_auth_identity, set_current_auth_identity};
3593 use reddb_types::Value;
3594
3595 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3596 clear_preset_env();
3597 unsafe {
3599 std::env::set_var(PRESET_ENV, PRESET_REGULATED);
3600 }
3601
3602 let options = no_auth_test_config(false)
3603 .to_db_options()
3604 .expect("regulated options");
3605 assert!(
3606 options.control_events.compliance_mode,
3607 "regulated preset must enable fail-closed control evidence before runtime boot"
3608 );
3609 assert!(
3610 options.query_audit.enabled && options.query_audit.rules.is_empty(),
3611 "regulated preset must enable query-audit infrastructure without global rules"
3612 );
3613
3614 let runtime = RedDBRuntime::with_options(options).expect("runtime");
3615 let auth_store = Arc::new(AuthStore::new(crate::auth::AuthConfig::default()));
3616 apply_preset(&runtime, &auth_store).expect("regulated preset applies cleanly");
3617 runtime.set_auth_store(Arc::clone(&auth_store));
3618
3619 assert!(runtime.control_events_require_persistence());
3620 assert!(runtime.query_audit().is_enabled());
3621 assert!(runtime.query_audit().rules().is_empty());
3622 assert!(auth_store
3623 .get_policy(REGULATED_PROTECT_MANAGED_POLICY)
3624 .is_some());
3625
3626 let managed_policy = runtime
3627 .config_registry()
3628 .get_active(REGULATED_PROTECT_MANAGED_POLICY)
3629 .expect("regulated managed policy registry entry");
3630 assert!(managed_policy.managed);
3631 assert_eq!(managed_policy.resource_type, "policy");
3632 assert!(
3633 runtime
3634 .config_registry()
3635 .get_active(REGULATED_AUDIT_CONFIG_NAMESPACE)
3636 .expect("regulated audit config namespace")
3637 .managed
3638 );
3639
3640 let registry_rows = runtime
3641 .execute_query(&format!(
3642 "SELECT id, managed FROM red.registry WHERE id = '{}'",
3643 REGULATED_PROTECT_MANAGED_POLICY
3644 ))
3645 .expect("red.registry query");
3646 assert_eq!(registry_rows.result.records.len(), 1);
3647 assert_eq!(
3648 registry_rows.result.records[0].get("managed"),
3649 Some(&Value::Boolean(true))
3650 );
3651
3652 let managed_policy_rows = runtime
3653 .execute_query(&format!(
3654 "SELECT policy_id FROM red.managed_policies WHERE policy_id = '{}'",
3655 REGULATED_PROTECT_MANAGED_POLICY
3656 ))
3657 .expect("red.managed_policies query");
3658 assert_eq!(managed_policy_rows.result.records.len(), 1);
3659
3660 let capability_rows = runtime
3661 .execute_query(
3662 "SELECT action FROM red.control_capabilities WHERE action = 'evidence:export'",
3663 )
3664 .expect("red.control_capabilities query");
3665 assert_eq!(capability_rows.result.records.len(), 1);
3666
3667 auth_store
3668 .create_user("alice", "p", Role::Admin)
3669 .expect("create ordinary admin");
3670 let allow_all = Policy::from_json_str(
3671 r#"{
3672 "id": "alice-allow-all",
3673 "version": 1,
3674 "statements": [{
3675 "effect": "allow",
3676 "actions": ["*"],
3677 "resources": ["*"]
3678 }]
3679 }"#,
3680 )
3681 .expect("allow-all policy");
3682 auth_store.put_policy(allow_all).expect("install allow-all");
3683 auth_store
3684 .attach_policy(
3685 PrincipalRef::User(UserId::platform("alice")),
3686 "alice-allow-all",
3687 )
3688 .expect("attach allow-all");
3689 let ctx = EvalContext {
3690 principal_tenant: None,
3691 current_tenant: None,
3692 peer_ip: None,
3693 mfa_present: false,
3694 now_ms: 1_700_000_000_000,
3695 principal_is_admin_role: true,
3696 principal_is_platform_scoped: true,
3697 };
3698 assert!(
3699 !auth_store.check_policy_authz(
3700 &UserId::platform("alice"),
3701 "policy:drop",
3702 &ResourceRef::new("policy", REGULATED_PROTECT_MANAGED_POLICY),
3703 &ctx,
3704 ),
3705 "managed guardrail deny policy must be effective for ordinary admins"
3706 );
3707
3708 set_current_auth_identity("alice".to_string(), Role::Admin);
3709 let denied = runtime.execute_query(&format!(
3710 "DROP POLICY '{}'",
3711 REGULATED_PROTECT_MANAGED_POLICY
3712 ));
3713 clear_current_auth_identity();
3714 let err = denied.expect_err("managed policy guardrail must deny ordinary admin");
3715 assert!(
3716 err.to_string().contains("managed policy"),
3717 "error should name the managed guardrail: {err}"
3718 );
3719 assert!(
3720 auth_store
3721 .get_policy(REGULATED_PROTECT_MANAGED_POLICY)
3722 .is_some(),
3723 "denied mutation must leave managed policy installed"
3724 );
3725
3726 let denied_events = runtime
3727 .execute_query(&format!(
3728 "SELECT action, resource, outcome FROM red.control_events \
3729 WHERE action = 'policy:drop' AND resource = 'policy:{}'",
3730 REGULATED_PROTECT_MANAGED_POLICY
3731 ))
3732 .expect("red.control_events denied policy drop");
3733 assert_eq!(denied_events.result.records.len(), 1);
3734 assert_eq!(
3735 denied_events.result.records[0].get("outcome"),
3736 Some(&Value::text("denied"))
3737 );
3738
3739 set_current_auth_identity("alice".to_string(), Role::Admin);
3740 let config_denied = runtime.execute_query("SET CONFIG red.config.audit.enabled = true");
3741 clear_current_auth_identity();
3742 let err = config_denied.expect_err("managed config guardrail must deny ordinary admin");
3743 assert!(
3744 err.to_string().contains("managed config"),
3745 "error should name the managed config guardrail: {err}"
3746 );
3747
3748 let denied_config_events = runtime
3749 .execute_query(
3750 "SELECT action, resource, outcome FROM red.control_events \
3751 WHERE action = 'config:write' AND resource = 'config:red.config.audit.enabled'",
3752 )
3753 .expect("red.control_events denied config write");
3754 assert_eq!(denied_config_events.result.records.len(), 1);
3755 assert_eq!(
3756 denied_config_events.result.records[0].get("outcome"),
3757 Some(&Value::text("denied"))
3758 );
3759
3760 runtime
3761 .execute_query("CREATE TABLE regulated_docs (id INT)")
3762 .expect("create user table");
3763 runtime
3764 .execute_query("SELECT * FROM regulated_docs")
3765 .expect("select user table");
3766 let audit_rows = runtime
3767 .db()
3768 .store()
3769 .get_collection(crate::runtime::query_audit::QUERY_AUDIT_COLLECTION)
3770 .expect("query audit collection")
3771 .query_all(|_| true);
3772 assert!(
3773 audit_rows.is_empty(),
3774 "regulated preset must not globally audit data-plane queries"
3775 );
3776
3777 clear_preset_env();
3778 }
3779
3780 #[test]
3781 fn bootstrap_manifest_installs_initial_users_policies_guardrails_and_config() {
3782 use crate::auth::policies::{EvalContext, ResourceRef};
3783 use crate::auth::UserId;
3784 use reddb_types::Value;
3785
3786 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3787 clear_preset_env();
3788
3789 let manifest_dir = std::env::current_dir()
3790 .expect("current dir")
3791 .join(".red/tmp/bootstrap-manifest-tests");
3792 std::fs::create_dir_all(&manifest_dir).expect("create manifest test dir");
3793 let manifest_path = manifest_dir.join(format!(
3794 "reddb-bootstrap-manifest-{}-{}.json",
3795 std::process::id(),
3796 std::time::SystemTime::now()
3797 .duration_since(std::time::UNIX_EPOCH)
3798 .unwrap_or_default()
3799 .as_millis()
3800 ));
3801 std::fs::write(
3802 &manifest_path,
3803 r#"{
3804 "users": [
3805 {
3806 "username": "ops",
3807 "password": "hunter2",
3808 "role": "admin"
3809 }
3810 ],
3811 "policies": [
3812 {
3813 "id": "bootstrap-registry-admin",
3814 "version": 1,
3815 "statements": [
3816 {
3817 "effect": "allow",
3818 "actions": ["red.registry:*", "policy:*", "config:write", "select"],
3819 "resources": ["registry:*", "policy:*", "config:*", "collection:docs"]
3820 }
3821 ]
3822 }
3823 ],
3824 "managed_policies": [
3825 {
3826 "id": "managed-deny-drop",
3827 "version": 1,
3828 "statements": [
3829 {
3830 "effect": "deny",
3831 "actions": ["policy:drop"],
3832 "resources": ["policy:managed-deny-drop"]
3833 }
3834 ],
3835 "required_resource": "policy:managed-deny-drop",
3836 "evidence": "full"
3837 }
3838 ],
3839 "attachments": [
3840 {"user": "ops", "policy": "bootstrap-registry-admin"}
3841 ],
3842 "managed_config_namespaces": [
3843 {
3844 "id": "red.ai",
3845 "required_action": "config:write",
3846 "required_resource": "config:red.ai.*",
3847 "evidence": "metadata"
3848 }
3849 ],
3850 "config": [
3851 {"key": "red.ai.default.provider", "value": "openai"},
3852 {
3853 "key": "red.ai.openai.default.secret_ref",
3854 "secret_ref": {"collection": "red.vault", "key": "openai"}
3855 }
3856 ],
3857 "actor": "ops"
3858 }"#,
3859 )
3860 .expect("write manifest");
3861 unsafe {
3863 std::env::set_var("REDDB_BOOTSTRAP_MANIFEST", &manifest_path);
3864 }
3865
3866 let (runtime, auth_store) = fresh_runtime_and_store();
3867 apply_preset(&runtime, &auth_store).expect("manifest applies cleanly");
3868
3869 let users = auth_store.list_users();
3870 assert_eq!(users.len(), 1);
3871 assert_eq!(users[0].username, "ops");
3872 assert!(users[0].tenant_id.is_none());
3873
3874 let actor = UserId::platform("ops");
3875 let ctx = EvalContext {
3876 principal_tenant: None,
3877 current_tenant: None,
3878 peer_ip: None,
3879 mfa_present: false,
3880 now_ms: 1_700_000_000_000,
3881 principal_is_admin_role: true,
3882 principal_is_platform_scoped: true,
3883 };
3884 assert!(auth_store.check_policy_authz(
3886 &actor,
3887 "select",
3888 &ResourceRef::new("collection", "docs"),
3889 &ctx
3890 ));
3891
3892 let managed_policy = runtime
3893 .config_registry()
3894 .get_active("managed-deny-drop")
3895 .expect("managed policy registry entry");
3896 assert!(managed_policy.managed);
3897 assert_eq!(managed_policy.resource_type, "policy");
3898 let managed_config = runtime
3899 .config_registry()
3900 .get_active("red.ai")
3901 .expect("managed config namespace registry entry");
3902 assert!(managed_config.managed);
3903 assert_eq!(managed_config.resource_type, "config_namespace");
3904
3905 let store = runtime.db().store();
3906 match store
3907 .get_config("red.ai.default.provider")
3908 .expect("plain config persisted")
3909 {
3910 Value::Text(s) => assert_eq!(s.as_ref(), "openai"),
3911 other => panic!("expected provider text, got {other:?}"),
3912 }
3913 let Value::Json(bytes) = store
3914 .get_config("red.ai.openai.default.secret_ref")
3915 .expect("secret ref config persisted")
3916 else {
3917 panic!("secret ref must be stored as structured JSON");
3918 };
3919 let reference: crate::serde_json::Value =
3920 crate::serde_json::from_slice(&bytes).expect("secret ref json");
3921 assert_eq!(
3922 reference.get("type").and_then(|v| v.as_str()),
3923 Some("secret_ref")
3924 );
3925 assert!(
3926 !String::from_utf8_lossy(&bytes).contains("hunter2"),
3927 "manifest password must not leak into secret ref config"
3928 );
3929
3930 let completed = store
3931 .get_config(BOOTSTRAP_COMPLETED_KEY)
3932 .expect("bootstrap completion persisted");
3933 assert!(matches!(completed, Value::Boolean(true)));
3934 assert!(
3935 store
3936 .get_config("system.bootstrap.manifest.registry_entries")
3937 .is_some(),
3938 "managed registry entries must be persisted internally"
3939 );
3940
3941 std::fs::remove_file(&manifest_path).expect("remove manifest after first boot");
3942 let restored_registry = Arc::new(crate::auth::registry::ConfigRegistry::new());
3943 crate::cli::bootstrap_manifest::rehydrate_manifest_registry(&runtime, &restored_registry)
3944 .expect("registry rehydrates without manifest file");
3945 assert!(restored_registry.get_active("managed-deny-drop").is_some());
3946 assert!(restored_registry.get_active("red.ai").is_some());
3947
3948 let fresh = Arc::new(AuthStore::new(crate::auth::AuthConfig::default()));
3949 apply_preset(&runtime, &fresh).expect("re-run must not need manifest file");
3950 assert!(fresh.needs_bootstrap());
3951
3952 clear_preset_env();
3953 }
3954
3955 #[test]
3956 fn production_preset_refuses_to_start_without_password() {
3957 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3958 clear_preset_env();
3959 unsafe {
3961 std::env::set_var(PRESET_ENV, PRESET_PRODUCTION);
3962 std::env::set_var("REDDB_USERNAME", "ops");
3963 }
3964
3965 let (runtime, auth_store) = fresh_runtime_and_store();
3966 let err = apply_preset(&runtime, &auth_store).expect_err("must reject missing password");
3967 assert!(
3968 err.contains("REDDB_PASSWORD"),
3969 "error must name the missing env: {err}"
3970 );
3971
3972 assert!(auth_store.needs_bootstrap());
3974 assert!(runtime
3975 .db()
3976 .store()
3977 .get_config(BOOTSTRAP_COMPLETED_KEY)
3978 .is_none());
3979
3980 clear_preset_env();
3981 }
3982
3983 #[test]
3984 fn re_running_production_after_first_boot_is_a_silent_skip() {
3985 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3986 clear_preset_env();
3987 unsafe {
3989 std::env::set_var(PRESET_ENV, PRESET_PRODUCTION);
3990 std::env::set_var("REDDB_USERNAME", "ops");
3991 std::env::set_var("REDDB_PASSWORD", "hunter2");
3992 }
3993
3994 let (runtime, auth_store) = fresh_runtime_and_store();
3995 apply_preset(&runtime, &auth_store).expect("first apply");
3996 assert_eq!(auth_store.list_users().len(), 1);
3997
3998 let fresh = Arc::new(AuthStore::new(crate::auth::AuthConfig::default()));
4005 apply_preset(&runtime, &fresh).expect("re-run is silent-skip");
4006 assert!(
4007 fresh.needs_bootstrap(),
4008 "re-run must not create a second admin"
4009 );
4010 assert!(
4011 fresh.get_policy(FIRST_ADMIN_ALLOW_ALL_POLICY).is_none(),
4012 "re-run must not re-install the allow-all policy on the fresh store"
4013 );
4014
4015 clear_preset_env();
4016 }
4017
4018 #[test]
4019 fn unrecognised_preset_value_is_rejected() {
4020 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
4021 clear_preset_env();
4022 unsafe {
4024 std::env::set_var(PRESET_ENV, "weird");
4025 }
4026
4027 let (runtime, auth_store) = fresh_runtime_and_store();
4028 let err = apply_preset(&runtime, &auth_store).expect_err("must reject unknown preset");
4029 assert!(err.contains("weird"), "error must echo the value: {err}");
4030 assert!(auth_store.needs_bootstrap());
4031
4032 clear_preset_env();
4033 }
4034
4035 #[test]
4036 fn no_auth_short_circuits_preset_entirely() {
4037 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
4038 clear_preset_env();
4039 unsafe {
4042 std::env::set_var(PRESET_ENV, PRESET_PRODUCTION);
4043 std::env::set_var("REDDB_USERNAME", "ops");
4044 std::env::set_var("REDDB_PASSWORD", "hunter2");
4045 }
4046
4047 let options = no_auth_test_config(true)
4048 .to_db_options()
4049 .expect("to_db_options");
4050 assert!(no_auth_active(&options));
4051
4052 let (runtime, auth_store) = fresh_runtime_and_store();
4055 if !no_auth_active(&options) {
4056 apply_preset(&runtime, &auth_store).expect("would apply preset");
4057 }
4058
4059 assert!(
4060 auth_store.needs_bootstrap(),
4061 "--no-auth must prevent any admin creation"
4062 );
4063 assert!(
4064 runtime
4065 .db()
4066 .store()
4067 .get_config(BOOTSTRAP_COMPLETED_KEY)
4068 .is_none(),
4069 "--no-auth must skip bootstrap-state persistence"
4070 );
4071
4072 clear_preset_env();
4073 }
4074
4075 #[test]
4076 fn implicit_bind_collision_degrades() {
4077 let held = TcpListener::bind("127.0.0.1:0").expect("hold test port");
4078 let addr = held.local_addr().expect("held addr").to_string();
4079 let mut readiness = TransportReadiness::default();
4080
4081 let listener =
4082 bind_listener_for_startup(&mut readiness, "http", &addr, false).expect("nonfatal");
4083
4084 assert!(listener.is_none());
4085 assert_eq!(readiness.active.len(), 0);
4086 assert_eq!(readiness.failed.len(), 1);
4087 assert!(!readiness.failed[0].explicit);
4088 assert_eq!(readiness.failed[0].bind_addr, addr);
4089 }
4090}