use std::sync::Arc;
use secrecy::SecretString;
use crate::error::CredentialError;
use crate::reference::CredentialRef;
use crate::store::{CredentialStore, KeyringStore};
pub struct Resolver {
keychain: Arc<dyn CredentialStore>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ResolutionSource {
Env,
Keychain,
Literal,
FallbackEnv,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ResolutionOutcome {
Resolved(ResolutionSource),
LiteralRefusedInCi,
Missing,
}
impl Resolver {
#[must_use]
pub fn new(keychain: Arc<dyn CredentialStore>) -> Self {
Self { keychain }
}
#[must_use]
pub fn with_platform_default() -> Self {
Self::new(Arc::new(KeyringStore::new()))
}
pub async fn probe(&self, cref: &CredentialRef) -> Result<ResolutionOutcome, CredentialError> {
if let Some(name) = cref.env.as_deref() {
if std::env::var(name).is_ok() {
return Ok(ResolutionOutcome::Resolved(ResolutionSource::Env));
}
}
if let Some(keyref) = cref.keychain.as_ref() {
match self.keychain.get(&keyref.service, &keyref.account).await {
Ok(_) => return Ok(ResolutionOutcome::Resolved(ResolutionSource::Keychain)),
Err(CredentialError::NotFound { .. }) => { }
Err(other) => return Err(other),
}
}
if cref.literal.is_some() {
if is_ci() {
return Ok(ResolutionOutcome::LiteralRefusedInCi);
}
return Ok(ResolutionOutcome::Resolved(ResolutionSource::Literal));
}
if let Some(name) = cref.fallback_env.as_deref() {
if std::env::var(name).is_ok() {
return Ok(ResolutionOutcome::Resolved(ResolutionSource::FallbackEnv));
}
}
Ok(ResolutionOutcome::Missing)
}
pub async fn resolve(&self, cref: &CredentialRef) -> Result<SecretString, CredentialError> {
if let Some(name) = cref.env.as_deref() {
if let Ok(val) = std::env::var(name) {
return Ok(SecretString::from(val));
}
}
if let Some(keyref) = cref.keychain.as_ref() {
match self.keychain.get(&keyref.service, &keyref.account).await {
Ok(secret) => return Ok(secret),
Err(CredentialError::NotFound { .. }) => { }
Err(other) => return Err(other),
}
}
if let Some(literal) = cref.literal.as_ref() {
if is_ci() {
return Err(CredentialError::LiteralRefusedInCi);
}
return Ok(literal.clone());
}
if let Some(name) = cref.fallback_env.as_deref() {
if let Ok(val) = std::env::var(name) {
return Ok(SecretString::from(val));
}
}
Err(CredentialError::NotFound { name: diagnostic_name(cref) })
}
}
impl Default for Resolver {
fn default() -> Self {
Self::with_platform_default()
}
}
fn is_ci() -> bool {
std::env::var("CI").as_deref() == Ok("true")
}
fn diagnostic_name(cref: &CredentialRef) -> String {
cref.fallback_env
.as_deref()
.map(String::from)
.or_else(|| cref.env.as_deref().map(String::from))
.or_else(|| cref.keychain.as_ref().map(|k| format!("{}/{}", k.service, k.account)))
.unwrap_or_else(|| "<unnamed credential>".to_string())
}