use crate::core::config_schema::{AuthMethod, Transport};
use crate::core::credential_storage::{load_credential, save_credential};
use super::auth_strategy::{AuthConfig, AuthStrategy, Credentials};
use super::errors::AuthError;
use super::request_credentials::RequestCredentials;
use super::strategies::api_key::ApiKeyStrategy;
use super::strategies::basic::BasicAuthStrategy;
use super::strategies::pat::PatAuthStrategy;
use super::strategies::stub::StubAuthStrategy;
const CREDENTIAL_ACCOUNT: &str = "active-credentials";
fn strategy_for(auth_method: AuthMethod) -> Box<dyn AuthStrategy> {
match auth_method {
AuthMethod::Basic => Box::new(BasicAuthStrategy),
AuthMethod::ApiKey => Box::new(ApiKeyStrategy),
AuthMethod::Pat => Box::new(PatAuthStrategy),
#[allow(unreachable_patterns)]
_ => Box::new(StubAuthStrategy),
}
}
pub fn header_location_for(auth_method: AuthMethod) -> (&'static str, &'static str) {
match auth_method {
AuthMethod::Pat => ("header", "Authorization"),
AuthMethod::ApiKey => ("header", "Authorization"),
AuthMethod::Basic => ("header", "Authorization"),
}
}
pub struct AuthManager {
auth_method: AuthMethod,
strategy: Box<dyn AuthStrategy>,
cached_credentials: Option<Credentials>,
}
impl AuthManager {
pub fn new(auth_method: AuthMethod) -> Self {
Self {
strategy: strategy_for(auth_method),
auth_method,
cached_credentials: None,
}
}
pub async fn login(&mut self, config: &AuthConfig) -> anyhow::Result<Credentials> {
let credentials = self.strategy.authenticate(config).await?;
self.cached_credentials = Some(credentials.clone());
save_credential(CREDENTIAL_ACCOUNT, &serde_json::to_string(&credentials)?)?;
Ok(credentials)
}
pub fn set_credentials(&mut self, credentials: Credentials) {
self.cached_credentials = Some(credentials);
}
pub async fn credentials(&mut self) -> anyhow::Result<Credentials> {
if let Some(cached) = &self.cached_credentials
&& self.strategy.validate_credentials(cached)
{
return Ok(cached.clone());
}
if let Some(stored) = load_credential(CREDENTIAL_ACCOUNT)? {
let parsed: Credentials = serde_json::from_str(&stored)?;
if self.strategy.validate_credentials(&parsed) {
self.cached_credentials = Some(parsed.clone());
return Ok(parsed);
}
if let Ok(refreshed) = self.strategy.refresh_token(&parsed).await {
self.cached_credentials = Some(refreshed.clone());
save_credential(CREDENTIAL_ACCOUNT, &serde_json::to_string(&refreshed)?)?;
return Ok(refreshed);
}
}
Err(AuthError::NoActiveCredentials(format!("{:?}", self.auth_method)).into())
}
pub async fn apply_auth_headers(
&mut self,
mut headers: std::collections::HashMap<String, String>,
method: &str,
url: &str,
transport: Transport,
request_override: Option<&RequestCredentials>,
) -> anyhow::Result<std::collections::HashMap<String, String>> {
let credentials = match (transport, request_override) {
(Transport::Http, Some(override_credentials)) => {
override_credentials.clone().into_credentials_map()
}
(Transport::Http, None) => {
anyhow::bail!(
"no credentials were supplied on this HTTP request; local config/env/keychain \
is never used as a fallback over HTTP transport"
);
}
(Transport::Stdio, _) => self.credentials().await?,
};
let _ = method;
let _ = url;
if let Some(name) = credentials.get("request_header_name") {
let value = credentials
.get("request_header_value")
.cloned()
.unwrap_or_default();
headers.insert(name.clone(), value);
} else if let Some(header) = credentials.get("authorization_header") {
headers.insert("Authorization".to_string(), header.clone());
} else if let Some(api_key) = credentials.get("api_key") {
headers.insert("X-Api-Key".to_string(), api_key.clone());
}
Ok(headers)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn seeded_credentials_win_over_stored_ones() {
let mut manager = AuthManager::new(AuthMethod::Pat);
let mut credentials = Credentials::new();
credentials.insert("token".to_string(), "abc".to_string());
manager.set_credentials(credentials.clone());
let resolved = manager.credentials().await.unwrap();
assert_eq!(resolved, credentials);
}
#[tokio::test]
async fn http_transport_uses_the_request_override_not_the_config_cascade() {
let mut manager = AuthManager::new(AuthMethod::Pat);
let mut stale = Credentials::new();
stale.insert(
"request_header_name".to_string(),
"X-Should-Not-Be-Used".to_string(),
);
manager.set_credentials(stale);
let override_credentials = RequestCredentials {
header_name: "X-Api-Key".to_string(),
value: "from-the-request".to_string(),
};
let headers = manager
.apply_auth_headers(
std::collections::HashMap::new(),
"GET",
"https://example.com",
Transport::Http,
Some(&override_credentials),
)
.await
.unwrap();
assert_eq!(
headers.get("X-Api-Key").map(String::as_str),
Some("from-the-request")
);
}
#[tokio::test]
async fn http_transport_without_an_override_errors_instead_of_falling_back() {
let mut manager = AuthManager::new(AuthMethod::Pat);
let mut stale = Credentials::new();
stale.insert(
"request_header_name".to_string(),
"X-Should-Not-Be-Used".to_string(),
);
manager.set_credentials(stale);
let result = manager
.apply_auth_headers(
std::collections::HashMap::new(),
"GET",
"https://example.com",
Transport::Http,
None,
)
.await;
assert!(result.is_err());
}
}