photoncloud-monorepo/iam/crates/iam-server/src/config.rs
centra 3eeb303dcb feat: Batch commit for T039.S3 deployment
Includes all pending changes needed for nixos-anywhere:
- fiberlb: L7 policy, rule, certificate types
- deployer: New service for cluster management
- nix-nos: Generic network modules
- Various service updates and fixes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-13 04:34:51 +09:00

335 lines
8.2 KiB
Rust

//! Server configuration
//!
//! Configuration for the IAM server including storage backend, authentication, and server settings.
use std::net::SocketAddr;
use std::path::Path;
use serde::{Deserialize, Serialize};
/// Main server configuration
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ServerConfig {
/// Server settings
pub server: ServerSettings,
/// Storage backend configuration
pub store: StoreConfig,
/// Authentication configuration
pub authn: AuthnConfig,
/// Logging configuration
#[serde(default)]
pub logging: LoggingConfig,
}
impl ServerConfig {
/// Load configuration from a TOML file
pub fn from_file(path: impl AsRef<Path>) -> Result<Self, ConfigError> {
let content = std::fs::read_to_string(path).map_err(|e| ConfigError::Io(e.to_string()))?;
toml::from_str(&content).map_err(|e| ConfigError::Parse(e.to_string()))
}
/// Load configuration from environment variables
pub fn from_env() -> Result<Self, ConfigError> {
// Try to find config file from environment
if let Ok(path) = std::env::var("IAM_CONFIG") {
return Self::from_file(path);
}
// Default paths to try
let default_paths = ["iam.toml", "/etc/iam/iam.toml", "config/iam.toml"];
for path in &default_paths {
if Path::new(path).exists() {
return Self::from_file(path);
}
}
// Use default configuration
Ok(Self::default())
}
/// Create a minimal configuration for testing
#[cfg(test)]
pub fn for_testing() -> Self {
Self {
server: ServerSettings {
addr: "127.0.0.1:50051".parse().unwrap(),
http_addr: "127.0.0.1:8083".parse().unwrap(),
tls: None,
},
store: StoreConfig {
backend: BackendKind::Memory,
chainfire_endpoints: None,
flaredb_endpoint: None,
flaredb_namespace: None,
},
authn: AuthnConfig {
jwt: None,
internal_token: InternalTokenConfig {
signing_key: "test-signing-key-for-development-only".into(),
issuer: "iam-test".into(),
default_ttl_seconds: 3600,
max_ttl_seconds: 86400,
},
},
logging: LoggingConfig::default(),
}
}
}
/// Server settings
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerSettings {
/// Listen address
#[serde(default = "default_addr")]
pub addr: SocketAddr,
/// HTTP REST API listen address
#[serde(default = "default_http_addr")]
pub http_addr: SocketAddr,
/// TLS configuration
pub tls: Option<TlsConfig>,
}
impl Default for ServerSettings {
fn default() -> Self {
Self {
addr: default_addr(),
http_addr: default_http_addr(),
tls: None,
}
}
}
fn default_addr() -> SocketAddr {
"0.0.0.0:50051".parse().unwrap()
}
fn default_http_addr() -> SocketAddr {
"127.0.0.1:8083".parse().unwrap()
}
/// TLS configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TlsConfig {
/// Path to certificate file (PEM)
pub cert_file: String,
/// Path to private key file (PEM)
pub key_file: String,
/// Path to CA certificate file for client verification (optional)
pub ca_file: Option<String>,
/// Require client certificates
#[serde(default)]
pub require_client_cert: bool,
}
/// Storage backend configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StoreConfig {
/// Backend type
#[serde(default)]
pub backend: BackendKind,
/// Chainfire endpoints (for chainfire backend)
pub chainfire_endpoints: Option<Vec<String>>,
/// FlareDB endpoint (for flaredb backend)
pub flaredb_endpoint: Option<String>,
/// FlareDB namespace (for flaredb backend)
pub flaredb_namespace: Option<String>,
}
impl Default for StoreConfig {
fn default() -> Self {
Self {
backend: BackendKind::Memory,
chainfire_endpoints: None,
flaredb_endpoint: None,
flaredb_namespace: Some("iam".into()),
}
}
}
/// Backend type
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum BackendKind {
/// In-memory backend (for testing)
#[default]
Memory,
/// Chainfire distributed KV
Chainfire,
/// FlareDB distributed database
FlareDb,
}
/// Authentication configuration
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct AuthnConfig {
/// JWT/OIDC configuration
pub jwt: Option<JwtConfig>,
/// Internal token configuration
pub internal_token: InternalTokenConfig,
}
/// JWT/OIDC configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JwtConfig {
/// JWKS endpoint URL
pub jwks_url: String,
/// Expected issuer
pub issuer: String,
/// Expected audience
pub audience: String,
/// JWKS cache TTL in seconds
#[serde(default = "default_jwks_cache_ttl")]
pub jwks_cache_ttl_seconds: u64,
}
fn default_jwks_cache_ttl() -> u64 {
3600
}
/// Internal token configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InternalTokenConfig {
/// Signing key (base64 encoded or raw string)
pub signing_key: String,
/// Token issuer identifier
#[serde(default = "default_issuer")]
pub issuer: String,
/// Default token TTL in seconds
#[serde(default = "default_token_ttl")]
pub default_ttl_seconds: u64,
/// Maximum token TTL in seconds
#[serde(default = "default_max_token_ttl")]
pub max_ttl_seconds: u64,
}
impl Default for InternalTokenConfig {
fn default() -> Self {
Self {
signing_key: String::new(),
issuer: default_issuer(),
default_ttl_seconds: default_token_ttl(),
max_ttl_seconds: default_max_token_ttl(),
}
}
}
fn default_issuer() -> String {
"iam".into()
}
fn default_token_ttl() -> u64 {
3600 // 1 hour
}
fn default_max_token_ttl() -> u64 {
86400 * 7 // 7 days
}
/// Logging configuration
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoggingConfig {
/// Log level
#[serde(default = "default_log_level")]
pub level: String,
/// Log format (json or text)
#[serde(default = "default_log_format")]
pub format: String,
}
impl Default for LoggingConfig {
fn default() -> Self {
Self {
level: default_log_level(),
format: default_log_format(),
}
}
}
fn default_log_level() -> String {
"info".into()
}
fn default_log_format() -> String {
"text".into()
}
/// Configuration errors
#[derive(Debug)]
pub enum ConfigError {
/// IO error
Io(String),
/// Parse error
Parse(String),
}
impl std::fmt::Display for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ConfigError::Io(e) => write!(f, "IO error: {}", e),
ConfigError::Parse(e) => write!(f, "Parse error: {}", e),
}
}
}
impl std::error::Error for ConfigError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = ServerConfig::default();
assert_eq!(config.server.addr, "0.0.0.0:50051".parse().unwrap());
assert!(matches!(config.store.backend, BackendKind::Memory));
}
#[test]
fn test_for_testing() {
let config = ServerConfig::for_testing();
assert!(!config.authn.internal_token.signing_key.is_empty());
}
#[test]
fn test_toml_parse() {
let toml = r#"
[server]
addr = "127.0.0.1:8080"
[store]
backend = "memory"
[authn.internal_token]
signing_key = "secret"
issuer = "test"
default_ttl_seconds = 1800
[logging]
level = "debug"
"#;
let config: ServerConfig = toml::from_str(toml).unwrap();
assert_eq!(config.server.addr, "127.0.0.1:8080".parse().unwrap());
assert_eq!(config.authn.internal_token.default_ttl_seconds, 1800);
assert_eq!(config.logging.level, "debug");
}
}