use super::error::CredentialError;
use super::handle::CredentialRef;
use super::principal::{Principal, Scope};
use super::registry::env_var_for;
use super::secret::Secret;
use super::{KeyStore, default_store, dotenv, resolve_key_with};
pub fn resolve(
credential: &CredentialRef,
principal: &Principal,
scope: &Scope,
) -> Result<Secret<String>, CredentialError> {
dotenv::load_env_local_once();
resolve_with(credential, principal, scope, default_store().as_ref())
}
pub fn resolve_with(
credential: &CredentialRef,
principal: &Principal,
scope: &Scope,
store: &dyn KeyStore,
) -> Result<Secret<String>, CredentialError> {
if env_var_for(credential.provider()).is_none() {
return Err(CredentialError::Missing {
credential: credential.clone(),
principal: principal.clone(),
hint: format!(
" — provider `{}` is not in the credential registry \
(`trusty_common::credentials::registry::REGISTRY`, #4564)",
credential.provider()
),
});
}
let _ = scope;
match resolve_key_with(&store_key(credential), store) {
Some(value) => Ok(Secret::new(value)),
None => Err(CredentialError::Missing {
credential: credential.clone(),
principal: principal.clone(),
hint: String::new(),
}),
}
}
fn store_key(credential: &CredentialRef) -> String {
credential.to_string()
}
pub trait FromCredential: Sized {
fn from_credential(
secret: &Secret<String>,
credential: &CredentialRef,
principal: &Principal,
) -> Result<Self, CredentialError>;
}
pub fn resolve_client<T: FromCredential>(
credential: &CredentialRef,
principal: &Principal,
scope: &Scope,
) -> Result<T, CredentialError> {
let secret = resolve(credential, principal, scope)?;
T::from_credential(&secret, credential, principal)
}
pub fn resolve_client_with<T: FromCredential>(
credential: &CredentialRef,
principal: &Principal,
scope: &Scope,
store: &dyn KeyStore,
) -> Result<T, CredentialError> {
let secret = resolve_with(credential, principal, scope, store)?;
T::from_credential(&secret, credential, principal)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::credentials::MemoryKeyStore;
use serial_test::serial;
struct AuthenticatedClient {
credential: String,
credential_len: usize,
}
impl FromCredential for AuthenticatedClient {
fn from_credential(
secret: &Secret<String>,
credential: &CredentialRef,
_principal: &Principal,
) -> Result<Self, CredentialError> {
Ok(Self {
credential: credential.to_string(),
credential_len: secret.expose().len(),
})
}
}
#[test]
#[serial(dotenv_credential_env)]
fn oauth_and_api_key_shapes_resolve_through_one_entry_point() {
let store = MemoryKeyStore::new();
store
.set("google-oauth", "oauth-refresh-token-value")
.unwrap();
store.set("brave", "plain-api-key-value").unwrap();
unsafe {
std::env::remove_var("GOOGLE_OAUTH_CLIENT_SECRET");
std::env::remove_var("BRAVE_API_KEY");
}
let principal = Principal::Operator;
let scope = Scope::read();
for (name, expected) in [
("google-oauth", "oauth-refresh-token-value"),
("brave", "plain-api-key-value"),
] {
let cred = CredentialRef::parse(name).unwrap();
let secret = resolve_with(&cred, &principal, &scope, &store).unwrap();
assert_eq!(secret.expose(), expected, "shape {name} did not resolve");
}
}
#[test]
#[serial(dotenv_credential_env)]
fn unregistered_provider_is_missing_and_names_the_registry() {
let store = MemoryKeyStore::new();
store
.set("not-a-provider", "value-that-must-not-be-returned")
.unwrap();
let cred = CredentialRef::parse("not-a-provider").unwrap();
let err = resolve_with(&cred, &Principal::Operator, &Scope::read(), &store).unwrap_err();
assert!(matches!(err, CredentialError::Missing { .. }));
let rendered = err.to_string();
assert!(
rendered.contains("registry"),
"no registry hint: {rendered}"
);
assert!(rendered.contains("not-a-provider"), "no ref: {rendered}");
assert!(
!rendered.contains("value-that-must-not-be-returned"),
"leaked the stored value: {rendered}"
);
}
#[test]
#[serial(dotenv_credential_env)]
fn absent_credential_is_missing() {
unsafe {
std::env::remove_var("LINEAR_API_KEY");
}
let store = MemoryKeyStore::new();
let cred = CredentialRef::parse("linear").unwrap();
let err = resolve_with(&cred, &Principal::Operator, &Scope::read(), &store).unwrap_err();
assert!(matches!(err, CredentialError::Missing { hint, .. } if hint.is_empty()));
}
#[test]
#[serial(dotenv_credential_env)]
fn resolve_client_never_hands_back_the_string() {
let store = MemoryKeyStore::new();
let value = "sk-brave-abcdefghijklmnop";
store.set("brave", value).unwrap();
unsafe {
std::env::remove_var("BRAVE_API_KEY");
}
let cred = CredentialRef::parse("brave").unwrap();
let client: AuthenticatedClient =
resolve_client_with(&cred, &Principal::Operator, &Scope::read(), &store).unwrap();
assert_eq!(client.credential, "brave");
assert_eq!(client.credential_len, value.len());
}
#[test]
#[serial(dotenv_credential_env)]
fn resolved_secret_does_not_render_in_debug_or_display() {
let store = MemoryKeyStore::new();
let value = concat!("xo", "xb", "-2314151234-2321313111-QwErTyUiOpAsDf");
store.set("slack", value).unwrap();
unsafe {
std::env::remove_var("SLACK_BOT_TOKEN");
}
let cred = CredentialRef::parse("slack").unwrap();
let secret = resolve_with(&cred, &Principal::Operator, &Scope::read(), &store).unwrap();
let rendered = format!("{secret} {secret:?} {cred}");
assert!(!rendered.contains(value), "leaked: {rendered}");
assert!(
!rendered.contains(concat!("xo", "xb")),
"leaked prefix: {rendered}"
);
assert!(
rendered.contains("slack"),
"handle should print: {rendered}"
);
}
#[test]
#[serial(dotenv_credential_env)]
fn qualified_refs_reach_distinct_store_rows() {
let store = MemoryKeyStore::new();
store.set("github/work", "work-token").unwrap();
store.set("github/personal", "personal-token").unwrap();
unsafe {
std::env::remove_var("GITHUB_TOKEN");
}
let principal = Principal::Operator;
let scope = Scope::read();
let work = CredentialRef::parse("github/work").unwrap();
let personal = CredentialRef::parse("github/personal").unwrap();
assert_eq!(
resolve_with(&work, &principal, &scope, &store)
.unwrap()
.expose(),
"work-token"
);
assert_eq!(
resolve_with(&personal, &principal, &scope, &store)
.unwrap()
.expose(),
"personal-token"
);
}
}