feat(enterprise): add auth TLS and recovery hardening
This commit is contained in:
@@ -46,6 +46,99 @@ pub struct RustDbOptions {
|
||||
/// Interval in ms for periodic persistence (default: 60000)
|
||||
#[serde(default = "default_persist_interval")]
|
||||
pub persist_interval_ms: u64,
|
||||
|
||||
/// Authentication configuration.
|
||||
#[serde(default)]
|
||||
pub auth: AuthOptions,
|
||||
|
||||
/// TLS transport configuration for TCP listeners.
|
||||
#[serde(default)]
|
||||
pub tls: TlsOptions,
|
||||
}
|
||||
|
||||
/// Authentication configuration for the embedded server.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AuthOptions {
|
||||
/// Whether clients must authenticate before issuing protected commands.
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
|
||||
/// Bootstrap users loaded at startup. Passwords are converted into SCRAM credentials in memory.
|
||||
#[serde(default)]
|
||||
pub users: Vec<AuthUserOptions>,
|
||||
|
||||
/// Optional path for persisted SCRAM user metadata. Stores derived credentials, never plaintext passwords.
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub users_path: Option<String>,
|
||||
|
||||
/// SCRAM iteration count used for bootstrap credentials.
|
||||
#[serde(default = "default_scram_iterations")]
|
||||
pub scram_iterations: u32,
|
||||
}
|
||||
|
||||
impl Default for AuthOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
users: Vec::new(),
|
||||
users_path: None,
|
||||
scram_iterations: default_scram_iterations(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// TLS transport configuration for the embedded server.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TlsOptions {
|
||||
/// Whether TCP client connections must use TLS.
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
|
||||
/// PEM-encoded server certificate chain.
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cert_path: Option<String>,
|
||||
|
||||
/// PEM-encoded server private key.
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub key_path: Option<String>,
|
||||
|
||||
/// PEM-encoded client CA roots for mTLS verification.
|
||||
#[serde(default)]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub ca_path: Option<String>,
|
||||
|
||||
/// Require clients to present a certificate signed by caPath.
|
||||
#[serde(default)]
|
||||
pub require_client_cert: bool,
|
||||
}
|
||||
|
||||
impl Default for TlsOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
cert_path: None,
|
||||
key_path: None,
|
||||
ca_path: None,
|
||||
require_client_cert: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A bootstrap user for SCRAM authentication.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AuthUserOptions {
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
#[serde(default = "default_auth_database")]
|
||||
pub database: String,
|
||||
#[serde(default)]
|
||||
pub roles: Vec<String>,
|
||||
}
|
||||
|
||||
fn default_port() -> u16 {
|
||||
@@ -60,6 +153,14 @@ fn default_persist_interval() -> u64 {
|
||||
60000
|
||||
}
|
||||
|
||||
fn default_scram_iterations() -> u32 {
|
||||
15000
|
||||
}
|
||||
|
||||
fn default_auth_database() -> String {
|
||||
"admin".to_string()
|
||||
}
|
||||
|
||||
impl Default for RustDbOptions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
@@ -70,6 +171,8 @@ impl Default for RustDbOptions {
|
||||
storage_path: None,
|
||||
persist_path: None,
|
||||
persist_interval_ms: default_persist_interval(),
|
||||
auth: AuthOptions::default(),
|
||||
tls: TlsOptions::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,6 +195,59 @@ impl RustDbOptions {
|
||||
"storagePath is required when storage is 'file'".to_string(),
|
||||
));
|
||||
}
|
||||
if self.auth.enabled {
|
||||
if self.auth.users.is_empty() && self.auth.users_path.is_none() {
|
||||
return Err(ConfigError::ValidationError(
|
||||
"auth.users or auth.usersPath must be set when auth.enabled is true".to_string(),
|
||||
));
|
||||
}
|
||||
if self.auth.scram_iterations < 4096 {
|
||||
return Err(ConfigError::ValidationError(
|
||||
"auth.scramIterations must be at least 4096".to_string(),
|
||||
));
|
||||
}
|
||||
for user in &self.auth.users {
|
||||
if user.username.is_empty() {
|
||||
return Err(ConfigError::ValidationError(
|
||||
"auth.users[].username must not be empty".to_string(),
|
||||
));
|
||||
}
|
||||
if user.password.is_empty() {
|
||||
return Err(ConfigError::ValidationError(
|
||||
format!("auth user '{}' must have a non-empty password", user.username),
|
||||
));
|
||||
}
|
||||
if user.database.is_empty() {
|
||||
return Err(ConfigError::ValidationError(
|
||||
format!("auth user '{}' must have a non-empty database", user.username),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.tls.enabled {
|
||||
if self.socket_path.is_some() {
|
||||
return Err(ConfigError::ValidationError(
|
||||
"tls.enabled is only supported for TCP listeners".to_string(),
|
||||
));
|
||||
}
|
||||
if self.tls.cert_path.as_deref().unwrap_or_default().is_empty() {
|
||||
return Err(ConfigError::ValidationError(
|
||||
"tls.certPath is required when tls.enabled is true".to_string(),
|
||||
));
|
||||
}
|
||||
if self.tls.key_path.as_deref().unwrap_or_default().is_empty() {
|
||||
return Err(ConfigError::ValidationError(
|
||||
"tls.keyPath is required when tls.enabled is true".to_string(),
|
||||
));
|
||||
}
|
||||
if self.tls.require_client_cert
|
||||
&& self.tls.ca_path.as_deref().unwrap_or_default().is_empty()
|
||||
{
|
||||
return Err(ConfigError::ValidationError(
|
||||
"tls.caPath is required when tls.requireClientCert is true".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -101,7 +257,12 @@ impl RustDbOptions {
|
||||
let encoded = urlencoding(socket_path);
|
||||
format!("mongodb://{}", encoded)
|
||||
} else {
|
||||
format!("mongodb://{}:{}", self.host, self.port)
|
||||
let base = format!("mongodb://{}:{}", self.host, self.port);
|
||||
if self.tls.enabled {
|
||||
format!("{}/?tls=true", base)
|
||||
} else {
|
||||
base
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user