rustant_core/
credentials.rs1use std::collections::HashMap;
9use std::sync::Mutex;
10
11#[derive(Debug, thiserror::Error)]
13pub enum CredentialError {
14 #[error("Credential not found for {service}:{account}")]
15 NotFound { service: String, account: String },
16
17 #[error("Failed to store credential: {message}")]
18 StoreFailed { message: String },
19
20 #[error("Failed to delete credential: {message}")]
21 DeleteFailed { message: String },
22
23 #[error("Keyring backend not available: {message}")]
24 BackendUnavailable { message: String },
25}
26
27pub trait CredentialStore: Send + Sync {
29 fn store_key(&self, provider: &str, api_key: &str) -> Result<(), CredentialError>;
31
32 fn get_key(&self, provider: &str) -> Result<String, CredentialError>;
34
35 fn delete_key(&self, provider: &str) -> Result<(), CredentialError>;
37
38 fn has_key(&self, provider: &str) -> bool;
40}
41
42pub struct KeyringCredentialStore {
47 service: String,
48}
49
50impl KeyringCredentialStore {
51 pub fn new() -> Self {
53 Self {
54 service: "rustant".to_string(),
55 }
56 }
57
58 pub fn account_name(provider: &str) -> String {
60 format!("provider:{}", provider)
61 }
62}
63
64impl Default for KeyringCredentialStore {
65 fn default() -> Self {
66 Self::new()
67 }
68}
69
70impl CredentialStore for KeyringCredentialStore {
71 fn store_key(&self, provider: &str, api_key: &str) -> Result<(), CredentialError> {
72 let account = Self::account_name(provider);
73 let entry = keyring::Entry::new(&self.service, &account).map_err(|e| {
74 CredentialError::BackendUnavailable {
75 message: e.to_string(),
76 }
77 })?;
78 entry
79 .set_password(api_key)
80 .map_err(|e| CredentialError::StoreFailed {
81 message: e.to_string(),
82 })
83 }
84
85 fn get_key(&self, provider: &str) -> Result<String, CredentialError> {
86 let account = Self::account_name(provider);
87 let entry = keyring::Entry::new(&self.service, &account).map_err(|e| {
88 CredentialError::BackendUnavailable {
89 message: e.to_string(),
90 }
91 })?;
92 entry.get_password().map_err(|e| match e {
93 keyring::Error::NoEntry => CredentialError::NotFound {
94 service: self.service.clone(),
95 account,
96 },
97 other => CredentialError::StoreFailed {
98 message: other.to_string(),
99 },
100 })
101 }
102
103 fn delete_key(&self, provider: &str) -> Result<(), CredentialError> {
104 let account = Self::account_name(provider);
105 let entry = keyring::Entry::new(&self.service, &account).map_err(|e| {
106 CredentialError::BackendUnavailable {
107 message: e.to_string(),
108 }
109 })?;
110 entry
111 .delete_credential()
112 .map_err(|e| CredentialError::DeleteFailed {
113 message: e.to_string(),
114 })
115 }
116
117 fn has_key(&self, provider: &str) -> bool {
118 self.get_key(provider).is_ok()
119 }
120}
121
122pub struct InMemoryCredentialStore {
126 store: Mutex<HashMap<String, String>>,
127}
128
129impl InMemoryCredentialStore {
130 pub fn new() -> Self {
132 Self {
133 store: Mutex::new(HashMap::new()),
134 }
135 }
136}
137
138impl Default for InMemoryCredentialStore {
139 fn default() -> Self {
140 Self::new()
141 }
142}
143
144impl CredentialStore for InMemoryCredentialStore {
145 fn store_key(&self, provider: &str, api_key: &str) -> Result<(), CredentialError> {
146 let account = KeyringCredentialStore::account_name(provider);
147 self.store
148 .lock()
149 .unwrap()
150 .insert(account, api_key.to_string());
151 Ok(())
152 }
153
154 fn get_key(&self, provider: &str) -> Result<String, CredentialError> {
155 let account = KeyringCredentialStore::account_name(provider);
156 self.store
157 .lock()
158 .unwrap()
159 .get(&account)
160 .cloned()
161 .ok_or_else(|| CredentialError::NotFound {
162 service: "rustant".to_string(),
163 account,
164 })
165 }
166
167 fn delete_key(&self, provider: &str) -> Result<(), CredentialError> {
168 let account = KeyringCredentialStore::account_name(provider);
169 self.store.lock().unwrap().remove(&account);
170 Ok(())
171 }
172
173 fn has_key(&self, provider: &str) -> bool {
174 let account = KeyringCredentialStore::account_name(provider);
175 self.store.lock().unwrap().contains_key(&account)
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 fn make_store() -> InMemoryCredentialStore {
184 InMemoryCredentialStore::new()
185 }
186
187 #[test]
188 fn test_store_and_retrieve_key() {
189 let store = make_store();
190 store.store_key("openai", "sk-test-123").unwrap();
191 assert_eq!(store.get_key("openai").unwrap(), "sk-test-123");
192 }
193
194 #[test]
195 fn test_get_nonexistent_key() {
196 let store = make_store();
197 let result = store.get_key("nonexistent");
198 assert!(result.is_err());
199 assert!(matches!(
200 result.unwrap_err(),
201 CredentialError::NotFound { .. }
202 ));
203 }
204
205 #[test]
206 fn test_delete_key() {
207 let store = make_store();
208 store.store_key("anthropic", "sk-ant-test").unwrap();
209 store.delete_key("anthropic").unwrap();
210 assert!(!store.has_key("anthropic"));
211 }
212
213 #[test]
214 fn test_has_key() {
215 let store = make_store();
216 assert!(!store.has_key("openai"));
217 store.store_key("openai", "sk-test").unwrap();
218 assert!(store.has_key("openai"));
219 }
220
221 #[test]
222 fn test_overwrite_key() {
223 let store = make_store();
224 store.store_key("openai", "sk-old").unwrap();
225 store.store_key("openai", "sk-new").unwrap();
226 assert_eq!(store.get_key("openai").unwrap(), "sk-new");
227 }
228
229 #[test]
230 fn test_account_name_format() {
231 assert_eq!(
232 KeyringCredentialStore::account_name("openai"),
233 "provider:openai"
234 );
235 assert_eq!(
236 KeyringCredentialStore::account_name("anthropic"),
237 "provider:anthropic"
238 );
239 }
240}