Skip to main content

aa_auth/
config.rs

1//! Authentication configuration from environment variables.
2
3use std::path::PathBuf;
4
5use thiserror::Error;
6
7/// Default path for API keys storage.
8const DEFAULT_API_KEYS_PATH: &str = "~/.aa/api-keys.json";
9
10/// Default rate limit: requests per minute per API key.
11const DEFAULT_RATE_LIMIT_RPM: u32 = 1000;
12
13/// Minimum length for the JWT secret (256 bits).
14const MIN_JWT_SECRET_LEN: usize = 32;
15
16/// Authentication mode.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum AuthMode {
19    /// Authentication is enabled (default).
20    On,
21    /// Authentication is disabled — all requests are treated as admin.
22    Off,
23}
24
25/// Authentication configuration for the API server.
26#[derive(Debug, Clone)]
27pub struct AuthConfig {
28    /// Whether auth is enabled or bypassed.
29    pub mode: AuthMode,
30    /// HMAC-SHA256 secret for JWT signing. `None` when `mode == Off`.
31    pub jwt_secret: Option<Vec<u8>>,
32    /// Path to the API keys JSON file.
33    pub api_keys_path: PathBuf,
34    /// Maximum requests per minute per API key.
35    pub rate_limit_rpm: u32,
36}
37
38/// Errors that can occur when loading auth configuration.
39#[derive(Debug, Error)]
40pub enum AuthConfigError {
41    #[error("AA_JWT_SECRET must be set when authentication is enabled")]
42    MissingJwtSecret,
43    #[error("AA_JWT_SECRET must be at least {MIN_JWT_SECRET_LEN} bytes (got {actual} bytes)")]
44    JwtSecretTooShort { actual: usize },
45    #[error("AA_RATE_LIMIT_RPM must be a positive integer: {0}")]
46    InvalidRateLimit(String),
47}
48
49impl AuthConfig {
50    /// Build auth configuration from environment variables.
51    ///
52    /// # Environment variables
53    ///
54    /// - `AA_AUTH`: `"on"` (default) or `"off"` (bypass mode)
55    /// - `AA_JWT_SECRET`: HMAC key for JWT, required when auth is enabled
56    /// - `AA_API_KEYS_PATH`: path to API keys file (default `~/.aa/api-keys.json`)
57    /// - `AA_RATE_LIMIT_RPM`: requests per minute per key (default 1000)
58    pub fn from_env() -> Result<Self, AuthConfigError> {
59        let mode = match std::env::var("AA_AUTH").as_deref() {
60            Ok("off") | Ok("OFF") => {
61                tracing::warn!("AA_AUTH=off: authentication is disabled — all requests treated as admin");
62                AuthMode::Off
63            }
64            _ => AuthMode::On,
65        };
66
67        let jwt_secret = if mode == AuthMode::On {
68            let secret = std::env::var("AA_JWT_SECRET").map_err(|_| AuthConfigError::MissingJwtSecret)?;
69            let bytes = secret.into_bytes();
70            if bytes.len() < MIN_JWT_SECRET_LEN {
71                return Err(AuthConfigError::JwtSecretTooShort { actual: bytes.len() });
72            }
73            Some(bytes)
74        } else {
75            None
76        };
77
78        let api_keys_path = std::env::var("AA_API_KEYS_PATH").unwrap_or_else(|_| DEFAULT_API_KEYS_PATH.to_string());
79        let api_keys_path = expand_tilde(&api_keys_path);
80
81        let rate_limit_rpm = resolve_rate_limit_rpm()?;
82
83        Ok(Self {
84            mode,
85            jwt_secret,
86            api_keys_path,
87            rate_limit_rpm,
88        })
89    }
90}
91
92/// Resolve the per-key requests-per-minute limit from `AA_RATE_LIMIT_RPM`.
93///
94/// Returns [`DEFAULT_RATE_LIMIT_RPM`] when the variable is unset, and an
95/// [`AuthConfigError::InvalidRateLimit`] when it is set to a non-`u32` value.
96/// Shared by [`AuthConfig::from_env`] and the local-mode entrypoint so the
97/// shipped server honours `AA_RATE_LIMIT_RPM` in its live rate limiter.
98pub fn resolve_rate_limit_rpm() -> Result<u32, AuthConfigError> {
99    match std::env::var("AA_RATE_LIMIT_RPM") {
100        Ok(val) => val.parse::<u32>().map_err(|_| AuthConfigError::InvalidRateLimit(val)),
101        Err(_) => Ok(DEFAULT_RATE_LIMIT_RPM),
102    }
103}
104
105/// Expand `~` prefix to the user's home directory.
106fn expand_tilde(path: &str) -> PathBuf {
107    if let Some(rest) = path.strip_prefix("~/") {
108        if let Some(home) = dirs_home() {
109            return home.join(rest);
110        }
111    }
112    PathBuf::from(path)
113}
114
115/// Get the user's home directory.
116fn dirs_home() -> Option<PathBuf> {
117    std::env::var("HOME").ok().map(PathBuf::from)
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use std::sync::Mutex;
124
125    /// Guard to serialize env-var-dependent tests.
126    static ENV_LOCK: Mutex<()> = Mutex::new(());
127
128    /// Helper: clear all auth-related env vars before a test.
129    fn clear_auth_env() {
130        std::env::remove_var("AA_AUTH");
131        std::env::remove_var("AA_JWT_SECRET");
132        std::env::remove_var("AA_API_KEYS_PATH");
133        std::env::remove_var("AA_RATE_LIMIT_RPM");
134    }
135
136    #[test]
137    fn test_config_auth_off_no_secret_required() {
138        let _lock = ENV_LOCK.lock().unwrap();
139        clear_auth_env();
140        std::env::set_var("AA_AUTH", "off");
141
142        let config = AuthConfig::from_env().expect("auth=off should succeed without secret");
143        assert_eq!(config.mode, AuthMode::Off);
144        assert!(config.jwt_secret.is_none());
145    }
146
147    #[test]
148    fn test_config_auth_on_missing_secret_fails() {
149        let _lock = ENV_LOCK.lock().unwrap();
150        clear_auth_env();
151        // AA_AUTH defaults to On when unset.
152
153        let result = AuthConfig::from_env();
154        assert!(result.is_err());
155        assert!(matches!(result.unwrap_err(), AuthConfigError::MissingJwtSecret));
156    }
157
158    #[test]
159    fn test_config_auth_on_short_secret_fails() {
160        let _lock = ENV_LOCK.lock().unwrap();
161        clear_auth_env();
162        std::env::set_var("AA_JWT_SECRET", "too-short");
163
164        let result = AuthConfig::from_env();
165        assert!(result.is_err());
166        assert!(matches!(result.unwrap_err(), AuthConfigError::JwtSecretTooShort { .. }));
167    }
168
169    #[test]
170    fn test_config_auth_on_valid_secret_succeeds() {
171        let _lock = ENV_LOCK.lock().unwrap();
172        clear_auth_env();
173        std::env::set_var("AA_JWT_SECRET", "a]secret-that-is-at-least-32-bytes-long!!");
174
175        let config = AuthConfig::from_env().expect("valid secret should succeed");
176        assert_eq!(config.mode, AuthMode::On);
177        assert!(config.jwt_secret.is_some());
178    }
179
180    #[test]
181    fn test_config_default_rate_limit() {
182        let _lock = ENV_LOCK.lock().unwrap();
183        clear_auth_env();
184        std::env::set_var("AA_AUTH", "off");
185
186        let config = AuthConfig::from_env().unwrap();
187        assert_eq!(config.rate_limit_rpm, 1000);
188    }
189
190    #[test]
191    fn test_config_custom_rate_limit() {
192        let _lock = ENV_LOCK.lock().unwrap();
193        clear_auth_env();
194        std::env::set_var("AA_AUTH", "off");
195        std::env::set_var("AA_RATE_LIMIT_RPM", "500");
196
197        let config = AuthConfig::from_env().unwrap();
198        assert_eq!(config.rate_limit_rpm, 500);
199    }
200}