vtcode_config/auth/
credentials.rs1use anyhow::{Context, Result, anyhow};
26use serde::{Deserialize, Serialize};
27
28#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(rename_all = "lowercase")]
37pub enum AuthCredentialsStoreMode {
38 Keyring,
42 File,
45 Auto,
47}
48
49impl Default for AuthCredentialsStoreMode {
50 fn default() -> Self {
53 Self::Keyring
54 }
55}
56
57impl AuthCredentialsStoreMode {
58 pub fn effective_mode(self) -> Self {
60 match self {
61 Self::Auto => {
62 if is_keyring_functional() {
64 Self::Keyring
65 } else {
66 tracing::debug!("Keyring not available, falling back to file storage");
67 Self::File
68 }
69 }
70 mode => mode,
71 }
72 }
73}
74
75pub(crate) fn is_keyring_functional() -> bool {
80 let test_user = format!("test_{}", std::process::id());
82 let entry = match keyring::Entry::new("vtcode", &test_user) {
83 Ok(e) => e,
84 Err(_) => return false,
85 };
86
87 if entry.set_password("test").is_err() {
89 return false;
90 }
91
92 let functional = entry.get_password().is_ok();
94
95 let _ = entry.delete_credential();
97
98 functional
99}
100
101pub struct CredentialStorage {
106 service: String,
107 user: String,
108}
109
110impl CredentialStorage {
111 pub fn new(service: impl Into<String>, user: impl Into<String>) -> Self {
117 Self {
118 service: service.into(),
119 user: user.into(),
120 }
121 }
122
123 pub fn store_with_mode(&self, value: &str, mode: AuthCredentialsStoreMode) -> Result<()> {
129 match mode.effective_mode() {
130 AuthCredentialsStoreMode::Keyring => self.store_keyring(value),
131 AuthCredentialsStoreMode::File => Err(anyhow!(
132 "File storage requires the file_storage feature or custom implementation"
133 )),
134 _ => unreachable!(),
135 }
136 }
137
138 pub fn store(&self, value: &str) -> Result<()> {
140 self.store_keyring(value)
141 }
142
143 fn store_keyring(&self, value: &str) -> Result<()> {
145 let entry = keyring::Entry::new(&self.service, &self.user)
146 .context("Failed to access OS keyring")?;
147
148 entry
149 .set_password(value)
150 .context("Failed to store credential in OS keyring")?;
151
152 tracing::debug!(
153 "Credential stored in OS keyring for {}/{}",
154 self.service,
155 self.user
156 );
157 Ok(())
158 }
159
160 pub fn load_with_mode(&self, mode: AuthCredentialsStoreMode) -> Result<Option<String>> {
164 match mode.effective_mode() {
165 AuthCredentialsStoreMode::Keyring => self.load_keyring(),
166 AuthCredentialsStoreMode::File => Err(anyhow!(
167 "File storage requires the file_storage feature or custom implementation"
168 )),
169 _ => unreachable!(),
170 }
171 }
172
173 pub fn load(&self) -> Result<Option<String>> {
177 self.load_keyring()
178 }
179
180 fn load_keyring(&self) -> Result<Option<String>> {
182 let entry = match keyring::Entry::new(&self.service, &self.user) {
183 Ok(e) => e,
184 Err(_) => return Ok(None),
185 };
186
187 match entry.get_password() {
188 Ok(value) => Ok(Some(value)),
189 Err(keyring::Error::NoEntry) => Ok(None),
190 Err(e) => Err(anyhow!("Failed to read from keyring: {}", e)),
191 }
192 }
193
194 pub fn clear_with_mode(&self, mode: AuthCredentialsStoreMode) -> Result<()> {
196 match mode.effective_mode() {
197 AuthCredentialsStoreMode::Keyring => self.clear_keyring(),
198 AuthCredentialsStoreMode::File => Ok(()), _ => unreachable!(),
200 }
201 }
202
203 pub fn clear(&self) -> Result<()> {
205 self.clear_keyring()
206 }
207
208 fn clear_keyring(&self) -> Result<()> {
210 let entry = match keyring::Entry::new(&self.service, &self.user) {
211 Ok(e) => e,
212 Err(_) => return Ok(()),
213 };
214
215 match entry.delete_credential() {
216 Ok(_) => {
217 tracing::debug!(
218 "Credential cleared from keyring for {}/{}",
219 self.service,
220 self.user
221 );
222 }
223 Err(keyring::Error::NoEntry) => {}
224 Err(e) => return Err(anyhow!("Failed to clear keyring entry: {}", e)),
225 }
226
227 Ok(())
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 #[test]
236 fn test_storage_mode_default_is_keyring() {
237 assert_eq!(
238 AuthCredentialsStoreMode::default(),
239 AuthCredentialsStoreMode::Keyring
240 );
241 }
242
243 #[test]
244 fn test_storage_mode_effective_mode() {
245 assert_eq!(
246 AuthCredentialsStoreMode::Keyring.effective_mode(),
247 AuthCredentialsStoreMode::Keyring
248 );
249 assert_eq!(
250 AuthCredentialsStoreMode::File.effective_mode(),
251 AuthCredentialsStoreMode::File
252 );
253
254 let auto_mode = AuthCredentialsStoreMode::Auto.effective_mode();
256 assert!(
257 auto_mode == AuthCredentialsStoreMode::Keyring
258 || auto_mode == AuthCredentialsStoreMode::File
259 );
260 }
261
262 #[test]
263 fn test_storage_mode_serialization() {
264 let keyring_json = serde_json::to_string(&AuthCredentialsStoreMode::Keyring).unwrap();
265 assert_eq!(keyring_json, "\"keyring\"");
266
267 let file_json = serde_json::to_string(&AuthCredentialsStoreMode::File).unwrap();
268 assert_eq!(file_json, "\"file\"");
269
270 let auto_json = serde_json::to_string(&AuthCredentialsStoreMode::Auto).unwrap();
271 assert_eq!(auto_json, "\"auto\"");
272
273 let parsed: AuthCredentialsStoreMode = serde_json::from_str("\"keyring\"").unwrap();
275 assert_eq!(parsed, AuthCredentialsStoreMode::Keyring);
276
277 let parsed: AuthCredentialsStoreMode = serde_json::from_str("\"file\"").unwrap();
278 assert_eq!(parsed, AuthCredentialsStoreMode::File);
279
280 let parsed: AuthCredentialsStoreMode = serde_json::from_str("\"auto\"").unwrap();
281 assert_eq!(parsed, AuthCredentialsStoreMode::Auto);
282 }
283
284 #[test]
285 fn test_credential_storage_new() {
286 let storage = CredentialStorage::new("vtcode", "test_key");
287 assert_eq!(storage.service, "vtcode");
288 assert_eq!(storage.user, "test_key");
289 }
290
291 #[test]
292 fn test_is_keyring_functional_check() {
293 let _functional = is_keyring_functional();
296 }
297}