use std::process::Command;
use anyhow::{Context, Result};
pub const KEYRING_SERVICE: &str = "cleanstart.com/cleanlib-enrich";
pub const KEYRING_USER: &str = "default";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BearerSource {
EnvVar,
Keyring,
GcloudImpersonation,
}
impl std::fmt::Display for BearerSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
BearerSource::EnvVar => "env",
BearerSource::Keyring => "keyring",
BearerSource::GcloudImpersonation => "gcloud-impersonation",
};
f.write_str(s)
}
}
pub fn resolve_bearer() -> Result<(BearerSource, String)> {
if let Ok(b) = std::env::var("CLEANLIB_ENRICH_BEARER") {
if !b.is_empty() {
return Ok((BearerSource::EnvVar, b));
}
}
if let Ok(b) = std::env::var("CLEANLIBRARY_API_KEY") {
if !b.is_empty() {
return Ok((BearerSource::EnvVar, b));
}
}
if let Ok(entry) = keyring::Entry::new(KEYRING_SERVICE, KEYRING_USER) {
if let Ok(b) = entry.get_password() {
if !b.is_empty() {
return Ok((BearerSource::Keyring, b));
}
}
}
if let Ok(b) = try_gcloud_access_token() {
if !b.is_empty() {
return Ok((BearerSource::GcloudImpersonation, b));
}
}
anyhow::bail!(
"CLIENT_BEARER_MISSING — set CLEANLIB_ENRICH_BEARER, run `cleanlib login`, \
or configure `gcloud auth application-default login` with impersonation \
(see docs/cli/auth.md)"
)
}
fn try_gcloud_access_token() -> Result<String> {
let output = Command::new("gcloud")
.args(["auth", "print-access-token"])
.output()
.context("gcloud CLI not on PATH")?;
if !output.status.success() {
anyhow::bail!("gcloud auth print-access-token failed (rc={})", output.status);
}
let token = String::from_utf8(output.stdout)
.context("gcloud token output is not UTF-8")?
.trim()
.to_string();
Ok(token)
}
pub fn store_in_keyring(bearer: &str) -> Result<()> {
let entry = keyring::Entry::new(KEYRING_SERVICE, KEYRING_USER)
.context("failed to open keyring entry")?;
entry.set_password(bearer).context("failed to write keyring entry")?;
Ok(())
}
pub fn delete_from_keyring() -> Result<()> {
let entry = keyring::Entry::new(KEYRING_SERVICE, KEYRING_USER)
.context("failed to open keyring entry")?;
match entry.delete_credential() {
Ok(()) => Ok(()),
Err(keyring::Error::NoEntry) => Ok(()),
Err(e) => Err(anyhow::anyhow!(e).context("failed to delete keyring entry")),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bearer_source_display_does_not_leak_secret() {
let s = format!("{}", BearerSource::Keyring);
assert_eq!(s, "keyring");
assert!(s.len() < 32);
}
#[test]
fn env_var_tier_wins_when_set() {
let key = "CLEANLIBRARY_API_KEY";
let prior = std::env::var(key).ok();
std::env::set_var(key, "test-bearer-do-not-leak");
let (source, bearer) = resolve_bearer().expect("env tier must resolve");
assert_eq!(source, BearerSource::EnvVar);
assert_eq!(bearer, "test-bearer-do-not-leak");
match prior {
Some(v) => std::env::set_var(key, v),
None => std::env::remove_var(key),
}
}
#[test]
fn keyring_service_slug_is_stable() {
assert_eq!(KEYRING_SERVICE, "cleanstart.com/cleanlib-enrich");
}
}