use crate::crypto::CryptoVault;
use crate::error::ReviewError;
#[derive(Debug, Clone)]
pub enum CredentialSource {
Env,
Keyring,
Encrypted {
api_key_encrypted: String,
},
Vault {
vault_url: String,
vault_secret_name: String,
},
}
impl CredentialSource {
pub async fn resolve(&self, password: Option<&str>) -> Result<String, ReviewError> {
match self {
CredentialSource::Env => {
let raw = std::env::var("AZURE_AI_API_KEY").map_err(|_| {
ReviewError::Config("AZURE_AI_API_KEY environment variable not set".to_string())
})?;
match password {
Some(pwd) => decrypt_api_key(pwd, &raw),
None => Ok(raw),
}
}
CredentialSource::Keyring => Err(ReviewError::Config(
"Keyring credential source not yet implemented".to_string(),
)),
CredentialSource::Encrypted { api_key_encrypted } => {
let password = password.ok_or_else(|| {
ReviewError::Config(
"Password required for encrypted credential source".to_string(),
)
})?;
decrypt_api_key(password, api_key_encrypted)
}
CredentialSource::Vault {
vault_url,
vault_secret_name,
} => Err(ReviewError::Config(format!(
"Vault credential source not yet implemented (vault: {}, secret: {})",
vault_url, vault_secret_name
))),
}
}
}
pub fn encrypt_api_key(password: &str, api_key: &str) -> Result<String, ReviewError> {
let vault = CryptoVault::default();
Ok(vault.encrypt(password, api_key)?)
}
pub fn decrypt_api_key(password: &str, encrypted_base64: &str) -> Result<String, ReviewError> {
let vault = CryptoVault::default();
Ok(vault.decrypt(password, encrypted_base64)?)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_sync::ENV_MUTEX;
#[test]
fn env_variant_is_constructible() {
let source = CredentialSource::Env;
let debug = format!("{:?}", source);
assert!(debug.contains("Env"), "Debug output should contain 'Env'");
}
#[test]
fn keyring_variant_is_constructible() {
let source = CredentialSource::Keyring;
let debug = format!("{:?}", source);
assert!(
debug.contains("Keyring"),
"Debug output should contain 'Keyring'"
);
}
#[test]
fn encrypted_variant_holds_blob() {
let blob = "c2FsdC4uLm5vbmNlLi4u".to_string();
let source = CredentialSource::Encrypted {
api_key_encrypted: blob.clone(),
};
if let CredentialSource::Encrypted { api_key_encrypted } = &source {
assert_eq!(api_key_encrypted, &blob);
} else {
panic!("Expected Encrypted variant");
}
}
#[test]
fn vault_variant_holds_url_and_name() {
let url = "https://myvault.vault.azure.net".to_string();
let name = "panoptico-api-key".to_string();
let source = CredentialSource::Vault {
vault_url: url.clone(),
vault_secret_name: name.clone(),
};
if let CredentialSource::Vault {
vault_url,
vault_secret_name,
} = &source
{
assert_eq!(vault_url, &url);
assert_eq!(vault_secret_name, &name);
} else {
panic!("Expected Vault variant");
}
}
#[tokio::test]
async fn resolve_env_reads_environment_variable() {
let key = "test-api-key-12345";
{
let _guard = ENV_MUTEX.lock().unwrap();
std::env::set_var("AZURE_AI_API_KEY", key);
}
let source = CredentialSource::Env;
let result = source.resolve(None).await;
{
let _guard = ENV_MUTEX.lock().unwrap();
std::env::remove_var("AZURE_AI_API_KEY");
}
assert_eq!(result.unwrap(), key);
}
#[tokio::test]
async fn resolve_env_missing_var_returns_config_error() {
{
let _guard = ENV_MUTEX.lock().unwrap();
std::env::remove_var("AZURE_AI_API_KEY");
}
let source = CredentialSource::Env;
let result = source.resolve(None).await;
assert!(
matches!(result, Err(ReviewError::Config(_))),
"Missing env var should return ReviewError::Config"
);
}
#[tokio::test]
async fn resolve_encrypted_without_password_returns_error() {
let source = CredentialSource::Encrypted {
api_key_encrypted: "dW51c2Vk".to_string(),
};
let result = source.resolve(None).await;
assert!(
matches!(result, Err(ReviewError::Config(_))),
"Encrypted source without password should return ReviewError::Config"
);
}
#[tokio::test]
async fn resolve_encrypted_with_password_decrypts() {
let api_key = "sk-ant-api03-test-key";
let password = "strong-password-123";
let encrypted = encrypt_api_key(password, api_key).unwrap();
let source = CredentialSource::Encrypted {
api_key_encrypted: encrypted,
};
let result = source.resolve(Some(password)).await.unwrap();
assert_eq!(result, api_key);
}
#[tokio::test]
async fn resolve_env_with_password_decrypts_blob() {
let api_key = "sk-ant-api03-env-encrypted";
let password = "env-password-456";
let blob = encrypt_api_key(password, api_key).unwrap();
{
let _guard = ENV_MUTEX.lock().unwrap();
std::env::set_var("AZURE_AI_API_KEY", &blob);
}
let source = CredentialSource::Env;
let result = source.resolve(Some(password)).await;
{
let _guard = ENV_MUTEX.lock().unwrap();
std::env::remove_var("AZURE_AI_API_KEY");
}
assert_eq!(result.unwrap(), api_key);
}
#[tokio::test]
async fn resolve_env_with_password_bad_blob_returns_error() {
{
let _guard = ENV_MUTEX.lock().unwrap();
std::env::set_var("AZURE_AI_API_KEY", "not-a-valid-encrypted-blob");
}
let source = CredentialSource::Env;
let result = source.resolve(Some("any-password")).await;
{
let _guard = ENV_MUTEX.lock().unwrap();
std::env::remove_var("AZURE_AI_API_KEY");
}
assert!(
matches!(result, Err(ReviewError::Config(_) | ReviewError::Parse(_))),
"Bad encrypted blob in env var should return a crypto error: {:?}",
result
);
}
}