use anyhow::Result;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default, deny_unknown_fields)]
pub struct AuditLoggingConfig {
pub enabled: bool,
pub log_level: String,
pub include_sensitive_data: bool,
pub async_logging: bool,
pub buffer_size: u32,
pub flush_interval_secs: u32,
}
impl Default for AuditLoggingConfig {
fn default() -> Self {
Self {
enabled: true,
log_level: "info".to_string(),
include_sensitive_data: false,
async_logging: true,
buffer_size: 1000,
flush_interval_secs: 5,
}
}
}
impl AuditLoggingConfig {
pub fn to_json(&self) -> serde_json::Value {
serde_json::json!({
"enabled": self.enabled,
"logLevel": self.log_level,
"includeSensitiveData": self.include_sensitive_data,
"asyncLogging": self.async_logging,
"bufferSize": self.buffer_size,
"flushIntervalSecs": self.flush_interval_secs,
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default, deny_unknown_fields)]
pub struct ErrorSanitizationConfig {
pub enabled: bool,
pub generic_messages: bool,
pub internal_logging: bool,
pub leak_sensitive_details: bool,
pub user_facing_format: String,
}
impl Default for ErrorSanitizationConfig {
fn default() -> Self {
Self {
enabled: true,
generic_messages: true,
internal_logging: true,
leak_sensitive_details: false,
user_facing_format: "generic".to_string(),
}
}
}
impl ErrorSanitizationConfig {
pub fn validate(&self) -> Result<()> {
if self.leak_sensitive_details {
anyhow::bail!(
"leak_sensitive_details=true is a security risk! Never enable in production."
);
}
Ok(())
}
pub fn to_json(&self) -> serde_json::Value {
serde_json::json!({
"enabled": self.enabled,
"genericMessages": self.generic_messages,
"internalLogging": self.internal_logging,
"leakSensitiveDetails": self.leak_sensitive_details,
"userFacingFormat": self.user_facing_format,
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default, deny_unknown_fields)]
pub struct RateLimitConfig {
pub enabled: bool,
pub auth_start_max_requests: u32,
pub auth_start_window_secs: u64,
pub auth_callback_max_requests: u32,
pub auth_callback_window_secs: u64,
pub auth_refresh_max_requests: u32,
pub auth_refresh_window_secs: u64,
pub auth_logout_max_requests: u32,
pub auth_logout_window_secs: u64,
pub failed_login_max_requests: u32,
pub failed_login_window_secs: u64,
}
impl Default for RateLimitConfig {
fn default() -> Self {
Self {
enabled: true,
auth_start_max_requests: 100,
auth_start_window_secs: 60,
auth_callback_max_requests: 50,
auth_callback_window_secs: 60,
auth_refresh_max_requests: 10,
auth_refresh_window_secs: 60,
auth_logout_max_requests: 20,
auth_logout_window_secs: 60,
failed_login_max_requests: 5,
failed_login_window_secs: 3600,
}
}
}
impl RateLimitConfig {
pub fn validate(&self) -> Result<()> {
for (name, window) in &[
("auth_start_window_secs", self.auth_start_window_secs),
("auth_callback_window_secs", self.auth_callback_window_secs),
("auth_refresh_window_secs", self.auth_refresh_window_secs),
("auth_logout_window_secs", self.auth_logout_window_secs),
("failed_login_window_secs", self.failed_login_window_secs),
] {
if *window == 0 {
anyhow::bail!("{name} must be positive");
}
}
for (name, max_req) in &[
("auth_start_max_requests", self.auth_start_max_requests),
("auth_callback_max_requests", self.auth_callback_max_requests),
("auth_refresh_max_requests", self.auth_refresh_max_requests),
("auth_logout_max_requests", self.auth_logout_max_requests),
("failed_login_max_requests", self.failed_login_max_requests),
] {
if *max_req == 0 {
anyhow::bail!(
"{name} must be at least 1; \
setting it to 0 blocks all requests permanently"
);
}
}
Ok(())
}
pub fn to_json(&self) -> serde_json::Value {
serde_json::json!({
"enabled": self.enabled,
"authStart": {
"maxRequests": self.auth_start_max_requests,
"windowSecs": self.auth_start_window_secs,
},
"authCallback": {
"maxRequests": self.auth_callback_max_requests,
"windowSecs": self.auth_callback_window_secs,
},
"authRefresh": {
"maxRequests": self.auth_refresh_max_requests,
"windowSecs": self.auth_refresh_window_secs,
},
"authLogout": {
"maxRequests": self.auth_logout_max_requests,
"windowSecs": self.auth_logout_window_secs,
},
"failedLogin": {
"maxRequests": self.failed_login_max_requests,
"windowSecs": self.failed_login_window_secs,
},
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default, deny_unknown_fields)]
pub struct StateEncryptionConfig {
pub enabled: bool,
pub algorithm: String,
pub key_rotation_enabled: bool,
pub nonce_size: u32,
pub key_size: u32,
}
impl Default for StateEncryptionConfig {
fn default() -> Self {
Self {
enabled: true,
algorithm: "chacha20-poly1305".to_string(),
key_rotation_enabled: false,
nonce_size: 12,
key_size: 32,
}
}
}
const SUPPORTED_ALGORITHMS: &[&str] = &["chacha20-poly1305", "aes-256-gcm"];
impl StateEncryptionConfig {
pub fn validate(&self) -> Result<()> {
if !SUPPORTED_ALGORITHMS.contains(&self.algorithm.as_str()) {
anyhow::bail!(
"algorithm {:?} is not supported; must be one of: {}",
self.algorithm,
SUPPORTED_ALGORITHMS.join(", ")
);
}
if ![16, 24, 32].contains(&self.key_size) {
anyhow::bail!("key_size must be 16, 24, or 32 bytes");
}
if self.nonce_size != 12 {
anyhow::bail!("nonce_size must be 12 bytes (96-bit)");
}
Ok(())
}
pub fn to_json(&self) -> serde_json::Value {
serde_json::json!({
"enabled": self.enabled,
"algorithm": self.algorithm,
"keyRotationEnabled": self.key_rotation_enabled,
"nonceSize": self.nonce_size,
"keySize": self.key_size,
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default, deny_unknown_fields)]
pub struct ConstantTimeConfig {
pub enabled: bool,
pub apply_to_jwt: bool,
pub apply_to_session_tokens: bool,
pub apply_to_csrf_tokens: bool,
pub apply_to_refresh_tokens: bool,
}
impl Default for ConstantTimeConfig {
fn default() -> Self {
Self {
enabled: true,
apply_to_jwt: true,
apply_to_session_tokens: true,
apply_to_csrf_tokens: true,
apply_to_refresh_tokens: true,
}
}
}
impl ConstantTimeConfig {
pub fn to_json(&self) -> serde_json::Value {
serde_json::json!({
"enabled": self.enabled,
"applyToJwt": self.apply_to_jwt,
"applyToSessionTokens": self.apply_to_session_tokens,
"applytoCsrfTokens": self.apply_to_csrf_tokens,
"applyToRefreshTokens": self.apply_to_refresh_tokens,
})
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct RoleDefinitionConfig {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub scopes: Vec<String>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum TenancyModeConfig {
#[default]
None,
Row,
Schema,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default, deny_unknown_fields)]
pub struct TenancyTomlConfig {
pub mode: TenancyModeConfig,
pub tenant_claim: String,
}
impl Default for TenancyTomlConfig {
fn default() -> Self {
Self {
mode: TenancyModeConfig::None,
tenant_claim: "tenant_id".to_string(),
}
}
}
impl TenancyTomlConfig {
pub fn validate(&self) -> Result<()> {
if !matches!(self.mode, TenancyModeConfig::None) && self.tenant_claim.is_empty() {
anyhow::bail!("tenancy.tenant_claim must not be empty when mode is not 'none'");
}
Ok(())
}
pub fn to_json(&self) -> serde_json::Value {
serde_json::json!({
"mode": match self.mode {
TenancyModeConfig::None => "none",
TenancyModeConfig::Row => "row",
TenancyModeConfig::Schema => "schema",
},
"tenantClaim": self.tenant_claim,
})
}
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(default, deny_unknown_fields)]
pub struct SecurityConfig {
#[serde(rename = "audit_logging")]
pub audit_logging: AuditLoggingConfig,
#[serde(rename = "error_sanitization")]
pub error_sanitization: ErrorSanitizationConfig,
#[serde(rename = "rate_limiting")]
pub rate_limiting: RateLimitConfig,
#[serde(rename = "state_encryption")]
pub state_encryption: StateEncryptionConfig,
#[serde(rename = "constant_time")]
pub constant_time: ConstantTimeConfig,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub role_definitions: Vec<RoleDefinitionConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default_role: Option<String>,
}
impl SecurityConfig {
pub fn validate(&self) -> Result<()> {
self.error_sanitization.validate()?;
self.rate_limiting.validate()?;
self.state_encryption.validate()?;
for role in &self.role_definitions {
if role.name.is_empty() {
anyhow::bail!("Role name cannot be empty");
}
if role.scopes.is_empty() {
anyhow::bail!("Role '{}' must have at least one scope", role.name);
}
}
Ok(())
}
pub fn to_json(&self) -> serde_json::Value {
let mut json = serde_json::json!({
"auditLogging": self.audit_logging.to_json(),
"errorSanitization": self.error_sanitization.to_json(),
"rateLimiting": self.rate_limiting.to_json(),
"stateEncryption": self.state_encryption.to_json(),
"constantTime": self.constant_time.to_json(),
});
if !self.role_definitions.is_empty() {
json["roleDefinitions"] = serde_json::to_value(
self.role_definitions
.iter()
.map(|r| {
serde_json::json!({
"name": r.name,
"description": r.description,
"scopes": r.scopes,
})
})
.collect::<Vec<_>>(),
)
.unwrap_or_default();
}
if let Some(default_role) = &self.default_role {
json["defaultRole"] = serde_json::json!(default_role);
}
json
}
}