reddb_server/cli/
bootstrap.rs1use std::io::{BufRead, Write};
16use std::path::PathBuf;
17
18use crate::auth::store::AuthStore;
19use crate::auth::AuthConfig;
20use crate::{RedDBOptions, RedDBRuntime};
21
22pub struct BootstrapArgs {
26 pub path: PathBuf,
27 pub vault: bool,
28 pub username: String,
29 pub password: Option<String>,
32 pub password_stdin: bool,
33 pub print_certificate: bool,
34 pub json: bool,
35}
36
37#[derive(Debug)]
39pub struct BootstrapOutcome {
40 pub username: String,
41 pub api_key: String,
42 pub certificate: String,
43}
44
45pub fn run(args: BootstrapArgs) -> Result<BootstrapOutcome, String> {
49 if !args.vault {
50 return Err(
54 "bootstrap requires --vault (admin credentials must be sealed in the encrypted vault)"
55 .to_string(),
56 );
57 }
58
59 if std::env::var("REDDB_CERTIFICATE")
60 .ok()
61 .filter(|s| !s.is_empty())
62 .is_none()
63 && std::env::var("REDDB_VAULT_KEY")
64 .ok()
65 .filter(|s| !s.is_empty())
66 .is_none()
67 {
68 return Err("vault requires REDDB_CERTIFICATE or REDDB_VAULT_KEY (use the *_FILE companion to read from a mounted secret)".to_string());
69 }
70
71 if args.username.trim().is_empty() {
72 return Err(
73 "username is required (use --username, or set REDDB_USERNAME / REDDB_USERNAME_FILE)"
74 .to_string(),
75 );
76 }
77
78 let password = resolve_password(&args)?;
79 if password.is_empty() {
80 return Err("password is required (use --password-stdin or REDDB_PASSWORD_FILE)".into());
81 }
82
83 let opts = RedDBOptions::persistent(&args.path);
87 let runtime = RedDBRuntime::with_options(opts).map_err(|err| format!("open db: {err}"))?;
88
89 let pager = runtime
90 .db()
91 .store()
92 .pager()
93 .cloned()
94 .ok_or_else(|| "vault requires a paged database (persistent mode)".to_string())?;
95
96 let config = AuthConfig {
100 vault_enabled: true,
101 ..AuthConfig::default()
102 };
103
104 let store =
105 AuthStore::with_vault(config, pager, None).map_err(|err| format!("open vault: {err}"))?;
106
107 if !store.needs_bootstrap() {
108 let _ = runtime.checkpoint();
111 return Err("already bootstrapped — bootstrap is one-shot and irreversible".into());
112 }
113
114 let result = store
115 .bootstrap(&args.username, &password)
116 .map_err(|err| format!("bootstrap: {err}"))?;
117
118 let certificate = result.certificate.clone().ok_or_else(|| {
119 "bootstrap succeeded but no certificate was issued (vault not configured?)".to_string()
120 })?;
121 let api_key = result.api_key.key.clone();
122
123 drop(store);
131 drop(runtime);
132
133 Ok(BootstrapOutcome {
134 username: result.user.username,
135 api_key,
136 certificate,
137 })
138}
139
140fn resolve_password(args: &BootstrapArgs) -> Result<String, String> {
144 if args.password_stdin {
145 let mut buf = String::new();
146 let stdin = std::io::stdin();
147 stdin
148 .lock()
149 .read_line(&mut buf)
150 .map_err(|err| format!("read password from stdin: {err}"))?;
151 let trimmed = buf.trim_end_matches(['\n', '\r']).to_string();
154 return Ok(trimmed);
155 }
156 if let Some(p) = args.password.as_ref() {
157 let _ = writeln!(
158 std::io::stderr(),
159 "warning: --password leaks credentials to /proc/<pid>/cmdline; prefer --password-stdin or REDDB_PASSWORD_FILE"
160 );
161 return Ok(p.clone());
162 }
163 if let Ok(env_pwd) = std::env::var("REDDB_PASSWORD") {
164 if !env_pwd.is_empty() {
165 return Ok(env_pwd);
166 }
167 }
168 Ok(String::new())
169}
170
171pub fn render_success(outcome: &BootstrapOutcome, args: &BootstrapArgs) {
174 if args.json {
175 println!(
178 "{{\"username\":\"{}\",\"token\":\"{}\",\"certificate\":\"{}\"}}",
179 json_escape(&outcome.username),
180 json_escape(&outcome.api_key),
181 json_escape(&outcome.certificate),
182 );
183 return;
184 }
185 if args.print_certificate {
186 println!("{}", outcome.certificate);
188 return;
189 }
190 eprintln!(
191 "[reddb] bootstrapped admin user `{}` — SAVE THIS CERTIFICATE (only way to unseal):",
192 outcome.username
193 );
194 println!("{}", outcome.certificate);
195}
196
197fn json_escape(s: &str) -> String {
198 let mut out = String::with_capacity(s.len() + 2);
199 for c in s.chars() {
200 match c {
201 '"' => out.push_str("\\\""),
202 '\\' => out.push_str("\\\\"),
203 '\n' => out.push_str("\\n"),
204 '\r' => out.push_str("\\r"),
205 '\t' => out.push_str("\\t"),
206 c if (c as u32) < 0x20 => {
207 out.push_str(&format!("\\u{:04x}", c as u32));
208 }
209 c => out.push(c),
210 }
211 }
212 out
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 #[test]
220 fn vault_flag_required() {
221 let args = BootstrapArgs {
222 path: PathBuf::from("/tmp/reddb-bootstrap-test.rdb"),
223 vault: false,
224 username: "admin".into(),
225 password: Some("hunter2".into()),
226 password_stdin: false,
227 print_certificate: false,
228 json: false,
229 };
230 let err = run(args).unwrap_err();
231 assert!(err.contains("--vault"), "got: {err}");
232 }
233
234 #[test]
235 fn json_escape_handles_control_chars() {
236 assert_eq!(json_escape("a\"b"), "a\\\"b");
237 assert_eq!(json_escape("a\\b"), "a\\\\b");
238 assert_eq!(json_escape("x\n"), "x\\n");
239 assert_eq!(json_escape("\t"), "\\t");
240 }
241}