pub mod binding_secrets;
pub mod file;
pub mod keyring;
pub mod memory;
use secrecy::SecretString;
use crate::error::OlError;
pub trait CredentialStore: Send + Sync {
fn store(&self, key: SecretString) -> Result<(), OlError>;
fn retrieve(&self) -> Result<SecretString, OlError>;
fn delete(&self) -> Result<(), OlError>;
}
pub(crate) use crate::error::{
OL_4200_TOKEN_EXPIRED, OL_4201_KEYRING_UNAVAILABLE, OL_4204_TOKEN_FILE_UNREADABLE,
};
pub fn keychain_suggestion() -> String {
"If running on headless Linux, install gnome-keyring-daemon, kwallet, or \
`keepassxc`; or set OPENLATCH_PROVIDER_SKIP_KEYRING=1 to use the encrypted \
file fallback at ~/.openlatch/provider/auth.toml."
.to_string()
}
pub fn retrieve_credential(
primary: &dyn CredentialStore,
fallback: &dyn CredentialStore,
) -> Result<SecretString, OlError> {
if let Ok(key) = primary.retrieve() {
return Ok(key);
}
if let Ok(val) = std::env::var("OPENLATCH_TOKEN") {
if !val.is_empty() {
return Ok(SecretString::from(val));
}
}
if let Ok(key) = fallback.retrieve() {
return Ok(key);
}
Err(OlError::new(
OL_4200_TOKEN_EXPIRED,
"No API key found in keychain, OPENLATCH_TOKEN env var, or encrypted file",
)
.with_suggestion("Run 'openlatch-provider login' to authenticate, or set OPENLATCH_TOKEN."))
}
pub use self::file::FileCredentialStore;
pub use self::keyring::KeyringCredentialStore;
pub use self::memory::InMemoryCredentialStore;
pub struct FallbackCredentialStore {
primary: Box<dyn CredentialStore>,
fallback: Box<dyn CredentialStore>,
}
impl FallbackCredentialStore {
pub fn new(primary: Box<dyn CredentialStore>, fallback: Box<dyn CredentialStore>) -> Self {
Self { primary, fallback }
}
}
impl CredentialStore for FallbackCredentialStore {
fn store(&self, key: SecretString) -> Result<(), OlError> {
self.primary.store(key)
}
fn retrieve(&self) -> Result<SecretString, OlError> {
retrieve_credential(self.primary.as_ref(), self.fallback.as_ref())
}
fn delete(&self) -> Result<(), OlError> {
self.primary.delete()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::auth::memory::InMemoryCredentialStore;
use secrecy::ExposeSecret;
#[test]
fn test_retrieve_credential_uses_primary_first() {
let primary = InMemoryCredentialStore::new();
primary
.store(SecretString::from("primary-key".to_string()))
.unwrap();
let fallback = InMemoryCredentialStore::new();
fallback
.store(SecretString::from("fallback-key".to_string()))
.unwrap();
let result = retrieve_credential(&primary, &fallback).unwrap();
assert_eq!(result.expose_secret(), "primary-key");
}
#[test]
#[ignore = "reads OPENLATCH_TOKEN env var; not safe in dev environments where it's set"]
fn test_retrieve_credential_falls_through_to_fallback_when_primary_empty() {
let primary = InMemoryCredentialStore::new();
let fallback = InMemoryCredentialStore::new();
fallback
.store(SecretString::from("fallback-key".to_string()))
.unwrap();
let result = retrieve_credential(&primary, &fallback).unwrap();
assert_eq!(result.expose_secret(), "fallback-key");
}
#[test]
#[ignore = "reads OPENLATCH_TOKEN env var; not safe in dev environments where it's set"]
fn test_retrieve_credential_returns_err_when_all_empty() {
let primary = InMemoryCredentialStore::new();
let fallback = InMemoryCredentialStore::new();
let result = retrieve_credential(&primary, &fallback);
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.code, OL_4200_TOKEN_EXPIRED);
}
#[test]
fn test_secret_string_debug_does_not_leak_value() {
let secret = SecretString::from("my-api-key".to_string());
let debug_output = format!("{:?}", secret);
assert!(
!debug_output.contains("my-api-key"),
"SecretString Debug output must not contain the actual secret: {debug_output}"
);
}
#[test]
#[ignore = "reads OPENLATCH_TOKEN env var; not safe in dev environments where it's set"]
fn test_retrieve_credential_env_var_override() {
let primary = InMemoryCredentialStore::new();
let fallback = InMemoryCredentialStore::new();
let result = retrieve_credential(&primary, &fallback);
assert!(result.is_err());
assert_eq!(result.unwrap_err().code, OL_4200_TOKEN_EXPIRED);
}
}