1use std::path::PathBuf;
4
5use thiserror::Error;
6
7const DEFAULT_API_KEYS_PATH: &str = "~/.aa/api-keys.json";
9
10const DEFAULT_RATE_LIMIT_RPM: u32 = 1000;
12
13const MIN_JWT_SECRET_LEN: usize = 32;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum AuthMode {
19 On,
21 Off,
23}
24
25#[derive(Debug, Clone)]
27pub struct AuthConfig {
28 pub mode: AuthMode,
30 pub jwt_secret: Option<Vec<u8>>,
32 pub api_keys_path: PathBuf,
34 pub rate_limit_rpm: u32,
36}
37
38#[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 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
92pub 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
105fn 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
115fn 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 static ENV_LOCK: Mutex<()> = Mutex::new(());
127
128 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 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}