Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 0 additions & 4 deletions crates/bin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ extenddb-core = { workspace = true }
extenddb-engine = { workspace = true }
extenddb-storage = { workspace = true }
extenddb-storage-postgres = { workspace = true, optional = true }
extenddb-auth = { workspace = true }
extenddb-server = { workspace = true }
tokio = { workspace = true }
anyhow = { workspace = true }
Expand All @@ -35,9 +34,6 @@ toml = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
syslog-tracing = { workspace = true }
bcrypt = { workspace = true }
aes-gcm = { workspace = true }
rand = { workspace = true }
base64 = { workspace = true }
libc = { workspace = true }
rcgen = { workspace = true }
Expand Down
10 changes: 8 additions & 2 deletions crates/bin/src/cmd_init.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,13 +255,19 @@ pub async fn run(args: InitArgs) -> anyhow::Result<u8> {
}

// Generate or update extenddb.toml.
let catalog_url = bootstrapper.catalog_connection_url();
let backend = args.backend.as_deref().unwrap_or("postgres");
let config_path = &args.config;

if Path::new(config_path).exists() {
std::fs::remove_file(config_path)?;
}
generate_config(config_path, &catalog_url, &bind_addr, docs_dir.as_deref())?;
generate_config(
config_path,
backend,
bootstrapper.as_ref(),
&bind_addr,
docs_dir.as_deref(),
)?;

println!(
"\n=== extenddb init complete ===\nStart the server with: extenddb serve --config {config_path}"
Expand Down
20 changes: 12 additions & 8 deletions crates/bin/src/init_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ pub fn generate_tls_cert_if_needed(bind_addr: &str) -> anyhow::Result<()> {
/// All other settings are commented out with their defaults.
pub(crate) fn generate_config(
config_path: &str,
catalog_url: &str,
backend: &str,
bootstrapper: &dyn extenddb_storage::bootstrapper::Bootstrapper,
bind_addr: &str,
docs_dir: Option<&str>,
) -> anyhow::Result<()> {
Expand All @@ -90,6 +91,15 @@ pub(crate) fn generate_config(
}
};

// Generate storage section with backend-specific config
let backend_config = bootstrapper.generate_backend_config_section();
let storage_section = format!(
r#"[storage]
backend = "{backend}"

{backend_config}"#
);

let toml = format!(
r#"# Generated by extenddb init on {timestamp}
#
Expand All @@ -114,13 +124,7 @@ bind_addr = "{bind_addr}"
cert_path = "{tls_cert}"
key_path = "{tls_key}"

[storage]
# backend = "postgres" # Only "postgres" is supported

[storage.postgres]
connection_string = "{catalog_url}"
# pool_size = 20 # Max connections for data operations (default 20, min 10)
# catalog_pool_size = # Max connections for management/catalog ops (defaults to pool_size, min 10)
{storage_section}

[auth]
# provider = "builtin" # SigV4 with local credential store (mandatory)
Expand Down
75 changes: 19 additions & 56 deletions crates/storage-postgres/src/bootstrapper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@
//! lazily as needed during the bootstrap sequence.

use async_trait::async_trait;
use extenddb_storage::bootstrapper::{AdminBootstrapResult, BootstrapConfig, Bootstrapper};
use extenddb_storage::bootstrapper::{
AdminBootstrapResult, BootstrapConfig, Bootstrapper,
helpers::{
check_conflict, extract_arg, generate_account_id, generate_encryption_key,
generate_random_password, hash_password_async,
},
};
use extenddb_storage::management_store::{OpError, OpResult};
use sqlx::PgPool;
use sqlx::postgres::{PgConnectOptions, PgPoolOptions};
Expand Down Expand Up @@ -221,9 +227,6 @@ impl Bootstrapper for PostgresBootstrapper {
}

async fn bootstrap_encryption_key(&self) -> OpResult<()> {
use aes_gcm::KeyInit;
use base64::Engine;

let pool = self.app_pool(&self.config.catalog_db).await?;
let exists: bool = sqlx::query_scalar(
"SELECT EXISTS(SELECT 1 FROM settings WHERE key = 'encryption_key')",
Expand All @@ -238,8 +241,7 @@ impl Bootstrapper for PostgresBootstrapper {
}

println!("--- Generating AES-256-GCM encryption key...");
let key = aes_gcm::Aes256Gcm::generate_key(&mut aes_gcm::aead::OsRng);
let key_b64 = base64::engine::general_purpose::STANDARD.encode(key);
let key_b64 = generate_encryption_key();

sqlx::query(
"INSERT INTO settings (key, value) VALUES ('encryption_key', $1) \
Expand Down Expand Up @@ -312,12 +314,7 @@ impl Bootstrapper for PostgresBootstrapper {
Some(p) if !p.is_empty() => (p.to_owned(), true),
_ => (generate_random_password(), false),
};
let pw_clone = password.clone();
let hash =
tokio::task::spawn_blocking(move || bcrypt::hash(pw_clone, bcrypt::DEFAULT_COST))
.await
.map_err(|e| OpError::Internal(format!("bcrypt hash task failed: {e}")))?
.map_err(|e| OpError::Internal(format!("bcrypt hash failed: {e}")))?;
let hash = hash_password_async(password.clone()).await?;

sqlx::query(
"INSERT INTO admin_users (admin_name, password_hash) VALUES ($1, $2) \
Expand Down Expand Up @@ -423,6 +420,16 @@ impl Bootstrapper for PostgresBootstrapper {
fn catalog_connection_url(&self) -> String {
self.app_connection_url(&self.config.catalog_db)
}

fn generate_backend_config_section(&self) -> String {
format!(
r#"[storage.postgres]
connection_string = "{}"
# pool_size = 20 # Max connections for data operations (default 20, min 10)
# catalog_pool_size = # Max connections for management/catalog ops (defaults to pool_size, min 10)"#,
self.catalog_connection_url()
)
}
}

// ── Helpers ────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -453,28 +460,6 @@ async fn create_database(pool: &PgPool, name: &str, owner: &str) -> OpResult<()>
Ok(())
}

/// Generate a random 12-digit numeric account ID (matches AWS account ID format).
fn generate_account_id() -> String {
use rand::Rng;
let mut rng = rand::rng();
let id: u64 = rng.random_range(100_000_000_000..1_000_000_000_000);
id.to_string()
}

/// Generate a 24-character random password using alphanumeric characters only.
///
/// Restricted to `[a-zA-Z0-9]` to avoid URL-encoding issues in form submissions,
/// shell copy-paste problems, and other contexts where special characters break.
/// At 24 characters from a 62-char alphabet, entropy is ~143 bits — more than sufficient.
fn generate_random_password() -> String {
use rand::Rng;
const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let mut rng = rand::rng();
(0..24)
.map(|_| CHARSET[rng.random_range(0..CHARSET.len())] as char)
.collect()
}

impl PostgresBootstrapper {
/// Create a bootstrapper from config file and CLI args. Parses
/// Postgres-specific arguments and merges with config.
Expand Down Expand Up @@ -582,25 +567,3 @@ impl PostgresBootstrapper {
.map_err(|e| StorageError::Internal(format!("{e:?}")))
}
}

/// Check that a CLI arg, if provided, matches the config value.
fn check_conflict<T: PartialEq + std::fmt::Display>(
cli_val: Option<&T>,
config_val: &T,
flag: &str,
) -> Result<(), extenddb_storage::error::StorageError> {
if let Some(v) = cli_val {
if v != config_val {
return Err(extenddb_storage::error::StorageError::Internal(format!(
"{} value '{}' conflicts with config file value '{}'",
flag, v, config_val
)));
}
}
Ok(())
}

/// Extract a CLI argument value by flag name.
fn extract_arg(args: &[String], flag: &str) -> Option<String> {
args.windows(2).find(|w| w[0] == flag).map(|w| w[1].clone())
}
4 changes: 4 additions & 0 deletions crates/storage/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,20 @@ rust-version.workspace = true
license.workspace = true

[dependencies]
aes-gcm = { workspace = true }
async-trait = { workspace = true }
base64 = { workspace = true }
bcrypt = { workspace = true }
bigdecimal = { workspace = true }
extenddb-auth = { workspace = true }
extenddb-core = { workspace = true }
futures = { workspace = true }
inventory = { workspace = true }
rand = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
time = { workspace = true }
tokio = { workspace = true }
toml = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
Loading
Loading