1use std::net::{SocketAddr, TcpListener, TcpStream, ToSocketAddrs};
2use std::path::PathBuf;
3use std::process::Command;
4use std::sync::Arc;
5use std::thread;
6use std::time::Duration;
7
8use crate::auth::store::AuthStore;
9use crate::replication::ReplicationConfig;
10use crate::runtime::RedDBRuntime;
11use crate::service_router::{serve_tcp_router, InProcessRouterConfig};
12use crate::storage::StorageProfileSelection;
13pub use crate::transport::{TransportListenerFailure, TransportListenerState, TransportReadiness};
14use crate::{
15 GrpcServerOptions, RedDBGrpcServer, RedDBOptions, RedDBServer, ServerOptions, StorageMode,
16};
17
18mod server_bootstrap;
19pub use server_bootstrap::{bootstrap_server, run_server, transport_set, BootedNode};
20
21pub const DEFAULT_ROUTER_BIND_ADDR: &str = "127.0.0.1:5050";
22
23fn suggested_worker_count(cpus: usize) -> usize {
24 match cpus {
25 0 | 1 => 1,
26 2 => 2,
27 _ => cpus - 1,
28 }
29}
30
31pub fn detect_runtime_config() -> RuntimeConfig {
33 let cpus = thread::available_parallelism()
34 .map(|n| n.get())
35 .unwrap_or(1);
36
37 let suggested_workers = suggested_worker_count(cpus);
40
41 RuntimeConfig {
42 available_cpus: cpus,
43 suggested_workers,
44 stack_size: 8 * 1024 * 1024, }
46}
47
48#[derive(Debug, Clone)]
49pub struct RuntimeConfig {
50 pub available_cpus: usize,
51 pub suggested_workers: usize,
52 pub stack_size: usize,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum ServerTransport {
57 Grpc,
58 Http,
59 Wire,
60}
61
62impl ServerTransport {
63 pub const fn as_str(self) -> &'static str {
64 match self {
65 Self::Grpc => "gRPC",
66 Self::Http => "HTTP",
67 Self::Wire => "wire",
68 }
69 }
70
71 pub const fn default_bind_addr(self) -> &'static str {
72 match self {
73 Self::Grpc => "127.0.0.1:55055",
74 Self::Http => "127.0.0.1:5000",
75 Self::Wire => "127.0.0.1:5050",
76 }
77 }
78}
79
80#[derive(Debug, Clone)]
81pub struct ServerCommandConfig {
82 pub path: Option<PathBuf>,
83 pub router_bind_addr: Option<String>,
84 pub router_bind_explicit: bool,
85 pub grpc_bind_addr: Option<String>,
86 pub grpc_bind_explicit: bool,
87 pub grpc_tls_bind_addr: Option<String>,
91 pub grpc_tls_cert: Option<PathBuf>,
97 pub grpc_tls_key: Option<PathBuf>,
100 pub grpc_tls_client_ca: Option<PathBuf>,
105 pub http_bind_addr: Option<String>,
106 pub http_bind_explicit: bool,
107 pub http_tls_bind_addr: Option<String>,
111 pub http_tls_cert: Option<PathBuf>,
114 pub http_tls_key: Option<PathBuf>,
117 pub http_tls_client_ca: Option<PathBuf>,
121 pub wire_bind_addr: Option<String>,
122 pub wire_bind_explicit: bool,
123 pub wire_tls_bind_addr: Option<String>,
125 pub wire_tls_cert: Option<PathBuf>,
127 pub wire_tls_key: Option<PathBuf>,
129 pub pg_bind_addr: Option<String>,
133 pub create_if_missing: bool,
134 pub bounded_disk_residency: bool,
135 pub read_only: bool,
136 pub role: String,
137 pub primary_addr: Option<String>,
138 pub storage_profile: StorageProfileSelection,
139 pub auth: bool,
142 pub require_auth: bool,
144 pub vault: bool,
145 pub no_auth: bool,
156 pub workers: Option<usize>,
158 pub telemetry: Option<crate::telemetry::TelemetryConfig>,
161 pub http_limits_cli: crate::server::HttpLimitsCliInput,
166 pub ui: bool,
171 pub ui_dir: Option<PathBuf>,
175 pub bootstrap: BootstrapConfig,
178}
179
180#[derive(Debug, Clone, Default)]
181pub struct BootstrapConfig {
182 pub preset: Option<String>,
183 pub manifest: Option<PathBuf>,
184 pub admin_username: Option<String>,
185 pub admin_password: Option<String>,
186 pub cloud_head_admin: Option<String>,
187 pub cloud_head_admin_password: Option<String>,
188 pub customer_admin: Option<String>,
189 pub customer_admin_password: Option<String>,
190 pub cert_out: Option<PathBuf>,
199}
200
201#[derive(Debug, Clone)]
202pub struct SystemdServiceConfig {
203 pub service_name: String,
204 pub binary_path: PathBuf,
205 pub run_user: String,
206 pub run_group: String,
207 pub data_path: PathBuf,
208 pub router_bind_addr: Option<String>,
209 pub grpc_bind_addr: Option<String>,
210 pub http_bind_addr: Option<String>,
211}
212
213impl SystemdServiceConfig {
214 pub fn data_dir(&self) -> PathBuf {
215 self.data_path
216 .parent()
217 .map(PathBuf::from)
218 .unwrap_or_else(|| PathBuf::from("."))
219 }
220
221 pub fn unit_path(&self) -> PathBuf {
222 PathBuf::from(format!("/etc/systemd/system/{}.service", self.service_name))
223 }
224}
225
226pub fn default_telemetry_for_path(
231 path: Option<&std::path::Path>,
232) -> crate::telemetry::TelemetryConfig {
233 let log_dir = match path {
234 Some(p) => p
235 .parent()
236 .map(|parent| parent.join("logs"))
237 .or_else(|| Some(std::path::PathBuf::from("./logs"))),
238 None => None, };
240 crate::telemetry::TelemetryConfig {
241 log_dir,
242 file_prefix: "reddb.log".to_string(),
243 level_filter: std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()),
244 format: if std::io::IsTerminal::is_terminal(&std::io::stderr()) {
245 crate::telemetry::LogFormat::Pretty
246 } else {
247 crate::telemetry::LogFormat::Json
248 },
249 rotation_keep_days: 14,
250 service_name: "reddb",
251 level_explicit: false,
253 format_explicit: false,
254 rotation_keep_days_explicit: false,
255 file_prefix_explicit: false,
256 log_dir_explicit: false,
257 log_file_disabled: false,
258 }
259}
260
261const BACKUP_INTERVAL_META_CHECKPOINT: &str = "red.boot.backup.checkpoint_interval_secs";
268const BACKUP_INTERVAL_META_WAL_FLUSH: &str = "red.boot.backup.wal_flush_interval_secs";
269const BACKUP_KIND_META: &str = "red.boot.backup.backend_kind";
270const BACKUP_PAUSE_ON_LAG_META: &str = "red.boot.backup.pause_on_lag_secs";
274
275pub(crate) const NO_AUTH_META: &str = "red.boot.no_auth";
283
284pub(crate) fn no_auth_active(options: &RedDBOptions) -> bool {
287 options
288 .metadata
289 .get(NO_AUTH_META)
290 .map(|v| v == "true")
291 .unwrap_or(false)
292}
293
294const NO_AUTH_WARNING: &str =
299 "⚠ auth disabled (--no-auth) — anonymous access, do NOT use in production";
300
301impl ServerCommandConfig {
302 fn to_db_options(&self) -> Result<RedDBOptions, String> {
303 let mut options = match &self.path {
304 Some(path) => RedDBOptions::persistent(path),
305 None => RedDBOptions::in_memory(),
306 };
307
308 options.mode = StorageMode::Persistent;
309 options.create_if_missing = self.create_if_missing;
310 options.bounded_disk_residency = self.bounded_disk_residency;
311 options.read_only = self.read_only
318 || env_nonempty("RED_READONLY")
319 .or_else(|| env_nonempty("REDDB_READONLY"))
320 .map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on"))
321 .unwrap_or(false)
322 || self.path.as_ref().is_some_and(|data_path| {
323 crate::server::handlers_admin::load_runtime_readonly(std::path::Path::new(
324 data_path,
325 ))
326 .unwrap_or(false)
327 });
328
329 options.replication = match self.role.as_str() {
330 "primary" => ReplicationConfig::primary(),
331 "replica" => {
332 let primary_addr = self
333 .primary_addr
334 .clone()
335 .unwrap_or_else(|| "http://127.0.0.1:55055".to_string());
336 ReplicationConfig::replica(primary_addr)
343 }
344 _ => ReplicationConfig::standalone(),
345 };
346 options.storage_profile = self.storage_profile.validate()?;
347
348 let no_auth = self.no_auth || env_truthy("REDDB_NO_AUTH") || env_truthy("REDDB_DEV");
349 let preset = self.bootstrap.resolved_preset_for_options()?;
350 let preset_requires_auth = matches!(preset.as_str(), PRESET_PRODUCTION | PRESET_CLOUD);
351
352 if self.auth || env_truthy("REDDB_AUTH") || preset_requires_auth {
353 options.auth.enabled = true;
354 }
355 if self.require_auth || env_truthy("REDDB_REQUIRE_AUTH") || preset_requires_auth {
356 options.auth.enabled = true;
357 options.auth.require_auth = true;
358 }
359 if self.vault || env_truthy("REDDB_VAULT") || preset_requires_auth {
360 options.auth.vault_enabled = true;
361 }
362
363 if no_auth {
374 options.auth.enabled = false;
375 options.auth.require_auth = false;
376 options.auth.vault_enabled = false;
377 options
378 .metadata
379 .insert(NO_AUTH_META.to_string(), "true".to_string());
380 }
381
382 let bootstrap_intent_present = !matches!(
402 self.bootstrap.auth_bootstrap_input(),
403 crate::cluster::AuthBootstrapInput::None
404 );
405 let path_absent = self.path.as_ref().is_some_and(|path| !path.exists());
406 let embedded_single_file = options.storage_profile.deploy_profile
409 == crate::storage::DeployProfile::Embedded
410 && options.storage_profile.packaging == crate::storage::StoragePackaging::SingleFile;
411 if options.auth.vault_enabled
412 && bootstrap_intent_present
413 && path_absent
414 && embedded_single_file
415 {
416 if !options.bounded_disk_residency {
420 options.storage_profile.packaging =
421 crate::storage::StoragePackaging::OperationalDirectory;
422 }
423 options.create_if_missing = true;
424 tracing::info!(
425 target: "reddb::bootstrap",
426 "first-boot self-bootstrap: creating paged vault in place for a fresh database with a bootstrap intent"
427 );
428 }
429
430 if let Some(raw) = env_nonempty("REDDB_COMPLIANCE_MODE") {
435 options.control_events.compliance_mode = matches!(
436 raw.to_ascii_lowercase().as_str(),
437 "1" | "true" | "yes" | "on"
438 );
439 }
440 if preset == PRESET_REGULATED {
441 options.control_events.compliance_mode = true;
442 options.query_audit = crate::runtime::query_audit::QueryAuditConfig::regulated();
443 }
444
445 match crate::backup_bootstrap::from_env(|k| std::env::var(k).ok()) {
450 Err(msg) => {
451 return Err(format!("backup bootstrap: {msg}"));
452 }
453 Ok(Some(cfg)) => {
454 apply_backup_config(&mut options, &cfg);
455 }
456 Ok(None) => {
457 configure_remote_backend_from_env(&mut options);
458 }
459 }
460
461 if options.remote_backend.is_some()
462 || options
463 .metadata
464 .contains_key(BACKUP_INTERVAL_META_CHECKPOINT)
465 {
466 let mut selection = options.storage_profile;
467 selection.managed_backup = true;
468 options.storage_profile = selection.validate()?;
469 }
470
471 Ok(options)
472 }
473
474 pub fn enabled_transports(&self) -> Vec<ServerTransport> {
475 let mut transports = Vec::with_capacity(3);
476 if self.router_bind_addr.is_some() || self.grpc_bind_addr.is_some() {
477 transports.push(ServerTransport::Grpc);
478 }
479 if self.router_bind_addr.is_some() || self.http_bind_addr.is_some() {
480 transports.push(ServerTransport::Http);
481 }
482 if self.router_bind_addr.is_some() || self.wire_bind_addr.is_some() {
483 transports.push(ServerTransport::Wire);
484 }
485 transports
486 }
487}
488
489fn env_nonempty(name: &str) -> Option<String> {
498 if let Some(value) = crate::utils::env_with_file_fallback(name) {
499 let trimmed = value.trim();
500 if !trimmed.is_empty() {
501 return Some(trimmed.to_string());
502 }
503 return None;
504 }
505 env_nonempty_plain(name)
506}
507
508fn env_nonempty_plain(name: &str) -> Option<String> {
509 crate::utils::env_with_file_fallback(name)
510}
511
512fn env_truthy(name: &str) -> bool {
513 env_nonempty(name)
514 .map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on"))
515 .unwrap_or(false)
516}
517
518impl BootstrapConfig {
519 fn resolved_preset(&self) -> String {
520 self.preset
521 .as_deref()
522 .map(str::trim)
523 .filter(|s| !s.is_empty())
524 .map(str::to_string)
525 .or_else(|| env_nonempty(BOOTSTRAP_PRESET_ENV).or_else(|| env_nonempty(PRESET_ENV)))
526 .unwrap_or_else(|| PRESET_SIMPLE.to_string())
527 }
528
529 fn resolved_preset_for_options(&self) -> Result<String, String> {
530 if let Some(path) = self.selected_manifest() {
531 if let Some(manifest_bootstrap) =
532 crate::cli::bootstrap_manifest::bootstrap_config_from_manifest_file(&path)?
533 {
534 return Ok(manifest_bootstrap.resolved_preset());
535 }
536 }
537 Ok(self.resolved_preset())
538 }
539
540 fn selected_manifest(&self) -> Option<PathBuf> {
541 self.manifest.clone().or_else(|| {
542 env_nonempty(crate::cli::bootstrap_manifest::MANIFEST_ENV).map(PathBuf::from)
543 })
544 }
545
546 fn selected_cert_out(&self) -> Option<PathBuf> {
551 self.cert_out
552 .clone()
553 .or_else(|| env_nonempty(BOOTSTRAP_CERT_OUT_ENV).map(PathBuf::from))
554 }
555
556 fn auth_bootstrap_input(&self) -> crate::cluster::AuthBootstrapInput {
560 use crate::cluster::AuthBootstrapInput;
561 if self.selected_manifest().is_some() {
562 return AuthBootstrapInput::Manifest;
563 }
564 let preset = self.resolved_preset();
565 let preset_creates_auth = matches!(
566 preset.as_str(),
567 PRESET_PRODUCTION | PRESET_CLOUD | PRESET_REGULATED
568 );
569 let credentials_present =
574 self.production_username().is_some() && self.production_password().is_some();
575 if preset_creates_auth || credentials_present {
576 AuthBootstrapInput::Env
577 } else {
578 AuthBootstrapInput::None
579 }
580 }
581
582 fn explicit_value(value: &Option<String>) -> Option<String> {
583 value
584 .as_deref()
585 .map(str::trim)
586 .filter(|s| !s.is_empty())
587 .map(str::to_string)
588 }
589
590 fn value_or_env(value: &Option<String>, env: &str) -> Option<String> {
591 Self::explicit_value(value).or_else(|| env_nonempty(env))
592 }
593
594 fn production_username(&self) -> Option<String> {
595 Self::value_or_env(&self.admin_username, "REDDB_USERNAME")
596 }
597
598 fn production_password(&self) -> Option<String> {
599 Self::value_or_env(&self.admin_password, "REDDB_PASSWORD")
600 }
601
602 fn cloud_head_username(&self) -> Option<String> {
603 Self::explicit_value(&self.cloud_head_admin)
604 .or_else(|| Self::explicit_value(&self.admin_username))
605 .or_else(|| env_nonempty("REDDB_CLOUD_HEAD_ADMIN"))
606 .or_else(|| env_nonempty("REDDB_USERNAME"))
607 }
608
609 fn cloud_head_password(&self) -> Option<String> {
610 Self::explicit_value(&self.cloud_head_admin_password)
611 .or_else(|| Self::explicit_value(&self.admin_password))
612 .or_else(|| env_nonempty("REDDB_CLOUD_HEAD_ADMIN_PASSWORD"))
613 .or_else(|| env_nonempty("REDDB_PASSWORD"))
614 }
615
616 fn customer_username(&self) -> Option<String> {
617 Self::value_or_env(&self.customer_admin, "REDDB_CUSTOMER_ADMIN")
618 }
619
620 fn customer_password(&self) -> Option<String> {
621 Self::value_or_env(
622 &self.customer_admin_password,
623 "REDDB_CUSTOMER_ADMIN_PASSWORD",
624 )
625 }
626}
627
628fn apply_backup_config(options: &mut RedDBOptions, cfg: &crate::backup_bootstrap::BackupConfig) {
634 let endpoint_host = endpoint_host(&cfg.endpoint);
635
636 options.metadata.insert(
637 BACKUP_INTERVAL_META_CHECKPOINT.to_string(),
638 cfg.checkpoint_interval_secs.to_string(),
639 );
640 options.metadata.insert(
641 BACKUP_INTERVAL_META_WAL_FLUSH.to_string(),
642 cfg.wal_flush_interval_secs.to_string(),
643 );
644 options
645 .metadata
646 .insert(BACKUP_KIND_META.to_string(), "s3".to_string());
647 options.metadata.insert(
648 BACKUP_PAUSE_ON_LAG_META.to_string(),
649 cfg.pause_on_lag_secs.to_string(),
650 );
651
652 #[cfg(feature = "backend-s3")]
653 {
654 let s3_cfg = crate::storage::backend::S3Config {
655 endpoint: cfg.endpoint.clone(),
656 bucket: cfg.bucket.clone(),
657 key_prefix: cfg.prefix.clone(),
658 access_key: cfg.access_key_id.clone(),
659 secret_key: cfg.secret_access_key.clone(),
660 region: cfg.region.clone(),
661 path_style: true,
662 };
663 let mut backend = crate::storage::backend::S3Backend::new(s3_cfg);
664 if let Some(path) = &cfg.credentials_file {
665 backend = backend.with_credentials_file(path);
666 }
667 let backend = Arc::new(backend);
668 options.remote_backend = Some(backend.clone());
669 options.remote_backend_atomic = Some(backend);
670 let trimmed = cfg.prefix.trim_end_matches('/');
675 options.remote_key = Some(reddb_file::remote_database_key(trimmed));
676
677 tracing::info!(
678 backend = "s3",
679 endpoint = %endpoint_host,
680 bucket = %cfg.bucket,
681 prefix = %cfg.prefix,
682 checkpoint_interval_secs = cfg.checkpoint_interval_secs,
683 wal_flush_interval_secs = cfg.wal_flush_interval_secs,
684 "backup backend configured from REDDB_BACKUP_* env"
685 );
686 }
687
688 #[cfg(not(feature = "backend-s3"))]
689 {
690 tracing::warn!(
691 backend = "s3",
692 endpoint = %endpoint_host,
693 bucket = %cfg.bucket,
694 prefix = %cfg.prefix,
695 "REDDB_BACKUP_S3_* configured but binary built without `backend-s3` feature; \
696 backend wiring skipped (archiver/checkpointer also disabled)"
697 );
698 }
699}
700
701fn endpoint_host(endpoint: &str) -> &str {
702 let after_scheme = endpoint
703 .split_once("://")
704 .map(|(_, r)| r)
705 .unwrap_or(endpoint);
706 after_scheme.split('/').next().unwrap_or(after_scheme)
707}
708
709fn spawn_backup_tasks_if_configured(
715 options: &RedDBOptions,
716 runtime: &RedDBRuntime,
717) -> Option<BackupTasksHandle> {
718 let checkpoint_secs: u64 = options
719 .metadata
720 .get(BACKUP_INTERVAL_META_CHECKPOINT)?
721 .parse()
722 .ok()?;
723 let wal_secs: u64 = options
724 .metadata
725 .get(BACKUP_INTERVAL_META_WAL_FLUSH)?
726 .parse()
727 .ok()?;
728 let pause_on_lag_secs: u64 = options
731 .metadata
732 .get(BACKUP_PAUSE_ON_LAG_META)
733 .and_then(|raw| raw.parse().ok())
734 .unwrap_or(0);
735 options.remote_backend.as_ref()?;
736
737 let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
738
739 if pause_on_lag_secs > 0 {
744 let now_ms = std::time::SystemTime::now()
745 .duration_since(std::time::UNIX_EPOCH)
746 .map(|d| d.as_millis() as u64)
747 .unwrap_or(0);
748 runtime
749 .write_gate()
750 .configure_archive_lag_pause(pause_on_lag_secs, now_ms);
751 tracing::info!(
752 pause_on_lag_secs,
753 "archive-lag pause enabled — engine will transition to read-only after threshold seconds of archiver silence"
754 );
755 }
756
757 let checkpoint_handle = {
758 let stop = Arc::clone(&stop);
759 let runtime = runtime.clone();
760 let interval = Duration::from_secs(checkpoint_secs);
761 thread::Builder::new()
762 .name("red-checkpointer".into())
763 .spawn(move || {
764 periodic_loop(stop, interval, move || {
765 if let Err(err) = runtime.checkpoint() {
766 tracing::warn!(error = %err, "periodic checkpoint failed");
767 }
768 })
769 })
770 .ok()
771 };
772
773 let archiver_handle = {
774 let stop = Arc::clone(&stop);
775 let runtime = runtime.clone();
776 let interval = Duration::from_secs(wal_secs);
777 let lag_enabled = pause_on_lag_secs > 0;
778 thread::Builder::new()
779 .name("red-wal-archiver".into())
780 .spawn(move || {
781 periodic_loop(stop, interval, move || match runtime.trigger_backup() {
782 Ok(_) if lag_enabled => {
783 let now_ms = std::time::SystemTime::now()
784 .duration_since(std::time::UNIX_EPOCH)
785 .map(|d| d.as_millis() as u64)
786 .unwrap_or(0);
787 runtime.write_gate().record_archive_success(now_ms);
788 runtime.write_gate().evaluate_archive_lag(now_ms);
792 }
793 Ok(_) => {}
794 Err(err) => {
795 tracing::warn!(error = %err, "periodic WAL archive/backup failed");
796 }
797 })
798 })
799 .ok()
800 };
801
802 let lag_monitor_handle = if pause_on_lag_secs > 0 {
807 let stop = Arc::clone(&stop);
808 let runtime = runtime.clone();
809 let interval = Duration::from_secs(5);
813 thread::Builder::new()
814 .name("red-archive-lag-monitor".into())
815 .spawn(move || {
816 periodic_loop(stop, interval, move || {
817 let now_ms = std::time::SystemTime::now()
818 .duration_since(std::time::UNIX_EPOCH)
819 .map(|d| d.as_millis() as u64)
820 .unwrap_or(0);
821 let was_paused = runtime.write_gate().is_auto_paused();
822 let now_paused = runtime.write_gate().evaluate_archive_lag(now_ms);
823 if now_paused && !was_paused {
824 tracing::warn!(
825 pause_on_lag_secs,
826 last_archive_at_ms = runtime.write_gate().last_archive_at_ms(),
827 "WAL archive lag exceeded threshold — entering graceful read-only mode (issue #519)"
828 );
829 } else if !now_paused && was_paused {
830 tracing::info!(
831 "WAL archive caught up — exiting graceful read-only mode (issue #519)"
832 );
833 }
834 })
835 })
836 .ok()
837 } else {
838 None
839 };
840
841 tracing::info!(
842 checkpoint_interval_secs = checkpoint_secs,
843 wal_flush_interval_secs = wal_secs,
844 "backup tasks spawned (checkpointer + WAL archiver)"
845 );
846
847 Some(BackupTasksHandle {
848 stop,
849 _checkpoint_handle: checkpoint_handle,
850 _archiver_handle: archiver_handle,
851 _lag_monitor_handle: lag_monitor_handle,
852 })
853}
854
855pub struct BackupTasksHandle {
858 stop: Arc<std::sync::atomic::AtomicBool>,
859 _checkpoint_handle: Option<thread::JoinHandle<()>>,
860 _archiver_handle: Option<thread::JoinHandle<()>>,
861 _lag_monitor_handle: Option<thread::JoinHandle<()>>,
864}
865
866impl Drop for BackupTasksHandle {
867 fn drop(&mut self) {
868 self.stop.store(true, std::sync::atomic::Ordering::Release);
869 }
870}
871
872fn periodic_loop<F: FnMut()>(
873 stop: Arc<std::sync::atomic::AtomicBool>,
874 interval: Duration,
875 mut tick: F,
876) {
877 let wake = Duration::from_secs(1);
880 let mut elapsed = Duration::ZERO;
881 while !stop.load(std::sync::atomic::Ordering::Acquire) {
882 thread::sleep(wake);
883 elapsed += wake;
884 if elapsed >= interval {
885 tick();
886 elapsed = Duration::ZERO;
887 }
888 }
889}
890
891fn configure_remote_backend_from_env(options: &mut RedDBOptions) {
892 let backend = env_nonempty("RED_BACKEND")
898 .or_else(|| env_nonempty("REDDB_REMOTE_BACKEND"))
899 .unwrap_or_else(|| "none".to_string())
900 .to_ascii_lowercase();
901
902 match backend.as_str() {
903 "s3" | "minio" | "r2" => {
908 #[cfg(feature = "backend-s3")]
909 {
910 if let Some(config) = s3_config_from_env() {
911 let remote_key = env_nonempty("RED_REMOTE_KEY")
912 .or_else(|| env_nonempty("REDDB_REMOTE_KEY"))
913 .unwrap_or_else(|| reddb_file::remote_database_key("clusters/dev"));
914 let backend = Arc::new(crate::storage::backend::S3Backend::new(config));
915 options.remote_backend = Some(backend.clone());
916 options.remote_backend_atomic = Some(backend);
917 options.remote_key = Some(remote_key);
918 }
919 }
920 #[cfg(not(feature = "backend-s3"))]
921 {
922 tracing::warn!(
923 backend = %backend,
924 "RED_BACKEND={backend} requested but binary was built without `backend-s3` feature"
925 );
926 }
927 }
928 "fs" | "local" => {
933 let base_path = env_nonempty("RED_FS_PATH").or_else(|| env_nonempty("REDDB_FS_PATH"));
934 let remote_key = match (
935 base_path,
936 env_nonempty("RED_REMOTE_KEY").or_else(|| env_nonempty("REDDB_REMOTE_KEY")),
937 ) {
938 (Some(base), Some(rel)) => Some(format!(
939 "{}/{}",
940 base.trim_end_matches('/'),
941 rel.trim_start_matches('/')
942 )),
943 (Some(base), None) => Some(reddb_file::remote_database_key(&format!(
944 "{}/clusters/dev",
945 base.trim_end_matches('/')
946 ))),
947 (None, Some(rel)) => Some(rel),
948 (None, None) => None,
949 };
950 if let Some(key) = remote_key {
951 let backend = Arc::new(crate::storage::backend::LocalBackend);
952 options.remote_backend = Some(backend.clone());
953 options.remote_backend_atomic = Some(backend);
954 options.remote_key = Some(key);
955 }
956 }
957 "http" => {
962 let base_url = match env_nonempty("RED_HTTP_BACKEND_URL")
963 .or_else(|| env_nonempty("REDDB_HTTP_BACKEND_URL"))
964 {
965 Some(u) => u,
966 None => {
967 tracing::warn!(
968 "RED_BACKEND=http requires RED_HTTP_BACKEND_URL — backend disabled"
969 );
970 return;
971 }
972 };
973 let prefix = env_nonempty("RED_HTTP_BACKEND_PREFIX")
974 .or_else(|| env_nonempty("REDDB_HTTP_BACKEND_PREFIX"))
975 .unwrap_or_default();
976 let auth_header = if let Some(path) = env_nonempty("RED_HTTP_BACKEND_AUTH_HEADER_FILE")
977 .or_else(|| env_nonempty("REDDB_HTTP_BACKEND_AUTH_HEADER_FILE"))
978 {
979 std::fs::read_to_string(&path)
980 .ok()
981 .map(|s| s.trim().to_string())
982 .filter(|s| !s.is_empty())
983 } else {
984 env_nonempty("RED_HTTP_BACKEND_AUTH_HEADER")
985 .or_else(|| env_nonempty("REDDB_HTTP_BACKEND_AUTH_HEADER"))
986 };
987
988 let mut config =
989 crate::storage::backend::HttpBackendConfig::new(base_url).with_prefix(prefix);
990 if let Some(auth) = auth_header {
991 config = config.with_auth_header(auth);
992 }
993 let conditional_writes = env_truthy("RED_HTTP_CONDITIONAL_WRITES")
994 || env_truthy("RED_HTTP_BACKEND_CONDITIONAL_WRITES")
995 || env_truthy("REDDB_HTTP_BACKEND_CONDITIONAL_WRITES");
996 config = config.with_conditional_writes(conditional_writes);
997 if conditional_writes {
1002 match crate::storage::backend::AtomicHttpBackend::try_new(config.clone()) {
1003 Ok(atomic) => {
1004 let atomic_arc = Arc::new(atomic);
1005 options.remote_backend = Some(atomic_arc.clone());
1006 options.remote_backend_atomic = Some(atomic_arc);
1007 }
1008 Err(err) => {
1009 tracing::warn!(error = %err, "AtomicHttpBackend init failed; falling back to plain HTTP (no CAS)");
1010 options.remote_backend =
1011 Some(Arc::new(crate::storage::backend::HttpBackend::new(config)));
1012 }
1013 }
1014 } else {
1015 options.remote_backend =
1016 Some(Arc::new(crate::storage::backend::HttpBackend::new(config)));
1017 }
1018 options.remote_key = env_nonempty("RED_REMOTE_KEY")
1019 .or_else(|| env_nonempty("REDDB_REMOTE_KEY"))
1020 .or_else(|| Some(reddb_file::remote_database_key("clusters/dev")));
1021 }
1022 "none" | "" => {}
1025 other => {
1026 tracing::warn!(
1027 backend = %other,
1028 "unknown RED_BACKEND value — supported: s3 | fs | http | none"
1029 );
1030 }
1031 }
1032}
1033
1034#[cfg(feature = "backend-s3")]
1039fn env_s3(suffix: &str) -> Option<String> {
1040 env_nonempty(&format!("RED_S3_{suffix}"))
1041 .or_else(|| env_nonempty(&format!("REDDB_S3_{suffix}")))
1042}
1043
1044#[cfg(feature = "backend-s3")]
1050fn env_s3_secret(suffix: &str) -> Option<String> {
1051 let file_key_red = format!("RED_S3_{suffix}_FILE");
1052 let file_key_legacy = format!("REDDB_S3_{suffix}_FILE");
1053 if let Some(path) = env_nonempty(&file_key_red).or_else(|| env_nonempty(&file_key_legacy)) {
1054 return std::fs::read_to_string(&path)
1055 .ok()
1056 .map(|s| s.trim().to_string())
1057 .filter(|s| !s.is_empty());
1058 }
1059 env_s3(suffix)
1060}
1061
1062#[cfg(feature = "backend-s3")]
1063fn s3_config_from_env() -> Option<crate::storage::backend::S3Config> {
1064 let endpoint = env_s3("ENDPOINT")?;
1065 let bucket = env_s3("BUCKET")?;
1066 let access_key = env_s3_secret("ACCESS_KEY")?;
1067 let secret_key = env_s3_secret("SECRET_KEY")?;
1068 let region = env_s3("REGION").unwrap_or_else(|| "us-east-1".to_string());
1069 let key_prefix = env_s3("KEY_PREFIX")
1070 .or_else(|| env_s3("PREFIX"))
1071 .unwrap_or_default();
1072 let path_style = env_s3("PATH_STYLE")
1073 .map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on"))
1074 .unwrap_or(true);
1075 Some(crate::storage::backend::S3Config {
1076 endpoint,
1077 bucket,
1078 key_prefix,
1079 access_key,
1080 secret_key,
1081 region,
1082 path_style,
1083 })
1084}
1085
1086pub fn render_systemd_unit(config: &SystemdServiceConfig) -> String {
1087 let data_dir = config.data_dir();
1088 let exec_start = render_systemd_exec_start(config);
1089 format!(
1090 "[Unit]\n\
1091Description=RedDB unified database service\n\
1092After=network-online.target\n\
1093Wants=network-online.target\n\
1094\n\
1095[Service]\n\
1096Type=simple\n\
1097User={user}\n\
1098Group={group}\n\
1099WorkingDirectory={workdir}\n\
1100ExecStart={exec_start}\n\
1101Restart=always\n\
1102RestartSec=2\n\
1103LimitSTACK=16M\n\
1104NoNewPrivileges=true\n\
1105PrivateTmp=true\n\
1106ProtectSystem=strict\n\
1107ProtectHome=true\n\
1108ProtectControlGroups=true\n\
1109ProtectKernelTunables=true\n\
1110ProtectKernelModules=true\n\
1111RestrictNamespaces=true\n\
1112LockPersonality=true\n\
1113MemoryDenyWriteExecute=true\n\
1114ReadWritePaths={workdir}\n\
1115\n\
1116[Install]\n\
1117WantedBy=multi-user.target\n",
1118 user = config.run_user,
1119 group = config.run_group,
1120 workdir = data_dir.display(),
1121 exec_start = exec_start,
1122 )
1123}
1124
1125#[cfg(target_os = "linux")]
1134pub fn install_systemd_service(config: &SystemdServiceConfig) -> Result<(), String> {
1135 ensure_root()?;
1136 ensure_command_available("systemctl")?;
1137 ensure_command_available("getent")?;
1138 ensure_command_available("groupadd")?;
1139 ensure_command_available("useradd")?;
1140 ensure_command_available("install")?;
1141 ensure_executable(&config.binary_path)?;
1142
1143 if !command_success("getent", ["group", config.run_group.as_str()])? {
1144 run_command("groupadd", ["--system", config.run_group.as_str()])?;
1145 }
1146
1147 if !command_success("id", ["-u", config.run_user.as_str()])? {
1148 let data_dir = config.data_dir();
1149 run_command(
1150 "useradd",
1151 [
1152 "--system",
1153 "--gid",
1154 config.run_group.as_str(),
1155 "--home-dir",
1156 data_dir.to_string_lossy().as_ref(),
1157 "--shell",
1158 "/usr/sbin/nologin",
1159 config.run_user.as_str(),
1160 ],
1161 )?;
1162 }
1163
1164 let data_dir = config.data_dir();
1165 run_command(
1166 "install",
1167 [
1168 "-d",
1169 "-o",
1170 config.run_user.as_str(),
1171 "-g",
1172 config.run_group.as_str(),
1173 "-m",
1174 "0750",
1175 data_dir.to_string_lossy().as_ref(),
1176 ],
1177 )?;
1178
1179 std::fs::write(config.unit_path(), render_systemd_unit(config))
1180 .map_err(|err| format!("failed to write systemd unit: {err}"))?;
1181
1182 run_command("systemctl", ["daemon-reload"])?;
1183 run_command(
1184 "systemctl",
1185 [
1186 "enable",
1187 "--now",
1188 format!("{}.service", config.service_name).as_str(),
1189 ],
1190 )?;
1191
1192 Ok(())
1193}
1194
1195#[cfg(not(target_os = "linux"))]
1200pub fn install_systemd_service(_config: &SystemdServiceConfig) -> Result<(), String> {
1201 Err("systemd install is Linux-only — use sc.exe (Windows) or \
1202 launchd (macOS) to install the service manually using the \
1203 unit printed by `red service print-unit`"
1204 .to_string())
1205}
1206
1207#[cfg(target_os = "linux")]
1208fn ensure_root() -> Result<(), String> {
1209 let output = Command::new("id")
1210 .arg("-u")
1211 .output()
1212 .map_err(|err| format!("failed to determine current uid: {err}"))?;
1213 if !output.status.success() {
1214 return Err("failed to determine current uid".to_string());
1215 }
1216 let uid = String::from_utf8_lossy(&output.stdout);
1217 if uid.trim() != "0" {
1218 return Err("run this command as root (sudo)".to_string());
1219 }
1220 Ok(())
1221}
1222
1223#[cfg(target_os = "linux")]
1224fn ensure_command_available(command: &str) -> Result<(), String> {
1225 let status = Command::new("sh")
1226 .args(["-lc", &format!("command -v {command} >/dev/null 2>&1")])
1227 .status()
1228 .map_err(|err| format!("failed to check command '{command}': {err}"))?;
1229 if status.success() {
1230 Ok(())
1231 } else {
1232 Err(format!("required command not found: {command}"))
1233 }
1234}
1235
1236#[cfg(target_os = "linux")]
1237fn ensure_executable(path: &std::path::Path) -> Result<(), String> {
1238 let metadata = std::fs::metadata(path)
1239 .map_err(|err| format!("binary not found '{}': {err}", path.display()))?;
1240 #[cfg(unix)]
1241 {
1242 use std::os::unix::fs::PermissionsExt;
1243 if metadata.permissions().mode() & 0o111 == 0 {
1244 return Err(format!("binary is not executable: {}", path.display()));
1245 }
1246 }
1247 #[cfg(not(unix))]
1248 {
1249 if !metadata.is_file() {
1250 return Err(format!("binary is not a file: {}", path.display()));
1251 }
1252 }
1253 Ok(())
1254}
1255
1256#[cfg(target_os = "linux")]
1257fn command_success<const N: usize>(program: &str, args: [&str; N]) -> Result<bool, String> {
1258 Command::new(program)
1259 .args(args)
1260 .status()
1261 .map(|status| status.success())
1262 .map_err(|err| format!("failed to run {program}: {err}"))
1263}
1264
1265#[cfg(target_os = "linux")]
1266fn run_command<const N: usize>(program: &str, args: [&str; N]) -> Result<(), String> {
1267 let output = Command::new(program)
1268 .args(args)
1269 .output()
1270 .map_err(|err| format!("failed to run {program}: {err}"))?;
1271 if output.status.success() {
1272 return Ok(());
1273 }
1274
1275 let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
1276 let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
1277 let detail = if !stderr.is_empty() {
1278 stderr
1279 } else if !stdout.is_empty() {
1280 stdout
1281 } else {
1282 format!("exit status {}", output.status)
1283 };
1284 Err(format!("{program} failed: {detail}"))
1285}
1286
1287fn render_systemd_exec_start(config: &SystemdServiceConfig) -> String {
1288 let mut parts = vec![
1289 config.binary_path.display().to_string(),
1290 "server".to_string(),
1291 "--path".to_string(),
1292 config.data_path.display().to_string(),
1293 ];
1294
1295 if let Some(bind_addr) = &config.router_bind_addr {
1296 parts.push("--bind".to_string());
1297 parts.push(bind_addr.clone());
1298 } else if let Some(bind_addr) = &config.grpc_bind_addr {
1299 parts.push("--grpc-bind".to_string());
1300 parts.push(bind_addr.clone());
1301 }
1302 if let Some(bind_addr) = &config.http_bind_addr {
1303 parts.push("--http-bind".to_string());
1304 parts.push(bind_addr.clone());
1305 }
1306
1307 parts.join(" ")
1308}
1309
1310pub fn probe_listener(target: &str, timeout: Duration) -> bool {
1311 let addresses: Vec<SocketAddr> = match target.to_socket_addrs() {
1312 Ok(addresses) => addresses.collect(),
1313 Err(_) => return false,
1314 };
1315
1316 addresses
1317 .into_iter()
1318 .any(|address| TcpStream::connect_timeout(&address, timeout).is_ok())
1319}
1320
1321pub fn bind_listener_for_startup(
1339 readiness: &mut TransportReadiness,
1340 transport: &str,
1341 bind_addr: &str,
1342 explicit: bool,
1343) -> Result<Option<TcpListener>, String> {
1344 match TcpListener::bind(bind_addr) {
1345 Ok(listener) => {
1346 readiness.active(transport, bind_addr, explicit);
1347 Ok(Some(listener))
1348 }
1349 Err(err) => {
1350 let reason = format!("{transport} listener bind {bind_addr}: {err}");
1351 readiness.failed(transport, bind_addr, explicit, reason.clone());
1352 if explicit {
1353 tracing::error!(
1354 transport,
1355 bind = %bind_addr,
1356 error = %err,
1357 "fatal explicit bind failure"
1358 );
1359 Err(format!("explicit {reason}"))
1360 } else {
1361 tracing::warn!(
1362 transport,
1363 bind = %bind_addr,
1364 error = %err,
1365 "non-fatal implicit bind failure; listener degraded"
1366 );
1367 Ok(None)
1368 }
1369 }
1370 }
1371}
1372
1373async fn spawn_lifecycle_signal_handler(runtime: RedDBRuntime) {
1396 let backup_on_shutdown = std::env::var("RED_BACKUP_ON_SHUTDOWN")
1397 .ok()
1398 .map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on"))
1399 .unwrap_or(true);
1400
1401 #[cfg(unix)]
1402 {
1403 use tokio::signal::unix::{signal, SignalKind};
1404
1405 let mut sigterm = match signal(SignalKind::terminate()) {
1406 Ok(s) => s,
1407 Err(err) => {
1408 tracing::warn!(
1409 error = %err,
1410 "could not install SIGTERM handler; orchestrator graceful shutdown will fall back to SIGKILL"
1411 );
1412 return;
1413 }
1414 };
1415 let mut sigint = match signal(SignalKind::interrupt()) {
1416 Ok(s) => s,
1417 Err(err) => {
1418 tracing::warn!(error = %err, "could not install SIGINT handler");
1419 return;
1420 }
1421 };
1422 let mut sighup = match signal(SignalKind::hangup()) {
1428 Ok(s) => Some(s),
1429 Err(err) => {
1430 tracing::warn!(error = %err, "could not install SIGHUP handler; secret reload via signal disabled");
1431 None
1432 }
1433 };
1434
1435 let reload_runtime = runtime.clone();
1436 tokio::spawn(async move {
1437 loop {
1438 let signal_name = match &mut sighup {
1439 Some(hup) => tokio::select! {
1440 _ = sigterm.recv() => "SIGTERM",
1441 _ = sigint.recv() => "SIGINT",
1442 _ = hup.recv() => "SIGHUP",
1443 },
1444 None => tokio::select! {
1445 _ = sigterm.recv() => "SIGTERM",
1446 _ = sigint.recv() => "SIGINT",
1447 },
1448 };
1449
1450 if signal_name == "SIGHUP" {
1451 handle_sighup_reload(&reload_runtime);
1452 continue; }
1454
1455 tracing::info!(
1456 signal = signal_name,
1457 "lifecycle signal received; shutting down"
1458 );
1459 match runtime.graceful_shutdown(backup_on_shutdown) {
1460 Ok(report) => {
1461 tracing::info!(
1462 duration_ms = report.duration_ms,
1463 flushed_wal = report.flushed_wal,
1464 final_checkpoint = report.final_checkpoint,
1465 backup_uploaded = report.backup_uploaded,
1466 "graceful shutdown complete"
1467 );
1468 }
1469 Err(err) => {
1470 tracing::error!(error = %err, "graceful shutdown failed");
1471 crate::telemetry::operator_event::OperatorEvent::ShutdownForced {
1477 reason: format!("graceful shutdown failed: {err}"),
1478 }
1479 .emit_global();
1480 }
1481 }
1482 std::process::exit(0);
1483 }
1484 });
1485 }
1486
1487 #[cfg(not(unix))]
1488 {
1489 tokio::spawn(async move {
1490 let interrupted = tokio::signal::ctrl_c().await;
1491 if let Err(err) = interrupted {
1492 tracing::warn!(error = %err, "could not install Ctrl+C handler");
1493 return;
1494 }
1495
1496 tracing::info!(
1497 signal = "Ctrl+C",
1498 "lifecycle signal received; shutting down"
1499 );
1500 match runtime.graceful_shutdown(backup_on_shutdown) {
1501 Ok(report) => {
1502 tracing::info!(
1503 duration_ms = report.duration_ms,
1504 flushed_wal = report.flushed_wal,
1505 final_checkpoint = report.final_checkpoint,
1506 backup_uploaded = report.backup_uploaded,
1507 "graceful shutdown complete"
1508 );
1509 }
1510 Err(err) => {
1511 tracing::error!(error = %err, "graceful shutdown failed");
1512 }
1513 }
1514 std::process::exit(0);
1515 });
1516 }
1517}
1518
1519fn handle_sighup_reload(runtime: &RedDBRuntime) {
1528 let now_ms = std::time::SystemTime::now()
1529 .duration_since(std::time::UNIX_EPOCH)
1530 .map(|d| d.as_millis() as u64)
1531 .unwrap_or(0);
1532 tracing::info!(
1533 target: "reddb::secrets",
1534 ts_unix_ms = now_ms,
1535 "SIGHUP received; secrets will be re-read from *_FILE on next access"
1536 );
1537 use crate::runtime::audit_log::{AuditAuthSource, AuditEvent, AuditFieldEscaper, Outcome};
1542 runtime.audit_log().record_event(
1543 AuditEvent::builder("config/sighup_reload")
1544 .source(AuditAuthSource::System)
1545 .resource("secrets")
1546 .outcome(Outcome::Success)
1547 .field(AuditFieldEscaper::field("ts_unix_ms", now_ms))
1548 .build(),
1549 );
1550}
1551
1552fn resolve_pg_wire_tls(config: &ServerCommandConfig) -> Option<crate::wire::WireTlsConfig> {
1563 let tls_configured = config.wire_tls_bind_addr.is_some()
1564 || (config.wire_tls_cert.is_some() && config.wire_tls_key.is_some());
1565 if !tls_configured {
1566 return None;
1567 }
1568 match resolve_wire_tls_config(config) {
1569 Ok(cfg) => Some(cfg),
1570 Err(e) => {
1571 tracing::error!(
1572 err = %e,
1573 "pg wire TLS config error; listener will run plaintext"
1574 );
1575 None
1576 }
1577 }
1578}
1579
1580fn resolve_grpc_tls_options(config: &ServerCommandConfig) -> Result<crate::GrpcTlsOptions, String> {
1594 let (cert_pem, key_pem) = match (&config.grpc_tls_cert, &config.grpc_tls_key) {
1595 (Some(cert), Some(key)) => {
1596 let cert_pem = std::fs::read(cert)
1597 .map_err(|e| format!("read grpc cert {}: {e}", cert.display()))?;
1598 let key_pem =
1599 std::fs::read(key).map_err(|e| format!("read grpc key {}: {e}", key.display()))?;
1600 (cert_pem, key_pem)
1601 }
1602 _ => {
1603 let dev = std::env::var("RED_GRPC_TLS_DEV")
1605 .ok()
1606 .map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on"))
1607 .unwrap_or(false);
1608 if !dev {
1609 return Err("gRPC TLS configured but no cert/key supplied — set \
1610 REDDB_GRPC_TLS_CERT / REDDB_GRPC_TLS_KEY (or \
1611 RED_GRPC_TLS_DEV=1 to auto-generate a self-signed cert)"
1612 .to_string());
1613 }
1614 let dir = config
1615 .path
1616 .as_ref()
1617 .and_then(|p| p.parent())
1618 .map(PathBuf::from)
1619 .unwrap_or_else(|| PathBuf::from("."));
1620 let (cert_pem_str, key_pem_str) =
1621 crate::wire::tls::generate_self_signed_cert("localhost")
1622 .map_err(|e| format!("auto-generate dev grpc cert: {e}"))?;
1623
1624 let fp = sha256_pem_fingerprint(cert_pem_str.as_bytes());
1629 tracing::warn!(
1630 target: "reddb::security",
1631 transport = "grpc",
1632 cert_sha256 = %fp,
1633 "RED_GRPC_TLS_DEV=1: using auto-generated self-signed cert; \
1634 DO NOT use in production"
1635 );
1636 let cert_path = dir.join("grpc-tls-cert.pem");
1638 let key_path = dir.join("grpc-tls-key.pem");
1639 if !cert_path.exists() || !key_path.exists() {
1640 let _ = std::fs::create_dir_all(&dir);
1641 std::fs::write(&cert_path, cert_pem_str.as_bytes())
1642 .map_err(|e| format!("write grpc dev cert: {e}"))?;
1643 std::fs::write(&key_path, key_pem_str.as_bytes())
1644 .map_err(|e| format!("write grpc dev key: {e}"))?;
1645 #[cfg(unix)]
1646 {
1647 use std::os::unix::fs::PermissionsExt;
1648 let _ =
1649 std::fs::set_permissions(&key_path, std::fs::Permissions::from_mode(0o600));
1650 }
1651 }
1652 (cert_pem_str.into_bytes(), key_pem_str.into_bytes())
1653 }
1654 };
1655
1656 let client_ca_pem = match &config.grpc_tls_client_ca {
1657 Some(path) => Some(
1658 std::fs::read(path)
1659 .map_err(|e| format!("read grpc client CA {}: {e}", path.display()))?,
1660 ),
1661 None => None,
1662 };
1663
1664 Ok(crate::GrpcTlsOptions {
1665 cert_pem,
1666 key_pem,
1667 client_ca_pem,
1668 })
1669}
1670
1671fn sha256_pem_fingerprint(pem: &[u8]) -> String {
1674 use sha2::{Digest, Sha256};
1675 let mut h = Sha256::new();
1676 h.update(pem);
1677 let d = h.finalize();
1678 let mut buf = String::with_capacity(64);
1679 for b in d.iter() {
1680 buf.push_str(&format!("{b:02x}"));
1681 }
1682 buf
1683}
1684
1685fn resolve_wire_tls_config(
1687 config: &ServerCommandConfig,
1688) -> Result<crate::wire::WireTlsConfig, String> {
1689 match (&config.wire_tls_cert, &config.wire_tls_key) {
1690 (Some(cert), Some(key)) => Ok(crate::wire::WireTlsConfig {
1691 cert_path: cert.clone(),
1692 key_path: key.clone(),
1693 }),
1694 _ => {
1695 let dir = config
1697 .path
1698 .as_ref()
1699 .and_then(|p| p.parent())
1700 .map(PathBuf::from)
1701 .unwrap_or_else(|| PathBuf::from("."));
1702 crate::wire::tls::auto_generate_cert(&dir).map_err(|e| e.to_string())
1703 }
1704 }
1705}
1706
1707#[inline(never)]
1708fn build_runtime_and_auth_store(
1709 config: &ServerCommandConfig,
1710 db_options: RedDBOptions,
1711) -> Result<
1712 (
1713 RedDBRuntime,
1714 Arc<AuthStore>,
1715 Option<crate::telemetry::TelemetryGuard>,
1716 ),
1717 String,
1718> {
1719 build_runtime_with_bootstrap(db_options, config.telemetry.clone(), &config.bootstrap)
1726}
1727
1728pub(crate) fn build_runtime_with_telemetry(
1738 db_options: RedDBOptions,
1739 cli_telemetry: Option<crate::telemetry::TelemetryConfig>,
1740) -> Result<
1741 (
1742 RedDBRuntime,
1743 Arc<AuthStore>,
1744 Option<crate::telemetry::TelemetryGuard>,
1745 ),
1746 String,
1747> {
1748 let bootstrap = BootstrapConfig::default();
1749 build_runtime_with_bootstrap(db_options, cli_telemetry, &bootstrap)
1750}
1751
1752fn build_runtime_with_bootstrap(
1753 db_options: RedDBOptions,
1754 cli_telemetry: Option<crate::telemetry::TelemetryConfig>,
1755 bootstrap: &BootstrapConfig,
1756) -> Result<
1757 (
1758 RedDBRuntime,
1759 Arc<AuthStore>,
1760 Option<crate::telemetry::TelemetryGuard>,
1761 ),
1762 String,
1763> {
1764 let runtime = RedDBRuntime::with_options(db_options.clone()).map_err(|err| {
1765 let msg = err.to_string();
1771 crate::telemetry::operator_event::OperatorEvent::StartupFailed {
1772 phase: "runtime_construction".to_string(),
1773 error: msg.clone(),
1774 }
1775 .emit_global();
1776 msg
1777 })?;
1778
1779 crate::runtime::lease_loop::start_lease_loop_if_required(&runtime).map_err(|err| {
1784 let msg = err.to_string();
1785 crate::telemetry::operator_event::OperatorEvent::StartupFailed {
1786 phase: "lease_loop".to_string(),
1787 error: msg.clone(),
1788 }
1789 .emit_global();
1790 msg
1791 })?;
1792
1793 if let Some(data_path) = db_options.data_path.as_deref() {
1797 let watch_dir = data_path.parent().unwrap_or(data_path);
1798 crate::runtime::disk_space_monitor::DiskSpaceMonitor::new(watch_dir, 90).spawn();
1799 }
1800
1801 {
1805 let config_path = crate::runtime::config_overlay::config_file_path();
1806 let store = runtime.db().store();
1807 crate::runtime::config_watcher::ConfigWatcher::new(config_path, store).spawn();
1808 }
1809
1810 let merged = merge_telemetry_with_config(
1813 cli_telemetry
1814 .unwrap_or_else(|| default_telemetry_for_path(db_options.data_path.as_deref())),
1815 &runtime,
1816 );
1817 let telemetry_guard = crate::telemetry::init(merged);
1818 if db_options.bounded_disk_residency {
1819 tracing::info!(
1820 target: "reddb::storage::residency",
1821 resident_memory_bytes_max = runtime.db().store().config().resident_memory_bytes_max,
1822 "bounded disk residency enabled"
1823 );
1824 }
1825
1826 let no_auth = no_auth_active(&db_options);
1827 let auth_store =
1828 if db_options.auth.vault_enabled {
1829 let pager =
1830 runtime.db().store().pager().cloned().ok_or_else(|| {
1831 "vault requires a paged database (persistent mode)".to_string()
1832 })?;
1833 let store = AuthStore::with_vault(db_options.auth.clone(), pager)
1834 .map_err(|err| err.to_string())?;
1835 Arc::new(store)
1836 } else {
1837 Arc::new(AuthStore::new(db_options.auth.clone()))
1838 };
1839 auth_store.configure_control_events(
1840 runtime.control_event_ledger(),
1841 runtime.control_event_config(),
1842 );
1843 let bootstrap_already_completed = runtime
1860 .db()
1861 .store()
1862 .try_get_config(BOOTSTRAP_COMPLETED_KEY)
1863 .map_err(|error| error.to_string())?
1864 .is_some();
1865 ensure_persisted_auth_state(
1866 bootstrap_already_completed,
1867 db_options.auth.require_auth,
1868 auth_store.needs_bootstrap(),
1869 )?;
1870 let disposition = crate::cluster::authorize_cluster_bootstrap(
1871 db_options.storage_profile.deploy_profile,
1872 no_auth,
1873 bootstrap.auth_bootstrap_input(),
1874 bootstrap_already_completed,
1875 )?;
1876 let vault_plan = crate::cluster::plan_vault_bootstrap(disposition);
1885 match disposition {
1886 crate::cluster::BootstrapDisposition::SkipDevBypass => {
1887 debug_assert_eq!(vault_plan, crate::cluster::VaultBootstrapPlan::SkipNoVault);
1888 eprintln!("{NO_AUTH_WARNING}");
1889 tracing::warn!("{NO_AUTH_WARNING}");
1890 }
1891 crate::cluster::BootstrapDisposition::AlreadyComplete => {
1892 debug_assert_eq!(
1899 vault_plan,
1900 crate::cluster::VaultBootstrapPlan::OpenClusterGlobalStore {
1901 consume_secret_inputs: false,
1902 }
1903 );
1904 apply_preset_from_config(&runtime, &auth_store, bootstrap)?;
1905 tracing::info!("bootstrap already completed, unsealing existing cluster auth store");
1906 }
1907 crate::cluster::BootstrapDisposition::ProceedLocal => {
1908 debug_assert_eq!(
1911 vault_plan,
1912 crate::cluster::VaultBootstrapPlan::OpenClusterGlobalStore {
1913 consume_secret_inputs: true,
1914 }
1915 );
1916 apply_preset_from_config(&runtime, &auth_store, bootstrap)?;
1917 maybe_apply_policy_break_glass(&auth_store);
1918 }
1919 }
1920
1921 {
1923 let store = Arc::clone(&auth_store);
1924 std::thread::Builder::new()
1925 .name("reddb-session-purge".into())
1926 .spawn(move || loop {
1927 std::thread::sleep(std::time::Duration::from_secs(300));
1928 store.purge_expired_sessions();
1929 })
1930 .ok();
1931 }
1932
1933 runtime.set_auth_store(Arc::clone(&auth_store));
1936 Ok((runtime, auth_store, telemetry_guard))
1937}
1938
1939fn maybe_apply_policy_break_glass(auth_store: &Arc<AuthStore>) {
1943 use crate::auth::self_lock_guard::BREAK_GLASS_ENV;
1944
1945 let enabled = std::env::var(BREAK_GLASS_ENV)
1946 .ok()
1947 .map(|v| {
1948 let trimmed = v.trim().to_ascii_lowercase();
1949 matches!(trimmed.as_str(), "1" | "true" | "yes")
1950 })
1951 .unwrap_or(false);
1952 if !enabled {
1953 return;
1954 }
1955 let now = crate::utils::now_unix_millis() as u128;
1956 match auth_store.apply_policy_break_glass(now) {
1957 Ok(()) => {
1958 tracing::warn!(env = BREAK_GLASS_ENV, "policy break-glass recovery applied");
1959 }
1960 Err(err) => {
1961 tracing::error!(env = BREAK_GLASS_ENV, %err, "policy break-glass recovery failed");
1962 }
1963 }
1964}
1965
1966pub(crate) const BOOTSTRAP_COMPLETED_KEY: &str = "system.bootstrap.completed";
1971pub(crate) const BOOTSTRAP_PRESET_KEY: &str = "system.bootstrap.preset";
1972pub(crate) const BOOTSTRAP_FIRST_ADMIN_KEY: &str = "system.bootstrap.first_admin_id";
1973
1974pub(crate) const BOOTSTRAP_PRESET_ENV: &str = "REDDB_BOOTSTRAP_PRESET";
1977pub(crate) const PRESET_ENV: &str = "REDDB_PRESET";
1978pub(crate) const BOOTSTRAP_CERT_OUT_ENV: &str = "REDDB_BOOTSTRAP_CERT_OUT";
1981pub(crate) const PRESET_SIMPLE: &str = "simple";
1982pub(crate) const PRESET_PRODUCTION: &str = "production";
1983pub(crate) const PRESET_REGULATED: &str = "regulated";
1984pub(crate) const PRESET_CLOUD: &str = "cloud";
1985
1986pub(crate) const FIRST_ADMIN_ALLOW_ALL_POLICY: &str = "system.bootstrap.first-admin-allow-all";
1990pub(crate) const REGULATED_PROTECT_MANAGED_POLICY: &str = "system.regulated.protect-managed";
1991pub(crate) const CLOUD_PROTECT_MANAGED_POLICY: &str = "system.cloud.protect-managed";
1992pub(crate) const CLOUD_CONFIG_NAMESPACE: &str = "red.config.cloud";
1993pub(crate) const REGULATED_AUDIT_CONFIG_NAMESPACE: &str = "red.config.audit";
1994pub(crate) const REGULATED_EVIDENCE_CONFIG_NAMESPACE: &str = "red.config.evidence";
1995pub(crate) const REGULATED_QUERY_AUDIT_CONFIG_NAMESPACE: &str = "red.config.query_audit";
1996
1997pub(crate) fn apply_preset(
2007 runtime: &RedDBRuntime,
2008 auth_store: &Arc<AuthStore>,
2009) -> Result<(), String> {
2010 let bootstrap = BootstrapConfig::default();
2011 apply_preset_from_config(runtime, auth_store, &bootstrap)
2012}
2013
2014fn apply_preset_from_config(
2015 runtime: &RedDBRuntime,
2016 auth_store: &Arc<AuthStore>,
2017 bootstrap: &BootstrapConfig,
2018) -> Result<(), String> {
2019 let store = runtime.db().store();
2020
2021 if store
2022 .try_get_config(BOOTSTRAP_COMPLETED_KEY)
2023 .map_err(|error| error.to_string())?
2024 .is_some()
2025 {
2026 crate::cli::bootstrap_manifest::rehydrate_manifest_registry(
2027 runtime,
2028 &runtime.config_registry(),
2029 )?;
2030 tracing::info!("bootstrap state present, skipping preset application");
2031 return Ok(());
2032 }
2033
2034 let preset = bootstrap.resolved_preset();
2035
2036 if let Some(path) = bootstrap.selected_manifest() {
2037 if let Some(manifest_bootstrap) =
2038 crate::cli::bootstrap_manifest::bootstrap_config_from_manifest_file(&path)?
2039 {
2040 apply_preset_from_config(runtime, auth_store, &manifest_bootstrap)?;
2041 return Ok(());
2042 }
2043 let first_admin_id = crate::cli::bootstrap_manifest::apply_manifest_file(
2044 runtime,
2045 auth_store,
2046 &runtime.config_registry(),
2047 &path,
2048 )?;
2049 persist_bootstrap_state(runtime, "manifest", Some(&first_admin_id))?;
2050 tracing::info!("bootstrap manifest applied");
2051 return Ok(());
2052 }
2053
2054 if !auth_store.needs_bootstrap() {
2058 persist_bootstrap_state(runtime, &preset, None)?;
2059 tracing::info!(preset = %preset, "durable auth state present, skipping preset bootstrap");
2060 return Ok(());
2061 }
2062
2063 if preset == PRESET_SIMPLE && auth_store.config().require_auth && auth_store.needs_bootstrap() {
2069 tracing::info!("required-auth simple preset is awaiting durable first-admin bootstrap");
2070 return Ok(());
2071 }
2072
2073 let mut certificate_output = if matches!(preset.as_str(), PRESET_PRODUCTION | PRESET_CLOUD)
2074 && auth_store.bootstrap_requires_certificate_delivery()
2075 {
2076 require_bootstrap_certificate_output(bootstrap, &preset)?;
2077 Some(open_bootstrap_certificate_output(bootstrap)?)
2078 } else {
2079 None
2080 };
2081 let first_admin_id = match preset.as_str() {
2082 PRESET_SIMPLE => {
2083 None
2087 }
2088 PRESET_PRODUCTION => Some(apply_production_preset_from_config(
2089 auth_store,
2090 bootstrap,
2091 &mut certificate_output,
2092 )?),
2093 PRESET_CLOUD => Some(apply_cloud_preset(
2094 runtime,
2095 auth_store,
2096 bootstrap,
2097 &mut certificate_output,
2098 )?),
2099 PRESET_REGULATED => {
2100 apply_regulated_preset(runtime, auth_store)?;
2101 None
2102 }
2103 other => {
2104 return Err(format!(
2105 "bootstrap preset {other:?} is not recognised (expected `simple`, `production`, `regulated`, or `cloud`)"
2106 ));
2107 }
2108 };
2109
2110 auth_store
2113 .persist_to_vault_result()
2114 .map_err(|err| format!("persist bootstrap auth state: {err}"))?;
2115 persist_bootstrap_state(runtime, &preset, first_admin_id.as_deref())?;
2116 tracing::info!(preset = %preset, "bootstrap preset applied");
2117 Ok(())
2118}
2119
2120struct BootstrapCertificateOutput {
2121 path: PathBuf,
2122 file: Option<std::fs::File>,
2123 committed: bool,
2124}
2125
2126impl Drop for BootstrapCertificateOutput {
2127 fn drop(&mut self) {
2128 if self.committed {
2129 return;
2130 }
2131 self.file.take();
2132 let _ = std::fs::remove_file(&self.path);
2133 }
2134}
2135
2136fn open_bootstrap_certificate_output(
2139 bootstrap: &BootstrapConfig,
2140) -> Result<BootstrapCertificateOutput, String> {
2141 let Some(path) = bootstrap.selected_cert_out() else {
2142 return Err("bootstrap certificate output is not configured".to_string());
2143 };
2144 let mut options = std::fs::OpenOptions::new();
2145 options.write(true).create_new(true);
2146 #[cfg(unix)]
2147 {
2148 use std::os::unix::fs::OpenOptionsExt;
2149 options.mode(0o600);
2150 }
2151 let file = options
2152 .open(&path)
2153 .map_err(|err| format!("create --bootstrap-cert-out {}: {err}", path.display()))?;
2154 Ok(BootstrapCertificateOutput {
2155 path,
2156 file: Some(file),
2157 committed: false,
2158 })
2159}
2160
2161fn stage_bootstrap_certificate(
2162 output: &mut BootstrapCertificateOutput,
2163 certificate_hex: &str,
2164) -> Result<(), String> {
2165 use std::io::Write;
2166 let path = output.path.clone();
2167 let file = output
2168 .file
2169 .as_mut()
2170 .ok_or_else(|| "bootstrap certificate output is closed".to_string())?;
2171 writeln!(file, "{certificate_hex}")
2172 .map_err(|err| format!("write --bootstrap-cert-out {}: {err}", path.display()))?;
2173 file.sync_all()
2174 .map_err(|err| format!("sync --bootstrap-cert-out {}: {err}", path.display()))?;
2175 Ok(())
2176}
2177
2178fn commit_bootstrap_certificate(mut output: BootstrapCertificateOutput) {
2179 let path = output.path.clone();
2180 output.committed = true;
2181 tracing::info!(
2182 target: "reddb::bootstrap",
2183 path = %path.display(),
2184 "first-boot self-bootstrap: wrote unseal certificate to --bootstrap-cert-out (consume via REDDB_CERTIFICATE_FILE on the next boot)"
2185 );
2186}
2187
2188fn bootstrap_with_certificate_output(
2189 auth_store: &Arc<AuthStore>,
2190 username: &str,
2191 password: &str,
2192 certificate_output: &mut Option<BootstrapCertificateOutput>,
2193) -> Result<crate::auth::store::BootstrapResult, String> {
2194 let mut certificate_staged = false;
2195 let result = auth_store
2196 .bootstrap_with_certificate_delivery(username, password, |certificate| {
2197 let output = certificate_output.as_mut().ok_or_else(|| {
2198 crate::auth::AuthError::Internal(
2199 "bootstrap certificate output was not prepared".to_string(),
2200 )
2201 })?;
2202 stage_bootstrap_certificate(output, certificate)
2203 .map_err(crate::auth::AuthError::Internal)?;
2204 certificate_staged = true;
2205 Ok(())
2206 })
2207 .map_err(|err| format!("bootstrap first admin: {err}"))?;
2208
2209 if !certificate_staged {
2213 if let (Some(certificate), Some(output)) =
2214 (result.certificate.as_deref(), certificate_output.as_mut())
2215 {
2216 stage_bootstrap_certificate(output, certificate)?;
2217 certificate_staged = true;
2218 }
2219 }
2220 if certificate_staged {
2221 let output = certificate_output
2222 .take()
2223 .ok_or_else(|| "bootstrap certificate output was not prepared".to_string())?;
2224 commit_bootstrap_certificate(output);
2225 }
2226 Ok(result)
2227}
2228
2229fn ensure_persisted_auth_state(
2230 bootstrap_completed: bool,
2231 require_auth: bool,
2232 needs_bootstrap: bool,
2233) -> Result<(), String> {
2234 if bootstrap_completed && require_auth && needs_bootstrap {
2235 return Err(
2236 "persistent database is marked bootstrapped but its authentication state is empty; refusing to reopen public bootstrap"
2237 .to_string(),
2238 );
2239 }
2240 Ok(())
2241}
2242
2243fn require_bootstrap_certificate_output(
2244 bootstrap: &BootstrapConfig,
2245 preset: &str,
2246) -> Result<(), String> {
2247 if bootstrap.selected_cert_out().is_some() {
2248 return Ok(());
2249 }
2250 Err(format!(
2251 "bootstrap preset `{preset}` requires --bootstrap-cert-out (or REDDB_BOOTSTRAP_CERT_OUT); vault certificates are never written to logs"
2252 ))
2253}
2254
2255fn apply_production_preset_from_config(
2256 auth_store: &Arc<AuthStore>,
2257 bootstrap: &BootstrapConfig,
2258 certificate_output: &mut Option<BootstrapCertificateOutput>,
2259) -> Result<String, String> {
2260 use crate::auth::store::PrincipalRef;
2261 use crate::auth::UserId;
2262
2263 let username = bootstrap.production_username().ok_or_else(|| {
2264 "production preset requires --bootstrap-admin or REDDB_USERNAME (or REDDB_USERNAME_FILE)"
2265 .to_string()
2266 })?;
2267 let password = bootstrap.production_password().ok_or_else(|| {
2268 "production preset requires --bootstrap-admin-password or REDDB_PASSWORD (or REDDB_PASSWORD_FILE)"
2269 .to_string()
2270 })?;
2271
2272 let result =
2276 bootstrap_with_certificate_output(auth_store, &username, &password, certificate_output)?;
2277 if result.certificate.is_some() {
2278 tracing::warn!(
2279 "vault certificate issued by REDDB_PRESET=production and written to the configured certificate output"
2280 );
2281 }
2282 let first_admin = UserId::platform(result.user.username.clone());
2283
2284 install_allow_all_policy(auth_store)?;
2286
2287 auth_store
2289 .attach_policy(
2290 PrincipalRef::User(first_admin.clone()),
2291 FIRST_ADMIN_ALLOW_ALL_POLICY,
2292 )
2293 .map_err(|err| format!("attach allow-all policy: {err}"))?;
2294
2295 Ok(first_admin.to_string())
2296}
2297
2298fn apply_cloud_preset(
2299 runtime: &RedDBRuntime,
2300 auth_store: &Arc<AuthStore>,
2301 bootstrap: &BootstrapConfig,
2302 certificate_output: &mut Option<BootstrapCertificateOutput>,
2303) -> Result<String, String> {
2304 use crate::auth::store::PrincipalRef;
2305 use crate::auth::{Role, UserId};
2306
2307 let head_username = bootstrap.cloud_head_username().ok_or_else(|| {
2308 "cloud preset requires --cloud-head-admin or REDDB_CLOUD_HEAD_ADMIN (or REDDB_USERNAME)"
2309 .to_string()
2310 })?;
2311 let head_password = bootstrap.cloud_head_password().ok_or_else(|| {
2312 "cloud preset requires --cloud-head-admin-password or REDDB_CLOUD_HEAD_ADMIN_PASSWORD (or REDDB_PASSWORD)"
2313 .to_string()
2314 })?;
2315 let customer_username = bootstrap.customer_username().ok_or_else(|| {
2316 "cloud preset requires --customer-admin or REDDB_CUSTOMER_ADMIN".to_string()
2317 })?;
2318 let customer_password = bootstrap.customer_password().ok_or_else(|| {
2319 "cloud preset requires --customer-admin-password or REDDB_CUSTOMER_ADMIN_PASSWORD"
2320 .to_string()
2321 })?;
2322 if head_username == customer_username {
2323 return Err("cloud preset requires distinct head and customer admin usernames".to_string());
2324 }
2325
2326 let head = bootstrap_with_certificate_output(
2327 auth_store,
2328 &head_username,
2329 &head_password,
2330 certificate_output,
2331 )
2332 .map_err(|err| format!("bootstrap cloud head admin: {err}"))?;
2333 let head_id = UserId::platform(head.user.username.clone());
2334
2335 auth_store
2336 .create_user(&customer_username, &customer_password, Role::Admin)
2337 .map_err(|err| format!("create cloud customer admin: {err}"))?;
2338 let customer_id = UserId::platform(customer_username.clone());
2339
2340 install_allow_all_policy(auth_store)?;
2341 for user_id in [&head_id, &customer_id] {
2342 auth_store
2343 .attach_policy(
2344 PrincipalRef::User(user_id.clone()),
2345 FIRST_ADMIN_ALLOW_ALL_POLICY,
2346 )
2347 .map_err(|err| format!("attach allow-all policy: {err}"))?;
2348 }
2349 install_cloud_guardrails(runtime, auth_store, &head_id)?;
2350 auth_store
2351 .attach_policy(
2352 PrincipalRef::User(customer_id),
2353 CLOUD_PROTECT_MANAGED_POLICY,
2354 )
2355 .map_err(|err| format!("attach cloud guardrail policy: {err}"))?;
2356
2357 Ok(head_id.to_string())
2358}
2359
2360pub(crate) fn install_allow_all_policy(auth_store: &AuthStore) -> Result<(), String> {
2361 use crate::auth::policies::Policy;
2362
2363 let policy = Policy::from_json_str(&format!(
2364 r#"{{
2365 "id": "{id}",
2366 "version": 1,
2367 "statements": [{{
2368 "effect": "allow",
2369 "actions": ["*"],
2370 "resources": ["*"]
2371 }}]
2372 }}"#,
2373 id = FIRST_ADMIN_ALLOW_ALL_POLICY
2374 ))
2375 .map_err(|err| format!("compile allow-all policy: {err}"))?;
2376 auth_store
2377 .put_policy(policy)
2378 .map_err(|err| format!("install allow-all policy: {err}"))
2379}
2380
2381fn install_cloud_guardrails(
2382 runtime: &RedDBRuntime,
2383 auth_store: &Arc<AuthStore>,
2384 head_admin: &crate::auth::UserId,
2385) -> Result<(), String> {
2386 use crate::auth::policies::Policy;
2387 use crate::auth::registry::EvidenceRequirement;
2388
2389 let policy = Policy::from_json_str(&format!(
2390 r#"{{
2391 "id": "{id}",
2392 "version": 1,
2393 "statements": [
2394 {{
2395 "effect": "deny",
2396 "actions": ["policy:put", "policy:drop", "policy:attach", "policy:detach"],
2397 "resources": ["policy:{id}"]
2398 }},
2399 {{
2400 "effect": "deny",
2401 "actions": [
2402 "user:delete",
2403 "user:disable",
2404 "user:password:change",
2405 "user:role:update"
2406 ],
2407 "resources": ["user:{head_admin}"]
2408 }},
2409 {{
2410 "effect": "deny",
2411 "actions": ["config:write"],
2412 "resources": ["config:{cloud}.*"]
2413 }}
2414 ]
2415 }}"#,
2416 id = CLOUD_PROTECT_MANAGED_POLICY,
2417 head_admin = head_admin,
2418 cloud = CLOUD_CONFIG_NAMESPACE,
2419 ))
2420 .map_err(|err| format!("compile cloud guardrail policy: {err}"))?;
2421 auth_store
2422 .put_policy(policy)
2423 .map_err(|err| format!("install cloud guardrail policy: {err}"))?;
2424
2425 let now_ms = crate::utils::now_unix_millis() as u128;
2426 let entries = vec![
2427 regulated_registry_entry(
2428 CLOUD_PROTECT_MANAGED_POLICY,
2429 crate::auth::managed_policy::RESOURCE_TYPE_POLICY,
2430 "policy",
2431 "policy:*",
2432 &format!("policy:{CLOUD_PROTECT_MANAGED_POLICY}"),
2433 EvidenceRequirement::Metadata,
2434 now_ms,
2435 ),
2436 regulated_registry_entry(
2437 CLOUD_CONFIG_NAMESPACE,
2438 crate::auth::managed_config::RESOURCE_TYPE_CONFIG_NAMESPACE,
2439 "config_namespace",
2440 "config:write",
2441 &format!("config:{CLOUD_CONFIG_NAMESPACE}.*"),
2442 EvidenceRequirement::Metadata,
2443 now_ms,
2444 ),
2445 ];
2446 for entry in entries.iter().cloned() {
2447 runtime
2448 .config_registry()
2449 .restore_bootstrap_entry(entry)
2450 .map_err(|err| format!("install cloud registry entry: {err}"))?;
2451 }
2452 crate::cli::bootstrap_manifest::persist_registry_state(runtime, &entries)?;
2453 Ok(())
2454}
2455
2456fn apply_regulated_preset(
2457 runtime: &RedDBRuntime,
2458 auth_store: &Arc<AuthStore>,
2459) -> Result<(), String> {
2460 use crate::auth::policies::Policy;
2461 use crate::auth::registry::EvidenceRequirement;
2462 use crate::auth::store::{PrincipalRef, PUBLIC_IAM_GROUP};
2463
2464 runtime.query_audit().enable_infrastructure();
2465
2466 let policy = Policy::from_json_str(&format!(
2467 r#"{{
2468 "id": "{id}",
2469 "version": 1,
2470 "statements": [
2471 {{
2472 "effect": "deny",
2473 "actions": ["policy:put", "policy:drop", "policy:attach", "policy:detach"],
2474 "resources": ["policy:{id}"]
2475 }},
2476 {{
2477 "effect": "deny",
2478 "actions": ["config:write"],
2479 "resources": [
2480 "config:{audit}.*",
2481 "config:{evidence}.*",
2482 "config:{query_audit}.*"
2483 ]
2484 }}
2485 ]
2486 }}"#,
2487 id = REGULATED_PROTECT_MANAGED_POLICY,
2488 audit = REGULATED_AUDIT_CONFIG_NAMESPACE,
2489 evidence = REGULATED_EVIDENCE_CONFIG_NAMESPACE,
2490 query_audit = REGULATED_QUERY_AUDIT_CONFIG_NAMESPACE,
2491 ))
2492 .map_err(|err| format!("compile regulated guardrail policy: {err}"))?;
2493 auth_store
2494 .put_policy(policy)
2495 .map_err(|err| format!("install regulated guardrail policy: {err}"))?;
2496 auth_store
2497 .attach_policy(
2498 PrincipalRef::Group(PUBLIC_IAM_GROUP.to_string()),
2499 REGULATED_PROTECT_MANAGED_POLICY,
2500 )
2501 .map_err(|err| format!("attach regulated guardrail policy: {err}"))?;
2502
2503 let now_ms = crate::utils::now_unix_millis() as u128;
2504 let entries = vec![
2505 regulated_registry_entry(
2506 REGULATED_PROTECT_MANAGED_POLICY,
2507 crate::auth::managed_policy::RESOURCE_TYPE_POLICY,
2508 "iam_policy",
2509 "policy:*",
2510 &format!("policy:{REGULATED_PROTECT_MANAGED_POLICY}"),
2511 EvidenceRequirement::Metadata,
2512 now_ms,
2513 ),
2514 regulated_registry_entry(
2515 REGULATED_AUDIT_CONFIG_NAMESPACE,
2516 crate::auth::managed_config::RESOURCE_TYPE_CONFIG_NAMESPACE,
2517 "config_namespace",
2518 "config:write",
2519 &format!("config:{REGULATED_AUDIT_CONFIG_NAMESPACE}.*"),
2520 EvidenceRequirement::Metadata,
2521 now_ms,
2522 ),
2523 regulated_registry_entry(
2524 REGULATED_EVIDENCE_CONFIG_NAMESPACE,
2525 crate::auth::managed_config::RESOURCE_TYPE_CONFIG_NAMESPACE,
2526 "config_namespace",
2527 "config:write",
2528 &format!("config:{REGULATED_EVIDENCE_CONFIG_NAMESPACE}.*"),
2529 EvidenceRequirement::Metadata,
2530 now_ms,
2531 ),
2532 regulated_registry_entry(
2533 REGULATED_QUERY_AUDIT_CONFIG_NAMESPACE,
2534 crate::auth::managed_config::RESOURCE_TYPE_CONFIG_NAMESPACE,
2535 "config_namespace",
2536 "config:write",
2537 &format!("config:{REGULATED_QUERY_AUDIT_CONFIG_NAMESPACE}.*"),
2538 EvidenceRequirement::Metadata,
2539 now_ms,
2540 ),
2541 ];
2542
2543 for entry in entries.iter().cloned() {
2544 runtime
2545 .config_registry()
2546 .restore_bootstrap_entry(entry)
2547 .map_err(|err| format!("install regulated registry entry: {err}"))?;
2548 }
2549 crate::cli::bootstrap_manifest::persist_registry_state(runtime, &entries)?;
2550 Ok(())
2551}
2552
2553fn regulated_registry_entry(
2554 id: &str,
2555 resource_type: &str,
2556 schema: &str,
2557 required_action: &str,
2558 required_resource: &str,
2559 evidence_requirement: crate::auth::registry::EvidenceRequirement,
2560 updated_at_ms: u128,
2561) -> crate::auth::registry::ConfigRegistryEntry {
2562 crate::auth::registry::ConfigRegistryEntry {
2563 id: id.to_string(),
2564 version: 1,
2565 resource_type: resource_type.to_string(),
2566 schema: schema.to_string(),
2567 mutability: crate::auth::registry::Mutability::Immutable,
2568 sensitivity: crate::auth::registry::Sensitivity::Internal,
2569 managed: true,
2570 required_action: required_action.to_string(),
2571 required_resource: required_resource.to_string(),
2572 evidence_requirement,
2573 updated_by: "system:regulated-preset".to_string(),
2574 updated_at_ms,
2575 }
2576}
2577
2578fn persist_bootstrap_state(
2579 runtime: &RedDBRuntime,
2580 preset: &str,
2581 first_admin_id: Option<&str>,
2582) -> Result<(), String> {
2583 let store = runtime.db().store();
2584 let mut tree = crate::serde_json::Map::new();
2585 tree.insert(
2586 BOOTSTRAP_COMPLETED_KEY.to_string(),
2587 crate::serde_json::Value::Bool(true),
2588 );
2589 tree.insert(
2590 BOOTSTRAP_PRESET_KEY.to_string(),
2591 crate::serde_json::Value::String(preset.to_string()),
2592 );
2593 if let Some(id) = first_admin_id {
2594 tree.insert(
2595 BOOTSTRAP_FIRST_ADMIN_KEY.to_string(),
2596 crate::serde_json::Value::String(id.to_string()),
2597 );
2598 }
2599 let json = crate::serde_json::Value::Object(tree);
2600 store
2601 .try_set_config_tree("", &json)
2602 .map_err(|error| error.to_string())?;
2603 Ok(())
2604}
2605
2606fn merge_telemetry_with_config(
2617 mut cli: crate::telemetry::TelemetryConfig,
2618 runtime: &RedDBRuntime,
2619) -> crate::telemetry::TelemetryConfig {
2620 use reddb_types::Value;
2621
2622 let store = runtime.db().store();
2623
2624 if !cli.level_explicit {
2625 if let Some(Value::Text(v)) = store.get_config("red.logging.level") {
2626 cli.level_filter = v.to_string();
2627 }
2628 }
2629 if !cli.format_explicit {
2630 if let Some(Value::Text(v)) = store.get_config("red.logging.format") {
2631 if let Some(parsed) = crate::telemetry::LogFormat::parse(&v) {
2632 cli.format = parsed;
2633 }
2634 }
2635 }
2636 if !cli.rotation_keep_days_explicit {
2637 match store.get_config("red.logging.keep_days") {
2638 Some(Value::Integer(n)) if n >= 0 && n <= u16::MAX as i64 => {
2639 cli.rotation_keep_days = n as u16
2640 }
2641 Some(Value::UnsignedInteger(n)) if n <= u16::MAX as u64 => {
2642 cli.rotation_keep_days = n as u16
2643 }
2644 Some(Value::Text(v)) => {
2645 if let Ok(n) = v.parse::<u16>() {
2646 cli.rotation_keep_days = n;
2647 }
2648 }
2649 _ => {}
2650 }
2651 }
2652 if !cli.file_prefix_explicit {
2653 if let Some(Value::Text(v)) = store.get_config("red.logging.file_prefix") {
2654 if !v.is_empty() {
2655 cli.file_prefix = v.to_string();
2656 }
2657 }
2658 }
2659 if !cli.log_dir_explicit && !cli.log_file_disabled {
2662 if let Some(Value::Text(v)) = store.get_config("red.logging.dir") {
2663 if !v.is_empty() {
2664 cli.log_dir = Some(std::path::PathBuf::from(v.as_ref()));
2665 }
2666 }
2667 }
2668
2669 cli
2670}
2671
2672#[cfg(test)]
2673mod telemetry_merge_tests {
2674 use super::*;
2675 use crate::telemetry::{LogFormat, TelemetryConfig};
2676
2677 fn fresh_runtime() -> RedDBRuntime {
2678 RedDBRuntime::with_options(RedDBOptions::in_memory()).expect("runtime")
2679 }
2680
2681 fn set_str(runtime: &RedDBRuntime, key: &str, value: &str) {
2682 runtime
2683 .db()
2684 .store()
2685 .set_config_tree(key, &crate::serde_json::Value::String(value.to_string()));
2686 }
2687
2688 fn cli_base() -> TelemetryConfig {
2689 TelemetryConfig {
2692 log_dir: Some(std::path::PathBuf::from("/tmp/reddb-default/logs")),
2693 format: LogFormat::Json,
2694 ..Default::default()
2695 }
2696 }
2697
2698 #[test]
2699 fn config_log_dir_promoted_when_flag_absent() {
2700 let runtime = fresh_runtime();
2701 set_str(&runtime, "red.logging.dir", "/var/log/reddb");
2702 let merged = merge_telemetry_with_config(cli_base(), &runtime);
2703 assert_eq!(
2704 merged.log_dir.as_deref(),
2705 Some(std::path::Path::new("/var/log/reddb"))
2706 );
2707 }
2708
2709 #[test]
2710 fn explicit_log_dir_wins_over_config() {
2711 let runtime = fresh_runtime();
2712 set_str(&runtime, "red.logging.dir", "/var/log/reddb");
2713 let mut cli = cli_base();
2714 cli.log_dir = Some(std::path::PathBuf::from("/custom/dir"));
2715 cli.log_dir_explicit = true;
2716 let merged = merge_telemetry_with_config(cli, &runtime);
2717 assert_eq!(
2718 merged.log_dir.as_deref(),
2719 Some(std::path::Path::new("/custom/dir"))
2720 );
2721 }
2722
2723 #[test]
2724 fn no_log_file_beats_config_log_dir() {
2725 let runtime = fresh_runtime();
2726 set_str(&runtime, "red.logging.dir", "/var/log/reddb");
2727 let mut cli = cli_base();
2728 cli.log_dir = None;
2729 cli.log_file_disabled = true;
2730 let merged = merge_telemetry_with_config(cli, &runtime);
2731 assert!(
2732 merged.log_dir.is_none(),
2733 "--no-log-file must veto config dir"
2734 );
2735 }
2736
2737 #[test]
2738 fn config_format_promoted_on_non_tty_default() {
2739 let runtime = fresh_runtime();
2743 set_str(&runtime, "red.logging.format", "pretty");
2744 let merged = merge_telemetry_with_config(cli_base(), &runtime);
2745 assert_eq!(merged.format, LogFormat::Pretty);
2746 }
2747
2748 #[test]
2749 fn explicit_format_wins_over_config() {
2750 let runtime = fresh_runtime();
2751 set_str(&runtime, "red.logging.format", "pretty");
2752 let mut cli = cli_base();
2753 cli.format = LogFormat::Json;
2754 cli.format_explicit = true;
2755 let merged = merge_telemetry_with_config(cli, &runtime);
2756 assert_eq!(merged.format, LogFormat::Json);
2757 }
2758}
2759
2760#[inline(never)]
2761fn build_http_server(
2762 runtime: RedDBRuntime,
2763 auth_store: Arc<AuthStore>,
2764 bind_addr: String,
2765) -> RedDBServer {
2766 build_http_server_with_transport_readiness(
2767 runtime,
2768 auth_store,
2769 bind_addr,
2770 TransportReadiness::default(),
2771 )
2772}
2773
2774fn apply_http_limits(
2780 server: RedDBServer,
2781 config: &ServerCommandConfig,
2782 runtime: &RedDBRuntime,
2783) -> RedDBServer {
2784 let store = runtime.db().store();
2785 let resolved =
2786 crate::server::http_limits::resolve_http_limits(&config.http_limits_cli, |key| match store
2787 .get_config(key)
2788 {
2789 Some(reddb_types::Value::Text(v)) => Some(v.to_string()),
2790 Some(reddb_types::Value::Integer(n)) if n >= 0 => Some(n.to_string()),
2791 Some(reddb_types::Value::UnsignedInteger(n)) => Some(n.to_string()),
2792 _ => None,
2793 });
2794 tracing::info!(
2795 target: "reddb::http_limits",
2796 max_handlers = resolved.max_handlers,
2797 execution_workers = resolved.execution_workers,
2798 handler_timeout_ms = resolved.handler_timeout_ms,
2799 retry_after_secs = resolved.retry_after_secs,
2800 max_inflight_per_principal = resolved.max_inflight_per_principal,
2801 "http_limits resolved"
2802 );
2803 server.with_http_limits(resolved)
2804}
2805
2806fn apply_ui_bundle(
2816 server: RedDBServer,
2817 config: &ServerCommandConfig,
2818) -> Result<RedDBServer, String> {
2819 if !config.ui {
2820 return Ok(server);
2821 }
2822 let ui_dir = match &config.ui_dir {
2823 Some(dir) => dir.clone(),
2824 None => {
2825 let cache_root = crate::server::ui_bundle_resolver::reddb_user_cache_root()
2826 .unwrap_or_else(|_| std::env::temp_dir().join("reddb"));
2827 crate::server::ui_bundle_resolver::resolve_ui_bundle(
2828 &cache_root,
2829 &crate::server::ui_bundle_resolver::HttpFetcher,
2830 )
2831 .map_err(|err| format!("resolve red-ui bundle for --ui: {err}"))?
2832 }
2833 };
2834 tracing::info!(target: "reddb::ui", dir = %ui_dir.display(), "serving red-ui bundle on HTTP surface");
2835 Ok(server.with_ui_dir(ui_dir))
2836}
2837
2838#[inline(never)]
2839fn build_http_server_with_transport_readiness(
2840 runtime: RedDBRuntime,
2841 auth_store: Arc<AuthStore>,
2842 bind_addr: String,
2843 transport_readiness: TransportReadiness,
2844) -> RedDBServer {
2845 RedDBServer::with_options(
2846 runtime,
2847 ServerOptions {
2848 bind_addr,
2849 transport_readiness,
2850 ..ServerOptions::default()
2851 },
2852 )
2853 .with_auth(auth_store)
2854}
2855
2856#[inline(never)]
2860fn build_admin_only_server(
2861 runtime: RedDBRuntime,
2862 auth_store: Arc<AuthStore>,
2863 bind_addr: String,
2864) -> RedDBServer {
2865 RedDBServer::with_options(
2866 runtime,
2867 ServerOptions {
2868 bind_addr,
2869 surface: crate::server::ServerSurface::AdminOnly,
2870 ..ServerOptions::default()
2871 },
2872 )
2873 .with_auth(auth_store)
2874}
2875
2876#[inline(never)]
2880fn build_metrics_only_server(
2881 runtime: RedDBRuntime,
2882 auth_store: Arc<AuthStore>,
2883 bind_addr: String,
2884) -> RedDBServer {
2885 RedDBServer::with_options(
2886 runtime,
2887 ServerOptions {
2888 bind_addr,
2889 surface: crate::server::ServerSurface::MetricsOnly,
2890 ..ServerOptions::default()
2891 },
2892 )
2893 .with_auth(auth_store)
2894}
2895
2896fn spawn_admin_metrics_listeners(runtime: &RedDBRuntime, auth_store: &Arc<AuthStore>) {
2900 if let Some(addr) = env_nonempty("RED_ADMIN_BIND") {
2901 let server = build_admin_only_server(runtime.clone(), auth_store.clone(), addr.clone());
2902 let _ = server.serve_in_background();
2903 tracing::info!(transport = "http", surface = "admin", bind = %addr, "listener online");
2904 }
2905 if let Some(addr) = env_nonempty("RED_METRICS_BIND") {
2906 let server = build_metrics_only_server(runtime.clone(), auth_store.clone(), addr.clone());
2907 let _ = server.serve_in_background();
2908 tracing::info!(transport = "http", surface = "metrics", bind = %addr, "listener online");
2909 }
2910}
2911
2912fn resolve_http_tls_config(
2914 config: &ServerCommandConfig,
2915) -> Result<crate::server::tls::HttpTlsConfig, String> {
2916 match (&config.http_tls_cert, &config.http_tls_key) {
2917 (Some(cert), Some(key)) => Ok(crate::server::tls::HttpTlsConfig {
2918 cert_path: cert.clone(),
2919 key_path: key.clone(),
2920 client_ca_path: config.http_tls_client_ca.clone(),
2921 }),
2922 (None, None) => {
2923 let dir = config
2925 .path
2926 .as_ref()
2927 .and_then(|p| p.parent().map(std::path::PathBuf::from))
2928 .unwrap_or_else(|| std::path::PathBuf::from("."));
2929 let auto = crate::server::tls::auto_generate_dev_cert(&dir)
2930 .map_err(|err| format!("HTTP TLS dev: {err}"))?;
2931 Ok(crate::server::tls::HttpTlsConfig {
2932 cert_path: auto.cert_path,
2933 key_path: auto.key_path,
2934 client_ca_path: config.http_tls_client_ca.clone(),
2935 })
2936 }
2937 _ => Err("HTTP TLS requires both --http-tls-cert and --http-tls-key (or neither, with RED_HTTP_TLS_DEV=1)".to_string()),
2938 }
2939}
2940
2941#[cfg(test)]
2942mod tests {
2943 use super::*;
2944
2945 #[test]
2946 fn server_runtime_receives_the_bootstrapped_auth_authority() {
2947 let mut options = RedDBOptions::in_memory();
2948 options.auth.enabled = true;
2949 let (runtime, store, _telemetry) =
2950 build_runtime_with_telemetry(options, None).expect("server runtime");
2951 let installed = runtime.auth_store().expect("runtime auth authority");
2952 assert!(Arc::ptr_eq(&store, &installed));
2953 }
2954
2955 #[test]
2956 fn runtime_workers_use_both_cores_on_small_instances() {
2957 assert_eq!(suggested_worker_count(1), 1);
2958 assert_eq!(suggested_worker_count(2), 2);
2959 assert_eq!(suggested_worker_count(4), 3);
2960 }
2961
2962 #[test]
2963 fn render_systemd_unit_contains_expected_execstart() {
2964 let config = SystemdServiceConfig {
2965 service_name: "reddb".to_string(),
2966 binary_path: PathBuf::from("/usr/local/bin/red"),
2967 run_user: "reddb".to_string(),
2968 run_group: "reddb".to_string(),
2969 data_path: reddb_file::default_service_database_path(),
2970 router_bind_addr: None,
2971 grpc_bind_addr: Some("0.0.0.0:55055".to_string()),
2972 http_bind_addr: None,
2973 };
2974
2975 let unit = render_systemd_unit(&config);
2976 assert!(unit.contains("ExecStart=/usr/local/bin/red server --path /var/lib/reddb/data.rdb --grpc-bind 0.0.0.0:55055"));
2977 assert!(unit.contains("ReadWritePaths=/var/lib/reddb"));
2978 }
2979
2980 #[test]
2981 fn systemd_service_config_derives_paths() {
2982 let config = SystemdServiceConfig {
2983 service_name: "reddb-api".to_string(),
2984 binary_path: PathBuf::from("/usr/local/bin/red"),
2985 run_user: "reddb".to_string(),
2986 run_group: "reddb".to_string(),
2987 data_path: PathBuf::from("/srv/reddb/live/data.rdb"),
2988 router_bind_addr: None,
2989 grpc_bind_addr: None,
2990 http_bind_addr: Some("127.0.0.1:5000".to_string()),
2991 };
2992
2993 assert_eq!(config.data_dir(), PathBuf::from("/srv/reddb/live"));
2994 assert_eq!(
2995 config.unit_path(),
2996 PathBuf::from("/etc/systemd/system/reddb-api.service")
2997 );
2998 }
2999
3000 #[test]
3001 fn render_systemd_unit_supports_dual_transport() {
3002 let config = SystemdServiceConfig {
3003 service_name: "reddb".to_string(),
3004 binary_path: PathBuf::from("/usr/local/bin/red"),
3005 run_user: "reddb".to_string(),
3006 run_group: "reddb".to_string(),
3007 data_path: reddb_file::default_service_database_path(),
3008 router_bind_addr: None,
3009 grpc_bind_addr: Some("0.0.0.0:55055".to_string()),
3010 http_bind_addr: Some("0.0.0.0:5000".to_string()),
3011 };
3012
3013 let unit = render_systemd_unit(&config);
3014 assert!(unit.contains("--grpc-bind 0.0.0.0:55055"));
3015 assert!(unit.contains("--http-bind 0.0.0.0:5000"));
3016 }
3017
3018 #[test]
3019 fn render_systemd_unit_supports_router_mode() {
3020 let config = SystemdServiceConfig {
3021 service_name: "reddb".to_string(),
3022 binary_path: PathBuf::from("/usr/local/bin/red"),
3023 run_user: "reddb".to_string(),
3024 run_group: "reddb".to_string(),
3025 data_path: reddb_file::default_service_database_path(),
3026 router_bind_addr: Some(DEFAULT_ROUTER_BIND_ADDR.to_string()),
3027 grpc_bind_addr: None,
3028 http_bind_addr: None,
3029 };
3030
3031 let unit = render_systemd_unit(&config);
3032 assert!(unit.contains("--bind 127.0.0.1:5050"));
3033 assert!(!unit.contains("--grpc-bind"));
3034 assert!(!unit.contains("--http-bind"));
3035 }
3036
3037 #[test]
3038 fn explicit_bind_collision_is_fatal() {
3039 let held = TcpListener::bind("127.0.0.1:0").expect("hold test port");
3040 let addr = held.local_addr().expect("held addr").to_string();
3041 let mut readiness = TransportReadiness::default();
3042
3043 let error = bind_listener_for_startup(&mut readiness, "http", &addr, true).unwrap_err();
3044
3045 assert!(error.contains("explicit http listener bind"));
3046 assert_eq!(readiness.active.len(), 0);
3047 assert_eq!(readiness.failed.len(), 1);
3048 assert!(readiness.failed[0].explicit);
3049 assert_eq!(readiness.failed[0].bind_addr, addr);
3050 }
3051
3052 fn no_auth_env_lock() -> &'static std::sync::Mutex<()> {
3059 static LOCK: std::sync::OnceLock<std::sync::Mutex<()>> = std::sync::OnceLock::new();
3060 LOCK.get_or_init(|| std::sync::Mutex::new(()))
3061 }
3062
3063 fn bootstrap_cert_path(label: &str) -> PathBuf {
3064 std::env::temp_dir().join(format!(
3065 "reddb-{label}-{}-{}.cert",
3066 std::process::id(),
3067 std::time::SystemTime::now()
3068 .duration_since(std::time::UNIX_EPOCH)
3069 .unwrap_or_default()
3070 .as_nanos()
3071 ))
3072 }
3073
3074 #[test]
3075 fn secure_presets_require_certificate_file_delivery() {
3076 let _guard = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3077 unsafe {
3078 std::env::remove_var(BOOTSTRAP_CERT_OUT_ENV);
3079 }
3080 let missing = BootstrapConfig::default();
3081 assert!(require_bootstrap_certificate_output(&missing, PRESET_PRODUCTION).is_err());
3082
3083 let configured = BootstrapConfig {
3084 cert_out: Some(bootstrap_cert_path("required-output")),
3085 ..BootstrapConfig::default()
3086 };
3087 assert!(require_bootstrap_certificate_output(&configured, PRESET_PRODUCTION).is_ok());
3088 }
3089
3090 #[test]
3091 fn official_images_enable_persistent_vault_auth() {
3092 for dockerfile in [
3093 include_str!("../../../Dockerfile"),
3094 include_str!("../../../docker/Dockerfile.release"),
3095 include_str!("../../../docker/Dockerfile.musl"),
3096 ] {
3097 assert!(dockerfile.contains("REDDB_AUTH=true"));
3098 assert!(dockerfile.contains("REDDB_REQUIRE_AUTH=true"));
3099 assert!(dockerfile.contains("REDDB_VAULT=true"));
3100 assert!(!dockerfile.contains("REDDB_VAULT=false"));
3101 }
3102 }
3103
3104 #[test]
3105 fn completed_required_auth_database_cannot_reenter_bootstrap() {
3106 let error = ensure_persisted_auth_state(true, true, true)
3107 .expect_err("empty auth state after durable completion must fail closed");
3108 assert!(error.contains("refusing to reopen public bootstrap"));
3109
3110 assert!(ensure_persisted_auth_state(false, true, true).is_ok());
3111 assert!(ensure_persisted_auth_state(true, false, true).is_ok());
3112 assert!(ensure_persisted_auth_state(true, true, false).is_ok());
3113 }
3114
3115 #[test]
3116 fn failed_certificate_delivery_reservation_is_removed() {
3117 let path = bootstrap_cert_path("failed-reservation");
3118 let bootstrap = BootstrapConfig {
3119 cert_out: Some(path.clone()),
3120 ..BootstrapConfig::default()
3121 };
3122
3123 let output = open_bootstrap_certificate_output(&bootstrap).expect("reserve output");
3124 assert!(path.exists());
3125 #[cfg(unix)]
3126 {
3127 use std::os::unix::fs::PermissionsExt;
3128 let mode = std::fs::metadata(&path)
3129 .expect("certificate metadata")
3130 .permissions()
3131 .mode()
3132 & 0o777;
3133 assert_eq!(mode, 0o600);
3134 }
3135 drop(output);
3136 assert!(!path.exists());
3137 }
3138
3139 #[test]
3140 fn configured_vault_credentials_do_not_require_certificate_output() {
3141 use crate::storage::engine::pager::{Pager, PagerConfig};
3142
3143 let config = crate::auth::AuthConfig {
3144 enabled: true,
3145 require_auth: true,
3146 vault_enabled: true,
3147 ..crate::auth::AuthConfig::default()
3148 };
3149
3150 let passphrase_dir = bootstrap_cert_path("passphrase-vault");
3151 std::fs::create_dir_all(&passphrase_dir).expect("passphrase temp directory");
3152 let passphrase_pager = Arc::new(
3153 Pager::open(passphrase_dir.join("test.rdb"), PagerConfig::default())
3154 .expect("passphrase pager"),
3155 );
3156 let passphrase_store = Arc::new(
3157 AuthStore::with_vault_passphrase(
3158 config.clone(),
3159 Arc::clone(&passphrase_pager),
3160 "test-passphrase",
3161 )
3162 .expect("passphrase vault"),
3163 );
3164 assert!(!passphrase_store.bootstrap_requires_certificate_delivery());
3165 bootstrap_with_certificate_output(&passphrase_store, "admin", "secret", &mut None)
3166 .expect("passphrase bootstrap without certificate output");
3167 drop(passphrase_store);
3168 drop(passphrase_pager);
3169 let _ = std::fs::remove_dir_all(passphrase_dir);
3170
3171 let certificate_dir = bootstrap_cert_path("certificate-vault");
3172 std::fs::create_dir_all(&certificate_dir).expect("certificate temp directory");
3173 let certificate_pager = Arc::new(
3174 Pager::open(certificate_dir.join("test.rdb"), PagerConfig::default())
3175 .expect("certificate pager"),
3176 );
3177 let certificate = "11".repeat(32);
3178 let certificate_store = Arc::new(
3179 AuthStore::with_vault_certificate(config, Arc::clone(&certificate_pager), &certificate)
3180 .expect("certificate vault"),
3181 );
3182 assert!(!certificate_store.bootstrap_requires_certificate_delivery());
3183 bootstrap_with_certificate_output(&certificate_store, "admin", "secret", &mut None)
3184 .expect("certificate bootstrap without duplicate output");
3185 drop(certificate_store);
3186 drop(certificate_pager);
3187 let _ = std::fs::remove_dir_all(certificate_dir);
3188 }
3189
3190 #[test]
3191 fn required_auth_simple_preset_waits_for_durable_admin() {
3192 let _guard = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3193 let runtime = RedDBRuntime::with_options(RedDBOptions::in_memory()).expect("runtime");
3194 let auth_store = Arc::new(AuthStore::new(crate::auth::AuthConfig {
3195 enabled: true,
3196 require_auth: true,
3197 vault_enabled: true,
3198 ..crate::auth::AuthConfig::default()
3199 }));
3200
3201 apply_preset(&runtime, &auth_store).expect("pending simple preset");
3202 assert!(runtime
3203 .db()
3204 .store()
3205 .try_get_config(BOOTSTRAP_COMPLETED_KEY)
3206 .expect("completion lookup")
3207 .is_none());
3208 }
3209
3210 #[test]
3211 fn server_options_preserve_explicit_disk_residency() {
3212 let _guard = no_auth_env_lock().lock().unwrap();
3213 let mut config = no_auth_test_config(true);
3214 assert!(!config.to_db_options().unwrap().bounded_disk_residency);
3215 config.bounded_disk_residency = true;
3216 assert!(config.to_db_options().unwrap().bounded_disk_residency);
3217 }
3218
3219 fn no_auth_test_config(no_auth: bool) -> ServerCommandConfig {
3220 ServerCommandConfig {
3221 path: None,
3222 router_bind_addr: Some(DEFAULT_ROUTER_BIND_ADDR.to_string()),
3223 router_bind_explicit: false,
3224 grpc_bind_addr: None,
3225 grpc_bind_explicit: false,
3226 grpc_tls_bind_addr: None,
3227 grpc_tls_cert: None,
3228 grpc_tls_key: None,
3229 grpc_tls_client_ca: None,
3230 http_bind_addr: None,
3231 http_bind_explicit: false,
3232 http_tls_bind_addr: None,
3233 http_tls_cert: None,
3234 http_tls_key: None,
3235 http_tls_client_ca: None,
3236 wire_bind_addr: None,
3237 wire_bind_explicit: false,
3238 wire_tls_bind_addr: None,
3239 wire_tls_cert: None,
3240 wire_tls_key: None,
3241 pg_bind_addr: None,
3242 create_if_missing: true,
3243 bounded_disk_residency: false,
3244 read_only: false,
3245 role: "standalone".to_string(),
3246 primary_addr: None,
3247 storage_profile: StorageProfileSelection::embedded_single_file(),
3248 auth: false,
3249 require_auth: false,
3250 vault: true,
3253 no_auth,
3254 workers: None,
3255 telemetry: None,
3256 http_limits_cli: crate::server::HttpLimitsCliInput::default(),
3257 ui: false,
3258 ui_dir: None,
3259 bootstrap: BootstrapConfig::default(),
3260 }
3261 }
3262
3263 #[test]
3264 fn no_auth_flag_disables_every_auth_knob_and_stamps_metadata() {
3265 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3266 unsafe {
3271 std::env::set_var("REDDB_USERNAME", "admin");
3272 std::env::set_var("REDDB_PASSWORD", "hunter2");
3273 }
3274 let config = no_auth_test_config(true);
3275 let options = config.to_db_options().expect("to_db_options");
3276
3277 assert!(no_auth_active(&options), "metadata should be stamped");
3278 assert!(!options.auth.enabled, "auth.enabled must be forced off");
3279 assert!(
3280 !options.auth.require_auth,
3281 "require_auth must be forced off"
3282 );
3283 assert!(
3284 !options.auth.vault_enabled,
3285 "vault_enabled must be forced off (overrides --vault)"
3286 );
3287 assert_eq!(
3288 options.metadata.get(NO_AUTH_META).map(String::as_str),
3289 Some("true"),
3290 );
3291
3292 unsafe {
3294 std::env::remove_var("REDDB_USERNAME");
3295 std::env::remove_var("REDDB_PASSWORD");
3296 }
3297 }
3298
3299 #[test]
3300 fn default_behaviour_without_no_auth_flag_is_unchanged() {
3301 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3302 let config = no_auth_test_config(false);
3303 let options = config.to_db_options().expect("to_db_options");
3304
3305 assert!(
3306 !no_auth_active(&options),
3307 "default boot must not be marked no-auth"
3308 );
3309 assert!(
3310 options.metadata.get(NO_AUTH_META).is_none(),
3311 "metadata key must be absent when flag is off"
3312 );
3313 assert!(options.auth.vault_enabled);
3315 }
3316
3317 #[test]
3318 fn no_auth_active_blocks_bootstrap_from_env() {
3319 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3320 unsafe {
3325 std::env::set_var("REDDB_USERNAME", "admin");
3326 std::env::set_var("REDDB_PASSWORD", "hunter2");
3327 }
3328
3329 let options = no_auth_test_config(true)
3330 .to_db_options()
3331 .expect("to_db_options");
3332
3333 let auth_store = AuthStore::new(options.auth.clone());
3337 if !no_auth_active(&options) {
3338 auth_store.bootstrap_from_env();
3339 }
3340
3341 assert!(
3342 auth_store.needs_bootstrap(),
3343 "no admin user must be bootstrapped under --no-auth even with REDDB_USERNAME/PASSWORD set"
3344 );
3345
3346 unsafe {
3348 std::env::remove_var("REDDB_USERNAME");
3349 std::env::remove_var("REDDB_PASSWORD");
3350 }
3351 }
3352
3353 fn clear_preset_env() {
3360 unsafe {
3362 std::env::remove_var(BOOTSTRAP_PRESET_ENV);
3363 std::env::remove_var(PRESET_ENV);
3364 std::env::remove_var("REDDB_BOOTSTRAP_MANIFEST");
3365 std::env::remove_var("REDDB_AUTH");
3366 std::env::remove_var("REDDB_REQUIRE_AUTH");
3367 std::env::remove_var("REDDB_NO_AUTH");
3368 std::env::remove_var("REDDB_DEV");
3369 std::env::remove_var("REDDB_VAULT");
3370 std::env::remove_var("REDDB_USERNAME");
3371 std::env::remove_var("REDDB_PASSWORD");
3372 std::env::remove_var("REDDB_USERNAME_FILE");
3373 std::env::remove_var("REDDB_PASSWORD_FILE");
3374 std::env::remove_var(BOOTSTRAP_CERT_OUT_ENV);
3375 std::env::remove_var("REDDB_CLOUD_HEAD_ADMIN");
3376 std::env::remove_var("REDDB_CLOUD_HEAD_ADMIN_PASSWORD");
3377 std::env::remove_var("REDDB_CLOUD_HEAD_ADMIN_PASSWORD_FILE");
3378 std::env::remove_var("REDDB_CUSTOMER_ADMIN");
3379 std::env::remove_var("REDDB_CUSTOMER_ADMIN_PASSWORD");
3380 std::env::remove_var("REDDB_CUSTOMER_ADMIN_PASSWORD_FILE");
3381 }
3382 }
3383
3384 fn clear_backup_env() {
3385 unsafe {
3387 std::env::remove_var("REDDB_BACKUP_S3_ENDPOINT");
3388 std::env::remove_var("REDDB_BACKUP_S3_BUCKET");
3389 std::env::remove_var("REDDB_BACKUP_S3_PREFIX");
3390 std::env::remove_var("REDDB_BACKUP_S3_ACCESS_KEY_ID");
3391 std::env::remove_var("REDDB_BACKUP_S3_SECRET_ACCESS_KEY");
3392 std::env::remove_var("REDDB_BACKUP_S3_REGION");
3393 std::env::remove_var("REDDB_BACKUP_CHECKPOINT_INTERVAL_SECS");
3394 std::env::remove_var("REDDB_BACKUP_WAL_FLUSH_INTERVAL_SECS");
3395 std::env::remove_var("REDDB_BACKUP_PAUSE_ON_LAG_SECS");
3396 }
3397 }
3398
3399 fn fresh_runtime_and_store() -> (RedDBRuntime, Arc<AuthStore>) {
3400 let runtime = RedDBRuntime::with_options(RedDBOptions::in_memory()).expect("runtime");
3401 let auth_store = Arc::new(AuthStore::new(crate::auth::AuthConfig::default()));
3402 (runtime, auth_store)
3403 }
3404
3405 #[test]
3406 fn auth_env_knobs_enable_auth_require_auth_and_vault() {
3407 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3408 clear_preset_env();
3409 unsafe {
3411 std::env::set_var("REDDB_AUTH", "true");
3412 std::env::set_var("REDDB_REQUIRE_AUTH", "true");
3413 std::env::set_var("REDDB_VAULT", "true");
3414 }
3415
3416 let mut config = no_auth_test_config(false);
3417 config.vault = false;
3418 let options = config.to_db_options().expect("to_db_options");
3419
3420 assert!(options.auth.enabled);
3421 assert!(options.auth.require_auth);
3422 assert!(options.auth.vault_enabled);
3423
3424 clear_preset_env();
3425 }
3426
3427 #[test]
3428 fn production_and_cloud_presets_force_auth_require_auth_and_vault() {
3429 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3430 clear_preset_env();
3431
3432 for preset in [PRESET_PRODUCTION, PRESET_CLOUD] {
3433 let mut config = no_auth_test_config(false);
3434 config.vault = false;
3435 config.bootstrap.preset = Some(preset.to_string());
3436
3437 let options = config.to_db_options().expect("to_db_options");
3438 assert!(options.auth.enabled, "{preset} should enable auth");
3439 assert!(
3440 options.auth.require_auth,
3441 "{preset} should require authenticated requests"
3442 );
3443 assert!(options.auth.vault_enabled, "{preset} should enable vault");
3444 assert!(!no_auth_active(&options));
3445 }
3446
3447 clear_preset_env();
3448 }
3449
3450 #[test]
3451 fn no_auth_env_overrides_preset_forced_auth() {
3452 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3453 clear_preset_env();
3454 unsafe {
3456 std::env::set_var("REDDB_NO_AUTH", "true");
3457 std::env::set_var(BOOTSTRAP_PRESET_ENV, PRESET_CLOUD);
3458 }
3459
3460 let mut config = no_auth_test_config(false);
3461 config.auth = true;
3462 config.require_auth = true;
3463 let options = config.to_db_options().expect("to_db_options");
3464
3465 assert!(no_auth_active(&options));
3466 assert!(!options.auth.enabled);
3467 assert!(!options.auth.require_auth);
3468 assert!(!options.auth.vault_enabled);
3469
3470 clear_preset_env();
3471 }
3472
3473 #[test]
3474 fn simple_preset_is_default_and_persists_state() {
3475 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3476 clear_preset_env();
3477
3478 let (runtime, auth_store) = fresh_runtime_and_store();
3479 apply_preset(&runtime, &auth_store).expect("simple preset applies cleanly");
3480
3481 assert!(
3483 auth_store.needs_bootstrap(),
3484 "simple preset must not create an admin"
3485 );
3486
3487 let store = runtime.db().store();
3489 let completed = store
3490 .get_config(BOOTSTRAP_COMPLETED_KEY)
3491 .expect("completed key persisted");
3492 assert!(matches!(completed, reddb_types::Value::Boolean(true)));
3493 let preset = store
3494 .get_config(BOOTSTRAP_PRESET_KEY)
3495 .expect("preset key persisted");
3496 match preset {
3497 reddb_types::Value::Text(s) => assert_eq!(s.as_ref(), PRESET_SIMPLE),
3498 other => panic!("expected Text(simple), got {other:?}"),
3499 }
3500 assert!(
3501 store.get_config(BOOTSTRAP_FIRST_ADMIN_KEY).is_none(),
3502 "simple preset must not record a first admin"
3503 );
3504
3505 clear_preset_env();
3506 }
3507
3508 #[test]
3509 fn production_preset_creates_first_admin_with_allow_all_policy() {
3510 use crate::auth::policies::{EvalContext, ResourceRef};
3511 use crate::auth::UserId;
3512
3513 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3514 clear_preset_env();
3515 let cert_out = bootstrap_cert_path("production-admin");
3516 unsafe {
3518 std::env::set_var(PRESET_ENV, PRESET_PRODUCTION);
3519 std::env::set_var("REDDB_USERNAME", "ops");
3520 std::env::set_var("REDDB_PASSWORD", "hunter2");
3521 std::env::set_var(BOOTSTRAP_CERT_OUT_ENV, &cert_out);
3522 }
3523
3524 let (runtime, auth_store) = fresh_runtime_and_store();
3525 apply_preset(&runtime, &auth_store).expect("production preset applies cleanly");
3526
3527 assert!(
3529 !auth_store.needs_bootstrap(),
3530 "production preset must seal bootstrap"
3531 );
3532 let users = auth_store.list_users();
3533 assert_eq!(users.len(), 1);
3534 let admin = &users[0];
3535 assert_eq!(admin.username, "ops");
3536 assert!(
3537 admin.tenant_id.is_none(),
3538 "first admin must be platform-scoped (tenant=None)"
3539 );
3540
3541 let policy = auth_store
3543 .get_policy(FIRST_ADMIN_ALLOW_ALL_POLICY)
3544 .expect("allow-all policy installed");
3545 assert!(!policy.statements.is_empty());
3546
3547 let actor = UserId::platform("ops");
3550 let ctx = EvalContext {
3551 principal_tenant: None,
3552 current_tenant: None,
3553 peer_ip: None,
3554 mfa_present: false,
3555 now_ms: 1_700_000_000_000,
3556 principal_is_admin_role: true,
3557 principal_is_platform_scoped: true,
3558 };
3559 let arbitrary_resource = ResourceRef::new("config", "red.config.audit.enabled");
3560 assert!(
3561 auth_store.check_policy_authz(&actor, "config:write", &arbitrary_resource, &ctx),
3562 "allow-all policy must grant arbitrary actions via the evaluator"
3563 );
3564
3565 let store = runtime.db().store();
3567 match store
3568 .get_config(BOOTSTRAP_FIRST_ADMIN_KEY)
3569 .expect("first_admin_id persisted")
3570 {
3571 reddb_types::Value::Text(s) => assert_eq!(s.as_ref(), "ops"),
3572 other => panic!("expected Text(ops), got {other:?}"),
3573 }
3574 match store.get_config(BOOTSTRAP_PRESET_KEY).unwrap() {
3575 reddb_types::Value::Text(s) => assert_eq!(s.as_ref(), PRESET_PRODUCTION),
3576 other => panic!("expected Text(production), got {other:?}"),
3577 }
3578
3579 clear_preset_env();
3580 let _ = std::fs::remove_file(cert_out);
3581 }
3582
3583 #[test]
3584 fn bootstrap_preset_env_takes_precedence_over_legacy_preset_env() {
3585 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3586 clear_preset_env();
3587 unsafe {
3589 std::env::set_var(BOOTSTRAP_PRESET_ENV, PRESET_REGULATED);
3590 std::env::set_var(PRESET_ENV, PRESET_SIMPLE);
3591 }
3592
3593 let options = no_auth_test_config(false)
3594 .to_db_options()
3595 .expect("regulated options");
3596 assert!(
3597 options.control_events.compliance_mode,
3598 "canonical REDDB_BOOTSTRAP_PRESET should win over REDDB_PRESET"
3599 );
3600 assert!(options.query_audit.enabled);
3601
3602 clear_preset_env();
3603 }
3604
3605 #[test]
3606 fn cloud_preset_creates_system_head_and_customer_admins() {
3607 use crate::auth::policies::{EvalContext, ResourceRef};
3608 use crate::auth::UserId;
3609
3610 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3611 clear_preset_env();
3612 let cert_out = bootstrap_cert_path("cloud-admins");
3613 unsafe {
3615 std::env::set_var(BOOTSTRAP_PRESET_ENV, PRESET_CLOUD);
3616 std::env::set_var("REDDB_CLOUD_HEAD_ADMIN", "head");
3617 std::env::set_var("REDDB_CLOUD_HEAD_ADMIN_PASSWORD", "head-pass");
3618 std::env::set_var("REDDB_CUSTOMER_ADMIN", "customer");
3619 std::env::set_var("REDDB_CUSTOMER_ADMIN_PASSWORD", "customer-pass");
3620 std::env::set_var(BOOTSTRAP_CERT_OUT_ENV, &cert_out);
3621 }
3622
3623 let (runtime, auth_store) = fresh_runtime_and_store();
3624 apply_preset(&runtime, &auth_store).expect("cloud preset applies cleanly");
3625
3626 let head = auth_store
3627 .get_user(None, "head")
3628 .expect("head admin should exist");
3629 assert_eq!(head.tenant_id, None);
3630 let customer = auth_store
3631 .get_user(None, "customer")
3632 .expect("customer admin should exist");
3633 assert_eq!(customer.tenant_id, None);
3634
3635 let ctx = EvalContext {
3636 principal_tenant: None,
3637 current_tenant: None,
3638 peer_ip: None,
3639 mfa_present: false,
3640 now_ms: 1_700_000_000_000,
3641 principal_is_admin_role: true,
3642 principal_is_platform_scoped: true,
3643 };
3644 assert!(auth_store.check_policy_authz(
3645 &UserId::platform("customer"),
3646 "config:write",
3647 &ResourceRef::new("config", "red.config.customer.enabled"),
3648 &ctx,
3649 ));
3650 assert!(
3651 auth_store.check_policy_authz(
3652 &UserId::platform("head"),
3653 "config:write",
3654 &ResourceRef::new("config", "red.config.cloud.enabled"),
3655 &ctx,
3656 ),
3657 "cloud head keeps allow-all authority over managed cloud config"
3658 );
3659 assert!(
3660 !auth_store.check_policy_authz(
3661 &UserId::platform("customer"),
3662 "config:write",
3663 &ResourceRef::new("config", "red.config.cloud.enabled"),
3664 &ctx,
3665 ),
3666 "cloud customer must be denied managed cloud config writes"
3667 );
3668 assert!(
3669 !auth_store.check_policy_authz(
3670 &UserId::platform("customer"),
3671 "policy:drop",
3672 &ResourceRef::new("policy", CLOUD_PROTECT_MANAGED_POLICY),
3673 &ctx,
3674 ),
3675 "cloud customer must be denied mutations of the managed guardrail policy"
3676 );
3677
3678 assert!(auth_store.get_user(None, "head").is_some());
3679 assert!(auth_store
3680 .get_policy(CLOUD_PROTECT_MANAGED_POLICY)
3681 .is_some());
3682 assert!(runtime
3683 .config_registry()
3684 .get_active(CLOUD_CONFIG_NAMESPACE)
3685 .is_some());
3686
3687 let store = runtime.db().store();
3688 match store
3689 .get_config(BOOTSTRAP_FIRST_ADMIN_KEY)
3690 .expect("head admin id persisted")
3691 {
3692 reddb_types::Value::Text(s) => assert_eq!(s.as_ref(), "head"),
3693 other => panic!("expected Text(head), got {other:?}"),
3694 }
3695 match store.get_config(BOOTSTRAP_PRESET_KEY).unwrap() {
3696 reddb_types::Value::Text(s) => assert_eq!(s.as_ref(), PRESET_CLOUD),
3697 other => panic!("expected Text(cloud), got {other:?}"),
3698 }
3699
3700 clear_preset_env();
3701 let _ = std::fs::remove_file(cert_out);
3702 }
3703
3704 #[test]
3705 fn cloud_preset_customer_cannot_disable_head_admin_or_drop_guardrail_policy() {
3706 use crate::auth::Role;
3707 use crate::runtime::mvcc::{clear_current_auth_identity, set_current_auth_identity};
3708
3709 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3710 clear_preset_env();
3711
3712 let bootstrap = BootstrapConfig {
3713 preset: Some(PRESET_CLOUD.to_string()),
3714 cloud_head_admin: Some("red_admin".to_string()),
3715 cloud_head_admin_password: Some("head-pass".to_string()),
3716 customer_admin: Some("admin".to_string()),
3717 customer_admin_password: Some("customer-pass".to_string()),
3718 cert_out: Some(bootstrap_cert_path("cloud-guardrail")),
3719 ..BootstrapConfig::default()
3720 };
3721 let (runtime, auth_store) = fresh_runtime_and_store();
3722 apply_preset_from_config(&runtime, &auth_store, &bootstrap)
3723 .expect("cloud preset applies cleanly");
3724 runtime.set_auth_store(Arc::clone(&auth_store));
3725
3726 set_current_auth_identity("admin".to_string(), Role::Admin);
3727 let disable_head = runtime.execute_query("ALTER USER red_admin DISABLE");
3728 let drop_guardrail =
3729 runtime.execute_query(&format!("DROP POLICY '{}'", CLOUD_PROTECT_MANAGED_POLICY));
3730 let create_user = runtime.execute_query("CREATE USER app_user WITH PASSWORD 'p' ROLE read");
3731 let disable_user = runtime.execute_query("ALTER USER app_user DISABLE");
3732 clear_current_auth_identity();
3733
3734 let disable_head_err = disable_head.expect_err("customer admin must not disable red_admin");
3735 assert!(
3736 disable_head_err.to_string().contains("user:disable"),
3737 "error should name the denied lifecycle action: {disable_head_err}"
3738 );
3739 assert!(
3740 auth_store
3741 .get_user(None, "red_admin")
3742 .expect("red_admin should still exist")
3743 .enabled,
3744 "denied mutation must leave red_admin enabled"
3745 );
3746
3747 let drop_guardrail_err =
3748 drop_guardrail.expect_err("customer admin must not drop cloud guardrail policy");
3749 assert!(
3750 drop_guardrail_err.to_string().contains("managed policy"),
3751 "error should name the managed policy guardrail: {drop_guardrail_err}"
3752 );
3753 assert!(auth_store
3754 .get_policy(CLOUD_PROTECT_MANAGED_POLICY)
3755 .is_some());
3756
3757 create_user.expect("customer admin should retain normal create-user authority");
3758 disable_user.expect("customer admin should retain normal user-disable authority");
3759 assert!(
3760 !auth_store
3761 .get_user(None, "app_user")
3762 .expect("app_user should exist")
3763 .enabled,
3764 "normal capability should still apply outside the reserved head admin"
3765 );
3766
3767 let _ = bootstrap.cert_out.as_ref().map(std::fs::remove_file);
3768 clear_preset_env();
3769 }
3770
3771 #[test]
3772 fn cloud_preset_cli_admin_alias_wins_over_cloud_env_password() {
3773 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3774 clear_preset_env();
3775 unsafe {
3777 std::env::set_var("REDDB_CLOUD_HEAD_ADMIN_PASSWORD", "env-head-pass");
3778 }
3779
3780 let bootstrap = BootstrapConfig {
3781 preset: Some(PRESET_CLOUD.to_string()),
3782 admin_username: Some("head".to_string()),
3783 admin_password: Some("cli-head-pass".to_string()),
3784 customer_admin: Some("customer".to_string()),
3785 customer_admin_password: Some("customer-pass".to_string()),
3786 cert_out: Some(bootstrap_cert_path("cloud-alias")),
3787 ..BootstrapConfig::default()
3788 };
3789 let (runtime, auth_store) = fresh_runtime_and_store();
3790 apply_preset_from_config(&runtime, &auth_store, &bootstrap)
3791 .expect("cloud preset applies cleanly");
3792
3793 auth_store
3794 .authenticate("head", "cli-head-pass")
3795 .expect("CLI alias password should win");
3796 assert!(
3797 auth_store.authenticate("head", "env-head-pass").is_err(),
3798 "cloud-specific env password must not beat CLI alias"
3799 );
3800
3801 let _ = bootstrap.cert_out.as_ref().map(std::fs::remove_file);
3802 clear_preset_env();
3803 }
3804
3805 #[test]
3806 fn bootstrap_intent_manifest_matches_cloud_flag_path() {
3807 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3808 clear_preset_env();
3809
3810 let manifest_dir = std::env::current_dir()
3811 .expect("current dir")
3812 .join(".red/tmp/bootstrap-manifest-tests");
3813 std::fs::create_dir_all(&manifest_dir).expect("create manifest test dir");
3814 let unique = format!(
3815 "{}-{}",
3816 std::process::id(),
3817 std::time::SystemTime::now()
3818 .duration_since(std::time::UNIX_EPOCH)
3819 .unwrap_or_default()
3820 .as_millis()
3821 );
3822 let head_password = manifest_dir.join(format!("head-{unique}.secret"));
3823 let customer_password = manifest_dir.join(format!("customer-{unique}.secret"));
3824 let manifest_path = manifest_dir.join(format!("intent-{unique}.json"));
3825 std::fs::write(&head_password, "head-pass\n").expect("write head secret");
3826 std::fs::write(&customer_password, "customer-pass\n").expect("write customer secret");
3827 std::fs::write(
3828 &manifest_path,
3829 format!(
3830 r#"{{
3831 "bootstrap": {{
3832 "preset": "cloud",
3833 "cloud_head_admin": {{
3834 "username": "head",
3835 "password_file": "{}"
3836 }},
3837 "customer_admin": {{
3838 "username": "customer",
3839 "password_file": "{}"
3840 }}
3841 }}
3842 }}"#,
3843 head_password.display(),
3844 customer_password.display()
3845 ),
3846 )
3847 .expect("write manifest");
3848
3849 let mut config = no_auth_test_config(false);
3850 config.bootstrap.manifest = Some(manifest_path.clone());
3851 let options = config.to_db_options().expect("manifest options");
3852 assert!(options.auth.enabled);
3853 assert!(options.auth.require_auth);
3854 assert!(options.auth.vault_enabled);
3855
3856 let (runtime, auth_store) = fresh_runtime_and_store();
3857 apply_preset_from_config(&runtime, &auth_store, &config.bootstrap)
3858 .expect("manifest intent applies cleanly");
3859
3860 auth_store
3861 .authenticate("head", "head-pass")
3862 .expect("head password comes from file");
3863 auth_store
3864 .authenticate("customer", "customer-pass")
3865 .expect("customer password comes from file");
3866 assert!(auth_store.get_user(None, "head").is_some());
3867 assert!(auth_store.get_user(None, "customer").is_some());
3868 assert!(auth_store
3869 .get_policy(CLOUD_PROTECT_MANAGED_POLICY)
3870 .is_some());
3871 match runtime
3872 .db()
3873 .store()
3874 .get_config(BOOTSTRAP_PRESET_KEY)
3875 .expect("preset key persisted")
3876 {
3877 reddb_types::Value::Text(s) => assert_eq!(s.as_ref(), PRESET_CLOUD),
3878 other => panic!("expected Text(cloud), got {other:?}"),
3879 }
3880
3881 apply_preset_from_config(&runtime, &auth_store, &config.bootstrap)
3882 .expect("second boot is idempotent");
3883
3884 let _ = std::fs::remove_file(&manifest_path);
3885 let _ = std::fs::remove_file(&head_password);
3886 let _ = std::fs::remove_file(&customer_password);
3887 clear_preset_env();
3888 }
3889
3890 #[test]
3891 fn regulated_preset_enables_query_audit_infrastructure_without_rules() {
3892 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3893 clear_preset_env();
3894 unsafe {
3896 std::env::set_var(PRESET_ENV, PRESET_REGULATED);
3897 }
3898
3899 let (runtime, auth_store) = fresh_runtime_and_store();
3900 apply_preset(&runtime, &auth_store).expect("regulated preset applies cleanly");
3901
3902 assert!(runtime.query_audit().is_enabled());
3903 assert!(runtime.query_audit().rules().is_empty());
3904 assert!(
3905 runtime
3906 .db()
3907 .store()
3908 .get_collection(crate::runtime::query_audit::QUERY_AUDIT_COLLECTION)
3909 .is_some(),
3910 "regulated preset should create the query-audit stream"
3911 );
3912
3913 runtime
3914 .execute_query("CREATE TABLE docs (id INT)")
3915 .expect("create table");
3916 runtime
3917 .execute_query("INSERT INTO docs (id) VALUES (1)")
3918 .expect("insert");
3919 runtime.execute_query("SELECT * FROM docs").expect("select");
3920 let rows = runtime
3921 .db()
3922 .store()
3923 .get_collection(crate::runtime::query_audit::QUERY_AUDIT_COLLECTION)
3924 .expect("query audit collection")
3925 .query_all(|_| true);
3926 assert!(
3927 rows.is_empty(),
3928 "regulated preset must not globally audit every query"
3929 );
3930
3931 clear_preset_env();
3932 }
3933
3934 #[test]
3935 fn managed_backup_env_rejects_primary_replica_single_file_storage() {
3936 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3937 clear_backup_env();
3938 unsafe {
3940 std::env::set_var("REDDB_BACKUP_S3_ENDPOINT", "https://s3.example.test");
3941 std::env::set_var("REDDB_BACKUP_S3_BUCKET", "reddb");
3942 std::env::set_var("REDDB_BACKUP_S3_PREFIX", "clusters/prod");
3943 std::env::set_var("REDDB_BACKUP_S3_ACCESS_KEY_ID", "AK");
3944 std::env::set_var("REDDB_BACKUP_S3_SECRET_ACCESS_KEY", "SK");
3945 }
3946
3947 let mut config = no_auth_test_config(false);
3948 config.role = "primary".to_string();
3949 config.storage_profile = crate::storage::StorageDeployPreset::PrimaryReplicaDev.selection();
3950
3951 let err = config.to_db_options().unwrap_err();
3952 assert!(err.contains("managed backup"), "got: {err}");
3953 assert!(err.contains("operational-directory"), "got: {err}");
3954
3955 clear_backup_env();
3956 }
3957
3958 #[test]
3959 fn regulated_preset_installs_managed_evidence_guardrails_end_to_end() {
3960 use crate::auth::policies::{EvalContext, Policy, ResourceRef};
3961 use crate::auth::store::PrincipalRef;
3962 use crate::auth::{Role, UserId};
3963 use crate::runtime::mvcc::{clear_current_auth_identity, set_current_auth_identity};
3964 use reddb_types::Value;
3965
3966 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
3967 clear_preset_env();
3968 unsafe {
3970 std::env::set_var(PRESET_ENV, PRESET_REGULATED);
3971 }
3972
3973 let options = no_auth_test_config(false)
3974 .to_db_options()
3975 .expect("regulated options");
3976 assert!(
3977 options.control_events.compliance_mode,
3978 "regulated preset must enable fail-closed control evidence before runtime boot"
3979 );
3980 assert!(
3981 options.query_audit.enabled && options.query_audit.rules.is_empty(),
3982 "regulated preset must enable query-audit infrastructure without global rules"
3983 );
3984
3985 let runtime = RedDBRuntime::with_options(options).expect("runtime");
3986 let auth_store = Arc::new(AuthStore::new(crate::auth::AuthConfig::default()));
3987 apply_preset(&runtime, &auth_store).expect("regulated preset applies cleanly");
3988 runtime.set_auth_store(Arc::clone(&auth_store));
3989
3990 assert!(runtime.control_events_require_persistence());
3991 assert!(runtime.query_audit().is_enabled());
3992 assert!(runtime.query_audit().rules().is_empty());
3993 assert!(auth_store
3994 .get_policy(REGULATED_PROTECT_MANAGED_POLICY)
3995 .is_some());
3996
3997 let managed_policy = runtime
3998 .config_registry()
3999 .get_active(REGULATED_PROTECT_MANAGED_POLICY)
4000 .expect("regulated managed policy registry entry");
4001 assert!(managed_policy.managed);
4002 assert_eq!(managed_policy.resource_type, "policy");
4003 assert!(
4004 runtime
4005 .config_registry()
4006 .get_active(REGULATED_AUDIT_CONFIG_NAMESPACE)
4007 .expect("regulated audit config namespace")
4008 .managed
4009 );
4010
4011 let registry_rows = runtime
4012 .execute_query(&format!(
4013 "SELECT id, managed FROM red.registry WHERE id = '{}'",
4014 REGULATED_PROTECT_MANAGED_POLICY
4015 ))
4016 .expect("red.registry query");
4017 assert_eq!(registry_rows.result.records.len(), 1);
4018 assert_eq!(
4019 registry_rows.result.records[0].get("managed"),
4020 Some(&Value::Boolean(true))
4021 );
4022
4023 let managed_policy_rows = runtime
4024 .execute_query(&format!(
4025 "SELECT policy_id FROM red.managed_policies WHERE policy_id = '{}'",
4026 REGULATED_PROTECT_MANAGED_POLICY
4027 ))
4028 .expect("red.managed_policies query");
4029 assert_eq!(managed_policy_rows.result.records.len(), 1);
4030
4031 let capability_rows = runtime
4032 .execute_query(
4033 "SELECT action FROM red.control_capabilities WHERE action = 'evidence:export'",
4034 )
4035 .expect("red.control_capabilities query");
4036 assert_eq!(capability_rows.result.records.len(), 1);
4037
4038 auth_store
4039 .create_user("alice", "p", Role::Admin)
4040 .expect("create ordinary admin");
4041 let allow_all = Policy::from_json_str(
4042 r#"{
4043 "id": "alice-allow-all",
4044 "version": 1,
4045 "statements": [{
4046 "effect": "allow",
4047 "actions": ["*"],
4048 "resources": ["*"]
4049 }]
4050 }"#,
4051 )
4052 .expect("allow-all policy");
4053 auth_store.put_policy(allow_all).expect("install allow-all");
4054 auth_store
4055 .attach_policy(
4056 PrincipalRef::User(UserId::platform("alice")),
4057 "alice-allow-all",
4058 )
4059 .expect("attach allow-all");
4060 let ctx = EvalContext {
4061 principal_tenant: None,
4062 current_tenant: None,
4063 peer_ip: None,
4064 mfa_present: false,
4065 now_ms: 1_700_000_000_000,
4066 principal_is_admin_role: true,
4067 principal_is_platform_scoped: true,
4068 };
4069 assert!(
4070 !auth_store.check_policy_authz(
4071 &UserId::platform("alice"),
4072 "policy:drop",
4073 &ResourceRef::new("policy", REGULATED_PROTECT_MANAGED_POLICY),
4074 &ctx,
4075 ),
4076 "managed guardrail deny policy must be effective for ordinary admins"
4077 );
4078
4079 set_current_auth_identity("alice".to_string(), Role::Admin);
4080 let denied = runtime.execute_query(&format!(
4081 "DROP POLICY '{}'",
4082 REGULATED_PROTECT_MANAGED_POLICY
4083 ));
4084 clear_current_auth_identity();
4085 let err = denied.expect_err("managed policy guardrail must deny ordinary admin");
4086 assert!(
4087 err.to_string().contains("managed policy"),
4088 "error should name the managed guardrail: {err}"
4089 );
4090 assert!(
4091 auth_store
4092 .get_policy(REGULATED_PROTECT_MANAGED_POLICY)
4093 .is_some(),
4094 "denied mutation must leave managed policy installed"
4095 );
4096
4097 let denied_events = runtime
4098 .execute_query(&format!(
4099 "SELECT action, resource, outcome FROM red.control_events \
4100 WHERE action = 'policy:drop' AND resource = 'policy:{}'",
4101 REGULATED_PROTECT_MANAGED_POLICY
4102 ))
4103 .expect("red.control_events denied policy drop");
4104 assert_eq!(denied_events.result.records.len(), 1);
4105 assert_eq!(
4106 denied_events.result.records[0].get("outcome"),
4107 Some(&Value::text("denied"))
4108 );
4109
4110 set_current_auth_identity("alice".to_string(), Role::Admin);
4111 let config_denied = runtime.execute_query("SET CONFIG red.config.audit.enabled = true");
4112 clear_current_auth_identity();
4113 let err = config_denied.expect_err("managed config guardrail must deny ordinary admin");
4114 assert!(
4115 err.to_string().contains("managed config"),
4116 "error should name the managed config guardrail: {err}"
4117 );
4118
4119 let denied_config_events = runtime
4120 .execute_query(
4121 "SELECT action, resource, outcome FROM red.control_events \
4122 WHERE action = 'config:write' AND resource = 'config:red.config.audit.enabled'",
4123 )
4124 .expect("red.control_events denied config write");
4125 assert_eq!(denied_config_events.result.records.len(), 1);
4126 assert_eq!(
4127 denied_config_events.result.records[0].get("outcome"),
4128 Some(&Value::text("denied"))
4129 );
4130
4131 runtime
4132 .execute_query("CREATE TABLE regulated_docs (id INT)")
4133 .expect("create user table");
4134 runtime
4135 .execute_query("SELECT * FROM regulated_docs")
4136 .expect("select user table");
4137 let audit_rows = runtime
4138 .db()
4139 .store()
4140 .get_collection(crate::runtime::query_audit::QUERY_AUDIT_COLLECTION)
4141 .expect("query audit collection")
4142 .query_all(|_| true);
4143 assert!(
4144 audit_rows.is_empty(),
4145 "regulated preset must not globally audit data-plane queries"
4146 );
4147
4148 clear_preset_env();
4149 }
4150
4151 #[test]
4152 fn bootstrap_manifest_installs_initial_users_policies_guardrails_and_config() {
4153 use crate::auth::policies::{EvalContext, ResourceRef};
4154 use crate::auth::UserId;
4155 use reddb_types::Value;
4156
4157 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
4158 clear_preset_env();
4159
4160 let manifest_dir = std::env::current_dir()
4161 .expect("current dir")
4162 .join(".red/tmp/bootstrap-manifest-tests");
4163 std::fs::create_dir_all(&manifest_dir).expect("create manifest test dir");
4164 let manifest_path = manifest_dir.join(format!(
4165 "reddb-bootstrap-manifest-{}-{}.json",
4166 std::process::id(),
4167 std::time::SystemTime::now()
4168 .duration_since(std::time::UNIX_EPOCH)
4169 .unwrap_or_default()
4170 .as_millis()
4171 ));
4172 std::fs::write(
4173 &manifest_path,
4174 r#"{
4175 "users": [
4176 {
4177 "username": "ops",
4178 "password": "hunter2",
4179 "role": "admin"
4180 }
4181 ],
4182 "policies": [
4183 {
4184 "id": "bootstrap-registry-admin",
4185 "version": 1,
4186 "statements": [
4187 {
4188 "effect": "allow",
4189 "actions": ["red.registry:*", "policy:*", "config:write", "select"],
4190 "resources": ["registry:*", "policy:*", "config:*", "collection:docs"]
4191 }
4192 ]
4193 }
4194 ],
4195 "managed_policies": [
4196 {
4197 "id": "managed-deny-drop",
4198 "version": 1,
4199 "statements": [
4200 {
4201 "effect": "deny",
4202 "actions": ["policy:drop"],
4203 "resources": ["policy:managed-deny-drop"]
4204 }
4205 ],
4206 "required_resource": "policy:managed-deny-drop",
4207 "evidence": "full"
4208 }
4209 ],
4210 "attachments": [
4211 {"user": "ops", "policy": "bootstrap-registry-admin"}
4212 ],
4213 "managed_config_namespaces": [
4214 {
4215 "id": "red.ai",
4216 "required_action": "config:write",
4217 "required_resource": "config:red.ai.*",
4218 "evidence": "metadata"
4219 }
4220 ],
4221 "config": [
4222 {"key": "red.ai.default.provider", "value": "openai"},
4223 {
4224 "key": "red.ai.openai.default.secret_ref",
4225 "secret_ref": {"collection": "red.vault", "key": "openai"}
4226 }
4227 ],
4228 "actor": "ops"
4229 }"#,
4230 )
4231 .expect("write manifest");
4232 unsafe {
4234 std::env::set_var("REDDB_BOOTSTRAP_MANIFEST", &manifest_path);
4235 }
4236
4237 let (runtime, auth_store) = fresh_runtime_and_store();
4238 apply_preset(&runtime, &auth_store).expect("manifest applies cleanly");
4239
4240 let users = auth_store.list_users();
4241 assert_eq!(users.len(), 1);
4242 assert_eq!(users[0].username, "ops");
4243 assert!(users[0].tenant_id.is_none());
4244
4245 let actor = UserId::platform("ops");
4246 let ctx = EvalContext {
4247 principal_tenant: None,
4248 current_tenant: None,
4249 peer_ip: None,
4250 mfa_present: false,
4251 now_ms: 1_700_000_000_000,
4252 principal_is_admin_role: true,
4253 principal_is_platform_scoped: true,
4254 };
4255 assert!(auth_store.check_policy_authz(
4257 &actor,
4258 "select",
4259 &ResourceRef::new("collection", "docs"),
4260 &ctx
4261 ));
4262
4263 let managed_policy = runtime
4264 .config_registry()
4265 .get_active("managed-deny-drop")
4266 .expect("managed policy registry entry");
4267 assert!(managed_policy.managed);
4268 assert_eq!(managed_policy.resource_type, "policy");
4269 let managed_config = runtime
4270 .config_registry()
4271 .get_active("red.ai")
4272 .expect("managed config namespace registry entry");
4273 assert!(managed_config.managed);
4274 assert_eq!(managed_config.resource_type, "config_namespace");
4275
4276 let store = runtime.db().store();
4277 match store
4278 .get_config("red.ai.default.provider")
4279 .expect("plain config persisted")
4280 {
4281 Value::Text(s) => assert_eq!(s.as_ref(), "openai"),
4282 other => panic!("expected provider text, got {other:?}"),
4283 }
4284 let Value::Json(bytes) = store
4285 .get_config("red.ai.openai.default.secret_ref")
4286 .expect("secret ref config persisted")
4287 else {
4288 panic!("secret ref must be stored as structured JSON");
4289 };
4290 let reference: crate::serde_json::Value =
4291 crate::serde_json::from_slice(&bytes).expect("secret ref json");
4292 assert_eq!(
4293 reference.get("type").and_then(|v| v.as_str()),
4294 Some("secret_ref")
4295 );
4296 assert!(
4297 !String::from_utf8_lossy(&bytes).contains("hunter2"),
4298 "manifest password must not leak into secret ref config"
4299 );
4300
4301 let completed = store
4302 .get_config(BOOTSTRAP_COMPLETED_KEY)
4303 .expect("bootstrap completion persisted");
4304 assert!(matches!(completed, Value::Boolean(true)));
4305 assert!(
4306 store
4307 .get_config("system.bootstrap.manifest.registry_entries")
4308 .is_some(),
4309 "managed registry entries must be persisted internally"
4310 );
4311
4312 std::fs::remove_file(&manifest_path).expect("remove manifest after first boot");
4313 let restored_registry = Arc::new(crate::auth::registry::ConfigRegistry::new());
4314 crate::cli::bootstrap_manifest::rehydrate_manifest_registry(&runtime, &restored_registry)
4315 .expect("registry rehydrates without manifest file");
4316 assert!(restored_registry.get_active("managed-deny-drop").is_some());
4317 assert!(restored_registry.get_active("red.ai").is_some());
4318
4319 let fresh = Arc::new(AuthStore::new(crate::auth::AuthConfig::default()));
4320 apply_preset(&runtime, &fresh).expect("re-run must not need manifest file");
4321 assert!(fresh.needs_bootstrap());
4322
4323 clear_preset_env();
4324 }
4325
4326 #[test]
4327 fn production_preset_refuses_to_start_without_password() {
4328 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
4329 clear_preset_env();
4330 let cert_out = bootstrap_cert_path("production-missing-password");
4331 unsafe {
4333 std::env::set_var(PRESET_ENV, PRESET_PRODUCTION);
4334 std::env::set_var("REDDB_USERNAME", "ops");
4335 std::env::set_var(BOOTSTRAP_CERT_OUT_ENV, &cert_out);
4336 }
4337
4338 let (runtime, auth_store) = fresh_runtime_and_store();
4339 let err = apply_preset(&runtime, &auth_store).expect_err("must reject missing password");
4340 assert!(
4341 err.contains("REDDB_PASSWORD"),
4342 "error must name the missing env: {err}"
4343 );
4344
4345 assert!(auth_store.needs_bootstrap());
4347 assert!(runtime
4348 .db()
4349 .store()
4350 .get_config(BOOTSTRAP_COMPLETED_KEY)
4351 .is_none());
4352 assert!(!cert_out.exists());
4353
4354 clear_preset_env();
4355 }
4356
4357 #[test]
4358 fn re_running_production_after_first_boot_is_a_silent_skip() {
4359 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
4360 clear_preset_env();
4361 let cert_out = bootstrap_cert_path("production-rerun");
4362 unsafe {
4364 std::env::set_var(PRESET_ENV, PRESET_PRODUCTION);
4365 std::env::set_var("REDDB_USERNAME", "ops");
4366 std::env::set_var("REDDB_PASSWORD", "hunter2");
4367 std::env::set_var(BOOTSTRAP_CERT_OUT_ENV, &cert_out);
4368 }
4369
4370 let (runtime, auth_store) = fresh_runtime_and_store();
4371 apply_preset(&runtime, &auth_store).expect("first apply");
4372 assert_eq!(auth_store.list_users().len(), 1);
4373
4374 let fresh = Arc::new(AuthStore::new(crate::auth::AuthConfig::default()));
4381 apply_preset(&runtime, &fresh).expect("re-run is silent-skip");
4382 assert!(
4383 fresh.needs_bootstrap(),
4384 "re-run must not create a second admin"
4385 );
4386 assert!(
4387 fresh.get_policy(FIRST_ADMIN_ALLOW_ALL_POLICY).is_none(),
4388 "re-run must not re-install the allow-all policy on the fresh store"
4389 );
4390
4391 clear_preset_env();
4392 let _ = std::fs::remove_file(cert_out);
4393 }
4394
4395 #[test]
4396 fn unrecognised_preset_value_is_rejected() {
4397 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
4398 clear_preset_env();
4399 unsafe {
4401 std::env::set_var(PRESET_ENV, "weird");
4402 }
4403
4404 let (runtime, auth_store) = fresh_runtime_and_store();
4405 let err = apply_preset(&runtime, &auth_store).expect_err("must reject unknown preset");
4406 assert!(err.contains("weird"), "error must echo the value: {err}");
4407 assert!(auth_store.needs_bootstrap());
4408
4409 clear_preset_env();
4410 }
4411
4412 #[test]
4413 fn no_auth_short_circuits_preset_entirely() {
4414 let _g = no_auth_env_lock().lock().unwrap_or_else(|e| e.into_inner());
4415 clear_preset_env();
4416 unsafe {
4419 std::env::set_var(PRESET_ENV, PRESET_PRODUCTION);
4420 std::env::set_var("REDDB_USERNAME", "ops");
4421 std::env::set_var("REDDB_PASSWORD", "hunter2");
4422 }
4423
4424 let options = no_auth_test_config(true)
4425 .to_db_options()
4426 .expect("to_db_options");
4427 assert!(no_auth_active(&options));
4428
4429 let (runtime, auth_store) = fresh_runtime_and_store();
4432 if !no_auth_active(&options) {
4433 apply_preset(&runtime, &auth_store).expect("would apply preset");
4434 }
4435
4436 assert!(
4437 auth_store.needs_bootstrap(),
4438 "--no-auth must prevent any admin creation"
4439 );
4440 assert!(
4441 runtime
4442 .db()
4443 .store()
4444 .get_config(BOOTSTRAP_COMPLETED_KEY)
4445 .is_none(),
4446 "--no-auth must skip bootstrap-state persistence"
4447 );
4448
4449 clear_preset_env();
4450 }
4451
4452 #[test]
4453 fn implicit_bind_collision_degrades() {
4454 let held = TcpListener::bind("127.0.0.1:0").expect("hold test port");
4455 let addr = held.local_addr().expect("held addr").to_string();
4456 let mut readiness = TransportReadiness::default();
4457
4458 let listener =
4459 bind_listener_for_startup(&mut readiness, "http", &addr, false).expect("nonfatal");
4460
4461 assert!(listener.is_none());
4462 assert_eq!(readiness.active.len(), 0);
4463 assert_eq!(readiness.failed.len(), 1);
4464 assert!(!readiness.failed[0].explicit);
4465 assert_eq!(readiness.failed[0].bind_addr, addr);
4466 }
4467}