1pub mod error;
80pub mod keyring_config;
81
82pub use error::KeyringError;
83pub use keyring_config::{Keyring, KeyringConfig};
84
85use figment2::{
86 providers::Serialized,
87 value::{Dict, Map, Value},
88 Error, Figment, Metadata, Profile, Provider,
89};
90use std::sync::Arc;
91
92pub trait KeyringKeyMapping {
118 fn service_key(&self) -> &str;
119
120 fn keyrings_key(&self) -> &str;
121
122 fn optional_key(&self) -> &str {
123 "optional"
124 }
125}
126
127pub struct KeyringProvider {
146 config_figment: Arc<Figment>,
147 credential_name: String,
148 config_key: Option<String>,
149 profile: Option<Profile>,
150}
151
152impl KeyringProvider {
153 pub fn configured_by(config_figment: Figment, credential_name: &str) -> Self {
154 Self {
155 config_figment: Arc::new(config_figment),
156 credential_name: credential_name.into(),
157 config_key: None,
158 profile: None,
159 }
160 }
161
162 pub fn configured_by_with_mapping<M: KeyringKeyMapping>(
163 config_figment: Figment,
164 mapping: &M,
165 credential_name: &str,
166 ) -> std::result::Result<Self, Error> {
167 let service: String = config_figment.extract_inner(mapping.service_key())?;
168 let keyrings: Vec<Keyring> = config_figment.extract_inner(mapping.keyrings_key())?;
169 let optional: bool = config_figment
170 .extract_inner(mapping.optional_key())
171 .unwrap_or(false);
172
173 let config = KeyringConfig {
174 service,
175 keyrings,
176 optional,
177 };
178
179 let figment = Figment::from(Serialized::defaults(config));
180 Ok(Self::configured_by(figment, credential_name))
181 }
182
183 pub fn focused(&self, path: &str) -> Self {
230 Self {
231 config_figment: Arc::new(self.config_figment.focus(path)),
232 credential_name: self.credential_name.clone(),
233 config_key: self.config_key.clone(),
234 profile: self.profile.clone(),
235 }
236 }
237
238 pub fn new(service: &str, credential_name: &str) -> Self {
239 let config = KeyringConfig {
240 service: service.into(),
241 keyrings: vec![Keyring::User],
242 optional: false,
243 };
244 let figment = Figment::from(Serialized::defaults(config));
245 Self::configured_by(figment, credential_name)
246 }
247
248 pub fn system(service: &str, credential_name: &str) -> Self {
249 let config = KeyringConfig {
250 service: service.into(),
251 keyrings: vec![Keyring::System],
252 optional: false,
253 };
254 let figment = Figment::from(Serialized::defaults(config));
255 Self::configured_by(figment, credential_name)
256 }
257
258 pub fn as_key(mut self, key: &str) -> Self {
259 self.config_key = Some(key.into());
260 self
261 }
262
263 pub fn with_profile(mut self, profile: Profile) -> Self {
264 self.profile = Some(profile);
265 self
266 }
267}
268
269impl Provider for KeyringProvider {
270 fn metadata(&self) -> Metadata {
271 Metadata::named("keyring")
272 }
273
274 fn data(&self) -> std::result::Result<Map<Profile, Dict>, Error> {
275 let config: KeyringConfig = self
276 .config_figment
277 .extract()
278 .map_err(|e| Error::from(format!("keyring config: {}", e)))?;
279
280 let secret = self.search_keyrings(&config)?;
281
282 let key = self.config_key.as_ref().unwrap_or(&self.credential_name);
283
284 let profile = self.profile.clone().unwrap_or_default();
285 let mut dict = Dict::new();
286
287 match secret {
288 Some(value) => {
289 dict.insert(key.clone(), Value::from(value));
290 }
291 None if config.optional => {}
292 None => {
293 return Err(Error::from(format!(
294 "secret '{}' not found in any keyring",
295 self.credential_name
296 )));
297 }
298 }
299
300 let mut map = Map::new();
301 map.insert(profile, dict);
302 Ok(map)
303 }
304}
305
306impl KeyringProvider {
307 fn search_keyrings(
308 &self,
309 config: &KeyringConfig,
310 ) -> std::result::Result<Option<String>, Error> {
311 for keyring in &config.keyrings {
312 match self.get_from_keyring(keyring, &config.service, &self.credential_name) {
313 Ok(secret) => return Ok(Some(secret)),
314 Err(KeyringError::NotFound(_)) => continue,
315 Err(e) => {
316 if config.optional {
317 continue;
318 } else {
319 return Err(Error::from(e.to_string()));
320 }
321 }
322 }
323 }
324 Ok(None)
325 }
326
327 fn get_from_keyring(
328 &self,
329 keyring: &Keyring,
330 service: &str,
331 username: &str,
332 ) -> std::result::Result<String, KeyringError> {
333 keyring_config::backend::get_secret(keyring, service, username)
334 }
335}
336
337#[cfg(test)]
338mod tests {
339 use super::*;
340
341 #[test]
342 fn test_keyring_from_str() {
343 assert_eq!(Keyring::from("user"), Keyring::User);
344 assert_eq!(Keyring::from("system"), Keyring::System);
345 assert_eq!(
346 Keyring::from("custom-keyring"),
347 Keyring::Named("custom-keyring".into())
348 );
349 }
350
351 #[test]
352 fn test_keyring_default() {
353 assert_eq!(Keyring::default(), Keyring::User);
354 }
355
356 #[test]
357 fn test_keyring_provider_new() {
358 let provider = KeyringProvider::new("test-app", "test-key");
359 assert_eq!(provider.credential_name, "test-key");
360 }
361
362 #[test]
363 fn test_keyring_provider_system() {
364 let provider = KeyringProvider::system("test-app", "test-key");
365 assert_eq!(provider.credential_name, "test-key");
366 }
367
368 #[test]
369 fn test_keyring_provider_as_key() {
370 let provider = KeyringProvider::new("test-app", "test-key").as_key("custom.config.key");
371 assert_eq!(provider.config_key, Some("custom.config.key".into()));
372 }
373
374 #[test]
375 fn test_keyring_provider_with_profile() {
376 let profile = Profile::from("production");
377 let provider = KeyringProvider::new("test-app", "test-key").with_profile(profile.clone());
378 assert_eq!(provider.profile, Some(profile));
379 }
380
381 #[test]
382 fn test_keyring_provider_focused() {
383 let config_figment = Figment::new();
384 let provider = KeyringProvider::configured_by(config_figment, "api_key");
385 let focused_provider = provider.focused("keyring");
386 assert_eq!(focused_provider.credential_name, "api_key");
387 assert_eq!(focused_provider.config_key, None);
388 assert_eq!(focused_provider.profile, None);
389 }
390
391 #[test]
392 fn test_keyring_provider_focused_preserves_config_key() {
393 let config_figment = Figment::new();
394 let provider =
395 KeyringProvider::configured_by(config_figment, "api_key").as_key("custom_key");
396 let focused_provider = provider.focused("keyring");
397 assert_eq!(focused_provider.config_key, Some("custom_key".into()));
398 }
399
400 #[test]
401 fn test_keyring_provider_focused_preserves_profile() {
402 let config_figment = Figment::new();
403 let profile = Profile::from("production");
404 let provider =
405 KeyringProvider::configured_by(config_figment, "api_key").with_profile(profile.clone());
406 let focused_provider = provider.focused("keyring");
407 assert_eq!(focused_provider.profile, Some(profile));
408 }
409
410 #[test]
411 fn test_key_mapping_default_keys() {
412 struct DefaultMapping;
413
414 impl KeyringKeyMapping for DefaultMapping {
415 fn service_key(&self) -> &str {
416 "service"
417 }
418 fn keyrings_key(&self) -> &str {
419 "keyrings"
420 }
421 }
422
423 let mapping = DefaultMapping;
424 let config_figment = Figment::from(Serialized::defaults(KeyringConfig {
425 service: "myapp".to_string(),
426 keyrings: vec![Keyring::User],
427 optional: true,
428 }));
429
430 let provider =
431 KeyringProvider::configured_by_with_mapping(config_figment, &mapping, "test-key")
432 .unwrap();
433
434 assert_eq!(provider.credential_name, "test-key");
435 }
436
437 #[test]
438 fn test_key_mapping_custom_keys() {
439 struct CustomMapping;
440
441 impl KeyringKeyMapping for CustomMapping {
442 fn service_key(&self) -> &str {
443 "app_name"
444 }
445 fn keyrings_key(&self) -> &str {
446 "stores"
447 }
448 fn optional_key(&self) -> &str {
449 "allow_missing"
450 }
451 }
452
453 let mapping = CustomMapping;
454 let config_figment = Figment::from(Serialized::defaults(serde_json::json!({
455 "app_name": "myapp",
456 "stores": ["user", "system"],
457 "allow_missing": false,
458 })));
459
460 let provider =
461 KeyringProvider::configured_by_with_mapping(config_figment, &mapping, "api_key")
462 .unwrap();
463
464 assert_eq!(provider.credential_name, "api_key");
465 }
466
467 #[test]
468 fn test_key_mapping_default_optional() {
469 struct MappingWithoutOptional;
470
471 impl KeyringKeyMapping for MappingWithoutOptional {
472 fn service_key(&self) -> &str {
473 "service"
474 }
475 fn keyrings_key(&self) -> &str {
476 "keyrings"
477 }
478 }
479
480 let mapping = MappingWithoutOptional;
481 let config_figment = Figment::from(Serialized::defaults(serde_json::json!({
482 "service": "myapp",
483 "keyrings": ["user"],
484 })));
485
486 let provider = KeyringProvider::configured_by_with_mapping(
487 config_figment,
488 &mapping,
489 "test-credential",
490 )
491 .unwrap();
492
493 assert_eq!(provider.credential_name, "test-credential");
494 }
495
496 #[test]
497 fn test_key_mapping_nested_keys() {
498 struct NestedMapping;
499
500 impl KeyringKeyMapping for NestedMapping {
501 fn service_key(&self) -> &str {
502 "secrets.service"
503 }
504 fn keyrings_key(&self) -> &str {
505 "secrets.backends"
506 }
507 fn optional_key(&self) -> &str {
508 "secrets.optional"
509 }
510 }
511
512 let mapping = NestedMapping;
513 let config_figment = Figment::from(Serialized::defaults(serde_json::json!({
514 "secrets": {
515 "service": "myapp",
516 "backends": ["system"],
517 "optional": true,
518 }
519 })));
520
521 let provider =
522 KeyringProvider::configured_by_with_mapping(config_figment, &mapping, "db_password")
523 .unwrap();
524
525 assert_eq!(provider.credential_name, "db_password");
526 }
527}