use std::path::PathBuf;
use thiserror::Error;
const DEFAULT_API_KEYS_PATH: &str = "~/.aa/api-keys.json";
const DEFAULT_RATE_LIMIT_RPM: u32 = 1000;
const MIN_JWT_SECRET_LEN: usize = 32;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuthMode {
On,
Off,
}
#[derive(Debug, Clone)]
pub struct AuthConfig {
pub mode: AuthMode,
pub jwt_secret: Option<Vec<u8>>,
pub api_keys_path: PathBuf,
pub rate_limit_rpm: u32,
}
#[derive(Debug, Error)]
pub enum AuthConfigError {
#[error("AA_JWT_SECRET must be set when authentication is enabled")]
MissingJwtSecret,
#[error("AA_JWT_SECRET must be at least {MIN_JWT_SECRET_LEN} bytes (got {actual} bytes)")]
JwtSecretTooShort { actual: usize },
#[error("AA_RATE_LIMIT_RPM must be a positive integer: {0}")]
InvalidRateLimit(String),
}
impl AuthConfig {
pub fn from_env() -> Result<Self, AuthConfigError> {
let mode = match std::env::var("AA_AUTH").as_deref() {
Ok("off") | Ok("OFF") => {
tracing::warn!("AA_AUTH=off: authentication is disabled — all requests treated as admin");
AuthMode::Off
}
_ => AuthMode::On,
};
let jwt_secret = if mode == AuthMode::On {
let secret = std::env::var("AA_JWT_SECRET").map_err(|_| AuthConfigError::MissingJwtSecret)?;
let bytes = secret.into_bytes();
if bytes.len() < MIN_JWT_SECRET_LEN {
return Err(AuthConfigError::JwtSecretTooShort { actual: bytes.len() });
}
Some(bytes)
} else {
None
};
let api_keys_path = std::env::var("AA_API_KEYS_PATH").unwrap_or_else(|_| DEFAULT_API_KEYS_PATH.to_string());
let api_keys_path = expand_tilde(&api_keys_path);
let rate_limit_rpm = resolve_rate_limit_rpm()?;
Ok(Self {
mode,
jwt_secret,
api_keys_path,
rate_limit_rpm,
})
}
}
pub fn resolve_rate_limit_rpm() -> Result<u32, AuthConfigError> {
match std::env::var("AA_RATE_LIMIT_RPM") {
Ok(val) => val.parse::<u32>().map_err(|_| AuthConfigError::InvalidRateLimit(val)),
Err(_) => Ok(DEFAULT_RATE_LIMIT_RPM),
}
}
fn expand_tilde(path: &str) -> PathBuf {
if let Some(rest) = path.strip_prefix("~/") {
if let Some(home) = dirs_home() {
return home.join(rest);
}
}
PathBuf::from(path)
}
fn dirs_home() -> Option<PathBuf> {
std::env::var("HOME").ok().map(PathBuf::from)
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
static ENV_LOCK: Mutex<()> = Mutex::new(());
fn clear_auth_env() {
std::env::remove_var("AA_AUTH");
std::env::remove_var("AA_JWT_SECRET");
std::env::remove_var("AA_API_KEYS_PATH");
std::env::remove_var("AA_RATE_LIMIT_RPM");
}
#[test]
fn test_config_auth_off_no_secret_required() {
let _lock = ENV_LOCK.lock().unwrap();
clear_auth_env();
std::env::set_var("AA_AUTH", "off");
let config = AuthConfig::from_env().expect("auth=off should succeed without secret");
assert_eq!(config.mode, AuthMode::Off);
assert!(config.jwt_secret.is_none());
}
#[test]
fn test_config_auth_on_missing_secret_fails() {
let _lock = ENV_LOCK.lock().unwrap();
clear_auth_env();
let result = AuthConfig::from_env();
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), AuthConfigError::MissingJwtSecret));
}
#[test]
fn test_config_auth_on_short_secret_fails() {
let _lock = ENV_LOCK.lock().unwrap();
clear_auth_env();
std::env::set_var("AA_JWT_SECRET", "too-short");
let result = AuthConfig::from_env();
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), AuthConfigError::JwtSecretTooShort { .. }));
}
#[test]
fn test_config_auth_on_valid_secret_succeeds() {
let _lock = ENV_LOCK.lock().unwrap();
clear_auth_env();
std::env::set_var("AA_JWT_SECRET", "a]secret-that-is-at-least-32-bytes-long!!");
let config = AuthConfig::from_env().expect("valid secret should succeed");
assert_eq!(config.mode, AuthMode::On);
assert!(config.jwt_secret.is_some());
}
#[test]
fn test_config_default_rate_limit() {
let _lock = ENV_LOCK.lock().unwrap();
clear_auth_env();
std::env::set_var("AA_AUTH", "off");
let config = AuthConfig::from_env().unwrap();
assert_eq!(config.rate_limit_rpm, 1000);
}
#[test]
fn test_config_custom_rate_limit() {
let _lock = ENV_LOCK.lock().unwrap();
clear_auth_env();
std::env::set_var("AA_AUTH", "off");
std::env::set_var("AA_RATE_LIMIT_RPM", "500");
let config = AuthConfig::from_env().unwrap();
assert_eq!(config.rate_limit_rpm, 500);
}
}