use super::error::{ConfigError, Result};
use std::env;
const KEYRING_PREFIX: &str = "keyring:";
#[cfg(feature = "secure-storage")]
const SERVICE_NAME: &str = "redisctl";
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub enum CredentialStorage {
#[cfg(feature = "secure-storage")]
Keyring,
Plaintext,
}
pub struct CredentialStore {
#[allow(dead_code)]
storage: CredentialStorage,
}
impl Default for CredentialStore {
fn default() -> Self {
Self::new()
}
}
impl CredentialStore {
pub fn new() -> Self {
#[cfg(feature = "secure-storage")]
{
if Self::is_keyring_available() {
Self {
storage: CredentialStorage::Keyring,
}
} else {
Self {
storage: CredentialStorage::Plaintext,
}
}
}
#[cfg(not(feature = "secure-storage"))]
{
Self {
storage: CredentialStorage::Plaintext,
}
}
}
#[cfg(feature = "secure-storage")]
fn is_keyring_available() -> bool {
match keyring::Entry::new(SERVICE_NAME, "__test__") {
Ok(entry) => {
let _ = entry.get_password();
true
}
Err(_) => false,
}
}
#[allow(dead_code)]
pub fn store_credential(&self, key: &str, value: &str) -> Result<String> {
#[cfg(feature = "secure-storage")]
{
match self.storage {
CredentialStorage::Keyring => {
let entry = keyring::Entry::new(SERVICE_NAME, key)
.map_err(|e| ConfigError::KeyringError(e.to_string()))?;
entry.set_password(value).map_err(|e| {
ConfigError::KeyringError(format!(
"Failed to store credential in keyring: {}",
e
))
})?;
Ok(format!("{}{}", KEYRING_PREFIX, key))
}
CredentialStorage::Plaintext => Ok(value.to_string()),
}
}
#[cfg(not(feature = "secure-storage"))]
{
let _ = key; Ok(value.to_string())
}
}
pub fn get_credential(&self, value: &str, env_var: Option<&str>) -> Result<String> {
if let Some(var) = env_var
&& let Ok(env_value) = env::var(var)
{
return Ok(env_value);
}
if value.starts_with(KEYRING_PREFIX) {
#[cfg(feature = "secure-storage")]
{
let key = value.trim_start_matches(KEYRING_PREFIX);
let entry = keyring::Entry::new(SERVICE_NAME, key)
.map_err(|e| ConfigError::KeyringError(e.to_string()))?;
entry.get_password().map_err(|e| {
ConfigError::KeyringError(format!(
"Failed to retrieve credential '{}' from keyring: {}",
key, e
))
})
}
#[cfg(not(feature = "secure-storage"))]
{
Err(ConfigError::CredentialError(
"Credential references keyring but secure-storage feature is not enabled"
.to_string(),
))
}
} else {
Ok(value.to_string())
}
}
#[allow(dead_code)]
pub fn delete_credential(&self, key: &str) -> Result<()> {
#[cfg(feature = "secure-storage")]
{
match self.storage {
CredentialStorage::Keyring => {
let entry = keyring::Entry::new(SERVICE_NAME, key)
.map_err(|e| ConfigError::KeyringError(e.to_string()))?;
match entry.delete_credential() {
Ok(()) => Ok(()),
Err(keyring::Error::NoEntry) => Ok(()), Err(e) => Err(ConfigError::KeyringError(format!(
"Failed to delete credential from keyring: {}",
e
))),
}
}
CredentialStorage::Plaintext => Ok(()), }
}
#[cfg(not(feature = "secure-storage"))]
{
let _ = key; Ok(()) }
}
#[allow(dead_code)]
pub fn is_keyring_reference(value: &str) -> bool {
value.starts_with(KEYRING_PREFIX)
}
#[allow(dead_code)]
pub fn storage_backend(&self) -> &str {
#[cfg(feature = "secure-storage")]
{
match self.storage {
CredentialStorage::Keyring => "keyring",
CredentialStorage::Plaintext => "plaintext",
}
}
#[cfg(not(feature = "secure-storage"))]
{
"plaintext"
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_plaintext_storage() {
let store = CredentialStore::new();
let result = store.get_credential("my-api-key", None).unwrap();
assert_eq!(result, "my-api-key");
}
#[test]
fn test_env_var_override() {
unsafe {
env::set_var("TEST_CREDENTIAL", "env-value");
}
let store = CredentialStore::new();
let result = store
.get_credential("config-value", Some("TEST_CREDENTIAL"))
.unwrap();
assert_eq!(result, "env-value");
unsafe {
env::remove_var("TEST_CREDENTIAL");
}
}
#[test]
fn test_keyring_reference_detection() {
assert!(CredentialStore::is_keyring_reference("keyring:my-key"));
assert!(!CredentialStore::is_keyring_reference("my-key"));
assert!(!CredentialStore::is_keyring_reference(""));
}
#[cfg(feature = "secure-storage")]
#[test]
#[ignore = "Requires keyring service to be available"]
fn test_keyring_storage() {
let store = CredentialStore::new();
let key = "test-credential";
let value = "test-value";
let reference = store.store_credential(key, value).unwrap();
assert!(reference.starts_with(KEYRING_PREFIX));
let retrieved = store.get_credential(&reference, None).unwrap();
assert_eq!(retrieved, value);
let _ = store.delete_credential(key);
}
}