use std::io::{BufRead, Write};
use std::path::PathBuf;
use crate::auth::store::AuthStore;
use crate::auth::AuthConfig;
use crate::{RedDBOptions, RedDBRuntime};
pub struct BootstrapArgs {
pub path: PathBuf,
pub vault: bool,
pub username: String,
pub password: Option<String>,
pub password_stdin: bool,
pub print_certificate: bool,
pub json: bool,
}
#[derive(Debug)]
pub struct BootstrapOutcome {
pub username: String,
pub api_key: String,
pub certificate: String,
}
pub fn run(args: BootstrapArgs) -> Result<BootstrapOutcome, String> {
if !args.vault {
return Err(
"bootstrap requires --vault (admin credentials must be sealed in the encrypted vault)"
.to_string(),
);
}
if std::env::var("REDDB_CERTIFICATE")
.ok()
.filter(|s| !s.is_empty())
.is_none()
&& std::env::var("REDDB_VAULT_KEY")
.ok()
.filter(|s| !s.is_empty())
.is_none()
{
return Err("vault requires REDDB_CERTIFICATE or REDDB_VAULT_KEY (use the *_FILE companion to read from a mounted secret)".to_string());
}
if args.username.trim().is_empty() {
return Err(
"username is required (use --username, or set REDDB_USERNAME / REDDB_USERNAME_FILE)"
.to_string(),
);
}
let password = resolve_password(&args)?;
if password.is_empty() {
return Err("password is required (use --password-stdin or REDDB_PASSWORD_FILE)".into());
}
let opts = RedDBOptions::persistent(&args.path);
let runtime = RedDBRuntime::with_options(opts).map_err(|err| format!("open db: {err}"))?;
let pager = runtime
.db()
.store()
.pager()
.cloned()
.ok_or_else(|| "vault requires a paged database (persistent mode)".to_string())?;
let config = AuthConfig {
vault_enabled: true,
..AuthConfig::default()
};
let store =
AuthStore::with_vault(config, pager, None).map_err(|err| format!("open vault: {err}"))?;
if !store.needs_bootstrap() {
let _ = runtime.checkpoint();
return Err("already bootstrapped — bootstrap is one-shot and irreversible".into());
}
let result = store
.bootstrap(&args.username, &password)
.map_err(|err| format!("bootstrap: {err}"))?;
let certificate = result.certificate.clone().ok_or_else(|| {
"bootstrap succeeded but no certificate was issued (vault not configured?)".to_string()
})?;
let api_key = result.api_key.key.clone();
drop(store);
drop(runtime);
Ok(BootstrapOutcome {
username: result.user.username,
api_key,
certificate,
})
}
fn resolve_password(args: &BootstrapArgs) -> Result<String, String> {
if args.password_stdin {
let mut buf = String::new();
let stdin = std::io::stdin();
stdin
.lock()
.read_line(&mut buf)
.map_err(|err| format!("read password from stdin: {err}"))?;
let trimmed = buf.trim_end_matches(['\n', '\r']).to_string();
return Ok(trimmed);
}
if let Some(p) = args.password.as_ref() {
let _ = writeln!(
std::io::stderr(),
"warning: --password leaks credentials to /proc/<pid>/cmdline; prefer --password-stdin or REDDB_PASSWORD_FILE"
);
return Ok(p.clone());
}
if let Ok(env_pwd) = std::env::var("REDDB_PASSWORD") {
if !env_pwd.is_empty() {
return Ok(env_pwd);
}
}
Ok(String::new())
}
pub fn render_success(outcome: &BootstrapOutcome, args: &BootstrapArgs) {
if args.json {
println!(
"{{\"username\":\"{}\",\"token\":\"{}\",\"certificate\":\"{}\"}}",
json_escape(&outcome.username),
json_escape(&outcome.api_key),
json_escape(&outcome.certificate),
);
return;
}
if args.print_certificate {
println!("{}", outcome.certificate);
return;
}
eprintln!(
"[reddb] bootstrapped admin user `{}` — SAVE THIS CERTIFICATE (only way to unseal):",
outcome.username
);
println!("{}", outcome.certificate);
}
fn json_escape(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => {
out.push_str(&format!("\\u{:04x}", c as u32));
}
c => out.push(c),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn vault_flag_required() {
let args = BootstrapArgs {
path: PathBuf::from("/tmp/reddb-bootstrap-test.rdb"),
vault: false,
username: "admin".into(),
password: Some("hunter2".into()),
password_stdin: false,
print_certificate: false,
json: false,
};
let err = run(args).unwrap_err();
assert!(err.contains("--vault"), "got: {err}");
}
#[test]
fn json_escape_handles_control_chars() {
assert_eq!(json_escape("a\"b"), "a\\\"b");
assert_eq!(json_escape("a\\b"), "a\\\\b");
assert_eq!(json_escape("x\n"), "x\\n");
assert_eq!(json_escape("\t"), "\\t");
}
}