1use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::path::{Path, PathBuf};
10use tracing::{info, warn};
11
12use crate::models::HttpError;
13
14const ENV_VAR_MAP: &[(&str, &str)] = &[
16 ("openai", "OPENAI_API_KEY"),
17 ("anthropic", "ANTHROPIC_API_KEY"),
18 ("fireworks", "FIREWORKS_API_KEY"),
19 ("google", "GOOGLE_API_KEY"),
20 ("groq", "GROQ_API_KEY"),
21 ("mistral", "MISTRAL_API_KEY"),
22 ("deepinfra", "DEEPINFRA_API_KEY"),
23 ("openrouter", "OPENROUTER_API_KEY"),
24 ("azure", "AZURE_OPENAI_API_KEY"),
25];
26
27#[derive(Debug, Default, Clone, Serialize, Deserialize)]
29struct AuthData {
30 #[serde(default)]
31 keys: HashMap<String, String>,
32 #[serde(default)]
33 tokens: HashMap<String, TokenEntry>,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38struct TokenEntry {
39 token: String,
40 #[serde(skip_serializing_if = "Option::is_none")]
41 metadata: Option<serde_json::Value>,
42}
43
44#[derive(Debug, Clone)]
46pub struct ProviderStatus {
47 pub provider: String,
48 pub has_env_key: bool,
49 pub has_stored_key: bool,
50 pub env_var: String,
51}
52
53pub struct CredentialStore {
57 path: PathBuf,
58 cache: Option<AuthData>,
59}
60
61impl CredentialStore {
62 pub fn new(auth_path: Option<PathBuf>) -> Self {
66 let path = auth_path.unwrap_or_else(|| {
67 dirs::home_dir()
68 .unwrap_or_else(|| PathBuf::from("/tmp"))
69 .join(".opendev")
70 .join("auth.json")
71 });
72 Self { path, cache: None }
73 }
74
75 pub fn get_key(&mut self, provider: &str) -> Option<String> {
77 let provider_lower = provider.to_lowercase();
78
79 if let Some(env_var) = env_var_for_provider(&provider_lower)
81 && let Ok(val) = std::env::var(env_var)
82 && !val.is_empty()
83 {
84 return Some(val);
85 }
86
87 let data = self.load();
89 data.keys.get(&provider_lower).cloned()
90 }
91
92 pub fn set_key(&mut self, provider: &str, key: &str) -> Result<(), HttpError> {
94 let mut data = self.load().clone();
95 data.keys.insert(provider.to_lowercase(), key.to_string());
96 self.save(&data)?;
97 info!("Stored API key for {}", provider);
98 Ok(())
99 }
100
101 pub fn remove_key(&mut self, provider: &str) -> Result<bool, HttpError> {
103 let mut data = self.load().clone();
104 let removed = data.keys.remove(&provider.to_lowercase()).is_some();
105 if removed {
106 self.save(&data)?;
107 }
108 Ok(removed)
109 }
110
111 pub fn list_providers(&mut self) -> Vec<ProviderStatus> {
113 let data = self.load();
114 ENV_VAR_MAP
115 .iter()
116 .map(|&(provider, env_var)| {
117 let has_env = std::env::var(env_var)
118 .map(|v| !v.is_empty())
119 .unwrap_or(false);
120 let has_stored = data.keys.contains_key(provider);
121 ProviderStatus {
122 provider: provider.to_string(),
123 has_env_key: has_env,
124 has_stored_key: has_stored,
125 env_var: env_var.to_string(),
126 }
127 })
128 .collect()
129 }
130
131 pub fn store_token(
133 &mut self,
134 name: &str,
135 token: &str,
136 metadata: Option<serde_json::Value>,
137 ) -> Result<(), HttpError> {
138 let mut data = self.load().clone();
139 data.tokens.insert(
140 name.to_string(),
141 TokenEntry {
142 token: token.to_string(),
143 metadata,
144 },
145 );
146 self.save(&data)
147 }
148
149 pub fn get_token(&mut self, name: &str) -> Option<String> {
151 let data = self.load();
152 data.tokens.get(name).map(|e| e.token.clone())
153 }
154
155 fn load(&mut self) -> &AuthData {
157 if let Some(ref cached) = self.cache {
158 return cached;
159 }
160
161 let data = if self.path.exists() {
162 #[cfg(unix)]
164 self.check_permissions();
165
166 match std::fs::read_to_string(&self.path) {
167 Ok(content) => serde_json::from_str(&content).unwrap_or_default(),
168 Err(e) => {
169 warn!("Failed to load credentials from {:?}: {}", self.path, e);
170 AuthData::default()
171 }
172 }
173 } else {
174 AuthData::default()
175 };
176
177 self.cache = Some(data);
178 self.cache.as_ref().expect("cache was just set to Some")
180 }
181
182 fn save(&mut self, data: &AuthData) -> Result<(), HttpError> {
184 self.cache = Some(data.clone());
185
186 if let Some(parent) = self.path.parent() {
187 std::fs::create_dir_all(parent)?;
188 }
189
190 let tmp_path = self.path.with_extension("tmp");
192 let json = serde_json::to_string_pretty(data)?;
193 std::fs::write(&tmp_path, &json)?;
194
195 #[cfg(unix)]
197 {
198 use std::os::unix::fs::PermissionsExt;
199 std::fs::set_permissions(&tmp_path, std::fs::Permissions::from_mode(0o600))?;
200 }
201
202 std::fs::rename(&tmp_path, &self.path)?;
203 Ok(())
204 }
205
206 #[cfg(unix)]
208 fn check_permissions(&self) {
209 use std::os::unix::fs::PermissionsExt;
210 if let Ok(meta) = std::fs::metadata(&self.path) {
211 let mode = meta.permissions().mode() & 0o777;
212 if mode & 0o077 != 0 {
213 warn!(
214 "Credential file {:?} has loose permissions ({:o}). Tightening to 0600.",
215 self.path, mode
216 );
217 let _ =
218 std::fs::set_permissions(&self.path, std::fs::Permissions::from_mode(0o600));
219 }
220 }
221 }
222
223 pub fn path(&self) -> &Path {
225 &self.path
226 }
227}
228
229impl std::fmt::Debug for CredentialStore {
230 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
231 f.debug_struct("CredentialStore")
232 .field("path", &self.path)
233 .finish()
234 }
235}
236
237fn env_var_for_provider(provider: &str) -> Option<&'static str> {
239 ENV_VAR_MAP
240 .iter()
241 .find(|&&(p, _)| p == provider)
242 .map(|&(_, v)| v)
243}
244
245#[cfg(test)]
246#[path = "auth_tests.rs"]
247mod tests;