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