use super::{Provider, ProviderUrl};
use crate::{Result, SecretSpecError};
use keyring::Entry;
use secrecy::{ExposeSecret, SecretString};
use serde::{Deserialize, Serialize};
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct KeyringConfig {
pub folder_prefix: Option<String>,
}
impl TryFrom<&ProviderUrl> for KeyringConfig {
type Error = SecretSpecError;
fn try_from(url: &ProviderUrl) -> std::result::Result<Self, Self::Error> {
if url.scheme() != "keyring" {
return Err(SecretSpecError::ProviderOperationFailed(format!(
"Invalid scheme '{}' for keyring provider",
url.scheme()
)));
}
let mut config = Self::default();
if let Some(host) = url.host() {
let path = url.path();
config.folder_prefix = Some(format!("{}{}", host, path));
}
Ok(config)
}
}
pub struct KeyringProvider {
config: KeyringConfig,
}
crate::register_provider! {
struct: KeyringProvider,
config: KeyringConfig,
name: "keyring",
description: "Uses system keychain (Recommended)",
schemes: ["keyring"],
examples: ["keyring://", "keyring://secretspec/shared/{profile}/{key}"],
}
impl KeyringProvider {
pub fn new(config: KeyringConfig) -> Self {
Self { config }
}
fn format_service(&self, project: &str, profile: &str, key: &str) -> String {
let format_string = self
.config
.folder_prefix
.as_deref()
.unwrap_or("secretspec/{project}/{profile}/{key}");
format_string
.replace("{project}", project)
.replace("{profile}", profile)
.replace("{key}", key)
}
}
impl Provider for KeyringProvider {
fn name(&self) -> &'static str {
Self::PROVIDER_NAME
}
fn uri(&self) -> String {
if let Some(ref prefix) = self.config.folder_prefix {
format!("keyring://{}", ProviderUrl::encode(prefix))
} else {
"keyring".to_string()
}
}
fn get(&self, project: &str, key: &str, profile: &str) -> Result<Option<SecretString>> {
let service = self.format_service(project, profile, key);
let username = whoami::username()
.map_err(|e| SecretSpecError::ProviderOperationFailed(e.to_string()))?;
let entry = Entry::new(&service, &username)?;
match entry.get_password() {
Ok(password) => Ok(Some(SecretString::new(password.into()))),
Err(keyring::Error::NoEntry) => Ok(None),
Err(e) => Err(e.into()),
}
}
fn set(&self, project: &str, key: &str, value: &SecretString, profile: &str) -> Result<()> {
let service = self.format_service(project, profile, key);
let username = whoami::username()
.map_err(|e| SecretSpecError::ProviderOperationFailed(e.to_string()))?;
let entry = Entry::new(&service, &username)?;
entry.set_password(value.expose_secret())?;
Ok(())
}
}