use std::sync::Arc;
use anyhow::{Context, Result};
use aviso::ClientError;
use aviso::auth::{AuthProvider, Basic, Bearer, Chain, Env};
use crate::config::AuthConfig;
pub(crate) fn provider_from_flags(
token: Option<&str>,
username: Option<&str>,
password: Option<&str>,
) -> Result<Option<Arc<dyn AuthProvider>>> {
if let Some(t) = token {
let bearer =
Bearer::new(t.to_string()).context("build Bearer auth provider from --token flag")?;
return Ok(Some(Arc::new(bearer)));
}
if let (Some(u), Some(p)) = (username, password) {
let basic = Basic::new(u.to_string(), p.to_string())
.context("build Basic auth provider from --username/--password flags")?;
return Ok(Some(Arc::new(basic)));
}
Ok(None)
}
pub(crate) fn provider_from_env() -> Result<Option<Arc<dyn AuthProvider>>> {
let any_set = any_auth_env_var_set();
match Env::from_process_env() {
Ok(env) => Ok(Some(Arc::new(env))),
Err(ClientError::Auth(_)) if !any_set => Ok(None),
Err(ClientError::Auth(reason)) => Err(crate::exit::usage_error(format!(
"env auth is misconfigured: {reason}. Set AVISO_TOKEN, OR set BOTH AVISO_USERNAME and AVISO_PASSWORD, OR unset all three to fall back to the config-file auth block."
))),
Err(other) => {
Err(anyhow::Error::from(other)).context("read auth credentials from environment")
}
}
}
fn any_auth_env_var_set() -> bool {
["AVISO_TOKEN", "AVISO_USERNAME", "AVISO_PASSWORD"]
.iter()
.any(|k| std::env::var_os(k).is_some_and(|v| !v.is_empty()))
}
pub(crate) fn provider_from_file(
cfg: Option<&AuthConfig>,
) -> Result<Option<Arc<dyn AuthProvider>>> {
let Some(cfg) = cfg else {
return Ok(None);
};
match (cfg.bearer_token.as_deref(), cfg.basic.as_ref()) {
(Some(_), Some(_)) => Err(crate::exit::usage_error(
"config file auth: set EITHER `auth.bearer_token` OR `auth.basic.{username,password}`, not both",
)),
(Some(token), None) => {
let bearer = Bearer::new(token.to_string())
.context("build Bearer auth provider from config-file auth.bearer_token")?;
Ok(Some(Arc::new(bearer)))
}
(None, Some(basic)) => {
let basic = Basic::new(basic.username.clone(), basic.password.clone())
.context("build Basic auth provider from config-file auth.basic")?;
Ok(Some(Arc::new(basic)))
}
(None, None) => Ok(None),
}
}
pub(crate) fn build_chain(
flag_provider: Option<Arc<dyn AuthProvider>>,
env_provider: Option<Arc<dyn AuthProvider>>,
file_provider: Option<Arc<dyn AuthProvider>>,
) -> Option<Arc<dyn AuthProvider>> {
let providers: Vec<Arc<dyn AuthProvider>> = [flag_provider, env_provider, file_provider]
.into_iter()
.flatten()
.collect();
match providers.len() {
0 => None,
1 => providers.into_iter().next(),
_ => Some(Arc::new(Chain::new(providers))),
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
reason = "test code: unwrap/expect on chain construction is the expected diagnostic"
)]
mod tests {
use super::*;
#[test]
fn flag_token_yields_bearer_provider() {
let p = provider_from_flags(Some("the-token"), None, None).unwrap();
assert!(p.is_some());
}
#[test]
fn flag_username_password_yield_basic_provider() {
let p = provider_from_flags(None, Some("alice"), Some("hunter2")).unwrap();
assert!(p.is_some());
}
#[test]
fn flag_username_only_yields_none() {
let p = provider_from_flags(None, Some("alice"), None).unwrap();
assert!(p.is_none());
}
#[test]
fn flag_empty_token_errors() {
let err = provider_from_flags(Some(""), None, None).unwrap_err();
let s = err.to_string();
assert!(
s.contains("Bearer") || s.contains("--token"),
"error should name the source: {s}"
);
}
#[test]
fn empty_flag_yields_none() {
let p = provider_from_flags(None, None, None).unwrap();
assert!(p.is_none());
}
#[test]
fn file_provider_none_when_block_absent() {
let p = provider_from_file(None).unwrap();
assert!(p.is_none());
}
#[test]
fn file_provider_bearer_from_block() {
let cfg = AuthConfig {
bearer_token: Some("from-file".into()),
basic: None,
};
let p = provider_from_file(Some(&cfg)).unwrap();
assert!(p.is_some());
}
#[test]
fn file_provider_rejects_both_bearer_and_basic_set() {
let cfg = AuthConfig {
bearer_token: Some("token".into()),
basic: Some(crate::config::BasicAuthConfig {
username: "alice".into(),
password: "pw".into(),
}),
};
let err = provider_from_file(Some(&cfg)).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("EITHER") || msg.contains("not both") || msg.contains("auth.bearer_token"),
"{msg}"
);
}
#[test]
fn chain_empty_returns_none() {
let chain = build_chain(None, None, None);
assert!(chain.is_none());
}
#[test]
fn chain_single_tier_returns_that_tier_unwrapped() {
let token = provider_from_flags(Some("flag-token"), None, None).unwrap();
let chain = build_chain(token, None, None);
assert!(chain.is_some());
}
#[test]
fn chain_multiple_tiers_assembled_into_chain() {
let flag = provider_from_flags(Some("flag-token"), None, None).unwrap();
let file = provider_from_file(Some(&AuthConfig {
bearer_token: Some("file-token".into()),
basic: None,
}))
.unwrap();
let chain = build_chain(flag, None, file);
assert!(chain.is_some());
}
}