use std::collections::HashMap;
use std::sync::RwLock;
use async_trait::async_trait;
use secrecy::{ExposeSecret, SecretString};
use crate::error::CredentialError;
#[async_trait]
pub trait CredentialStore: Send + Sync + 'static {
async fn get(&self, service: &str, account: &str) -> Result<SecretString, CredentialError>;
async fn set(
&self,
service: &str,
account: &str,
secret: SecretString,
) -> Result<(), CredentialError>;
async fn delete(&self, service: &str, account: &str) -> Result<(), CredentialError>;
}
#[derive(Default)]
pub struct MemoryStore {
inner: RwLock<HashMap<(String, String), SecretString>>,
}
impl std::fmt::Debug for MemoryStore {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MemoryStore").finish_non_exhaustive()
}
}
impl MemoryStore {
#[must_use]
pub fn new() -> Self {
Self::default()
}
}
#[async_trait]
impl CredentialStore for MemoryStore {
async fn get(&self, service: &str, account: &str) -> Result<SecretString, CredentialError> {
let map = self.inner.read().map_err(|_| poisoned())?;
map.get(&(service.to_string(), account.to_string()))
.cloned()
.ok_or_else(|| CredentialError::NotFound { name: format!("{service}/{account}") })
}
async fn set(
&self,
service: &str,
account: &str,
secret: SecretString,
) -> Result<(), CredentialError> {
{
let mut map = self.inner.write().map_err(|_| poisoned())?;
map.insert((service.to_string(), account.to_string()), secret);
}
Ok(())
}
async fn delete(&self, service: &str, account: &str) -> Result<(), CredentialError> {
{
let mut map = self.inner.write().map_err(|_| poisoned())?;
map.remove(&(service.to_string(), account.to_string()));
}
Ok(())
}
}
fn poisoned() -> CredentialError {
CredentialError::Keychain("in-memory lock poisoned".to_string())
}
#[derive(Debug, Default)]
pub struct EnvStore;
impl EnvStore {
#[must_use]
pub const fn new() -> Self {
Self
}
}
#[async_trait]
impl CredentialStore for EnvStore {
async fn get(&self, _service: &str, account: &str) -> Result<SecretString, CredentialError> {
std::env::var(account)
.map(SecretString::from)
.map_err(|_| CredentialError::NotFound { name: account.to_string() })
}
async fn set(&self, _: &str, _: &str, _: SecretString) -> Result<(), CredentialError> {
Err(CredentialError::ReadOnly)
}
async fn delete(&self, _: &str, _: &str) -> Result<(), CredentialError> {
Err(CredentialError::ReadOnly)
}
}
pub struct LiteralStore {
secret: SecretString,
}
impl std::fmt::Debug for LiteralStore {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LiteralStore").finish_non_exhaustive()
}
}
impl LiteralStore {
#[must_use]
pub const fn new(secret: SecretString) -> Self {
Self { secret }
}
}
#[async_trait]
impl CredentialStore for LiteralStore {
async fn get(&self, _: &str, _: &str) -> Result<SecretString, CredentialError> {
Ok(self.secret.clone())
}
async fn set(&self, _: &str, _: &str, _: SecretString) -> Result<(), CredentialError> {
Err(CredentialError::ReadOnly)
}
async fn delete(&self, _: &str, _: &str) -> Result<(), CredentialError> {
Err(CredentialError::ReadOnly)
}
}
#[derive(Debug, Default)]
pub struct KeyringStore;
impl KeyringStore {
#[must_use]
pub const fn new() -> Self {
Self
}
}
#[async_trait]
impl CredentialStore for KeyringStore {
async fn get(&self, service: &str, account: &str) -> Result<SecretString, CredentialError> {
let service = service.to_string();
let account = account.to_string();
tokio::task::spawn_blocking(move || -> Result<SecretString, CredentialError> {
let entry = keyring::Entry::new(&service, &account)
.map_err(|e| CredentialError::Keychain(e.to_string()))?;
match entry.get_password() {
Ok(pw) => Ok(SecretString::from(pw)),
Err(keyring::Error::NoEntry) => {
Err(CredentialError::NotFound { name: format!("{service}/{account}") })
}
Err(e) => Err(CredentialError::Keychain(e.to_string())),
}
})
.await
.map_err(|e| CredentialError::Keychain(format!("join error: {e}")))?
}
async fn set(
&self,
service: &str,
account: &str,
secret: SecretString,
) -> Result<(), CredentialError> {
let service = service.to_string();
let account = account.to_string();
tokio::task::spawn_blocking(move || -> Result<(), CredentialError> {
let entry = keyring::Entry::new(&service, &account)
.map_err(|e| CredentialError::Keychain(e.to_string()))?;
entry
.set_password(secret.expose_secret())
.map_err(|e| CredentialError::Keychain(e.to_string()))
})
.await
.map_err(|e| CredentialError::Keychain(format!("join error: {e}")))?
}
async fn delete(&self, service: &str, account: &str) -> Result<(), CredentialError> {
let service = service.to_string();
let account = account.to_string();
tokio::task::spawn_blocking(move || -> Result<(), CredentialError> {
let entry = keyring::Entry::new(&service, &account)
.map_err(|e| CredentialError::Keychain(e.to_string()))?;
match entry.delete_credential() {
Ok(()) | Err(keyring::Error::NoEntry) => Ok(()),
Err(e) => Err(CredentialError::Keychain(e.to_string())),
}
})
.await
.map_err(|e| CredentialError::Keychain(format!("join error: {e}")))?
}
}