use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum AuthMode {
Trust,
Password,
Md5Password,
Certificate,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JwtAuthConfig {
#[serde(default = "default_jwks_refresh")]
pub jwks_refresh_secs: u64,
#[serde(default = "default_jwks_min_refetch")]
pub jwks_min_refetch_secs: u64,
#[serde(default = "default_allowed_algorithms")]
pub allowed_algorithms: Vec<String>,
#[serde(default = "default_clock_skew")]
pub clock_skew_secs: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub jwks_cache_path: Option<String>,
#[serde(default)]
pub providers: Vec<JwtProviderConfig>,
#[serde(default)]
pub jit_provisioning: bool,
#[serde(default = "default_true")]
pub jit_sync_claims: bool,
#[serde(default)]
pub claims: std::collections::HashMap<String, String>,
#[serde(default)]
pub status_claim: Option<String>,
#[serde(default)]
pub blocked_statuses: Vec<String>,
#[serde(default)]
pub enforce_scopes: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JwtProviderConfig {
pub name: String,
pub jwks_url: String,
#[serde(default)]
pub issuer: String,
#[serde(default)]
pub audience: String,
}
fn default_jwks_refresh() -> u64 {
3600
}
fn default_jwks_min_refetch() -> u64 {
60
}
fn default_clock_skew() -> u64 {
60
}
fn default_allowed_algorithms() -> Vec<String> {
vec!["RS256".into(), "ES256".into()]
}
fn default_true() -> bool {
true
}
impl Default for JwtAuthConfig {
fn default() -> Self {
Self {
jwks_refresh_secs: default_jwks_refresh(),
jwks_min_refetch_secs: default_jwks_min_refetch(),
allowed_algorithms: default_allowed_algorithms(),
clock_skew_secs: default_clock_skew(),
jwks_cache_path: None,
providers: Vec::new(),
jit_provisioning: false,
jit_sync_claims: true,
claims: std::collections::HashMap::new(),
status_claim: None,
blocked_statuses: Vec::new(),
enforce_scopes: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthConfig {
pub mode: AuthMode,
pub superuser_name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub superuser_password: Option<String>,
pub min_password_length: usize,
pub max_failed_logins: u32,
pub lockout_duration_secs: u64,
pub idle_timeout_secs: u64,
pub max_connections_per_user: u32,
pub password_expiry_days: u32,
pub audit_retention_days: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub jwt: Option<JwtAuthConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rate_limit: Option<crate::control::security::ratelimit::config::RateLimitConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metering: Option<crate::control::security::metering::config::MeteringConfig>,
}
impl Default for AuthConfig {
fn default() -> Self {
Self {
mode: AuthMode::Trust,
superuser_name: "admin".into(),
superuser_password: None,
min_password_length: 8,
max_failed_logins: 5,
lockout_duration_secs: 300,
idle_timeout_secs: 3600,
max_connections_per_user: 0,
password_expiry_days: 0,
audit_retention_days: 0,
jwt: None,
rate_limit: None,
metering: None,
}
}
}
impl AuthConfig {
pub fn resolve_superuser_password(&self) -> crate::Result<Option<String>> {
if self.mode == AuthMode::Trust {
return Ok(None);
}
if let Ok(env_pw) = std::env::var("NODEDB_SUPERUSER_PASSWORD")
&& !env_pw.is_empty()
{
return Ok(Some(env_pw));
}
if let Some(ref pw) = self.superuser_password
&& !pw.is_empty()
{
return Ok(Some(pw.clone()));
}
Err(crate::Error::Config {
detail: format!(
"auth mode is '{:?}' but no superuser password provided. \
Set 'auth.superuser_password' in config or NODEDB_SUPERUSER_PASSWORD env var.",
self.mode
),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_is_trust() {
let cfg = AuthConfig::default();
assert_eq!(cfg.mode, AuthMode::Trust);
}
#[test]
fn trust_mode_no_password_needed() {
let cfg = AuthConfig {
mode: AuthMode::Trust,
..Default::default()
};
let result = cfg.resolve_superuser_password();
assert!(result.is_ok());
assert!(result.unwrap().is_none());
}
#[test]
fn password_mode_requires_password() {
let cfg = AuthConfig {
mode: AuthMode::Password,
superuser_password: None,
..Default::default()
};
unsafe { std::env::remove_var("NODEDB_SUPERUSER_PASSWORD") };
let result = cfg.resolve_superuser_password();
assert!(result.is_err());
}
#[test]
fn password_from_config() {
let cfg = AuthConfig {
mode: AuthMode::Password,
superuser_password: Some("secret123".into()),
..Default::default()
};
let pw = cfg.resolve_superuser_password().unwrap();
assert_eq!(pw, Some("secret123".into()));
}
#[test]
fn toml_roundtrip() {
let cfg = AuthConfig::default();
let toml_str = toml::to_string_pretty(&cfg).unwrap();
let parsed: AuthConfig = toml::from_str(&toml_str).unwrap();
assert_eq!(parsed.mode, cfg.mode);
assert_eq!(parsed.superuser_name, cfg.superuser_name);
}
}