use std::{path::Path, time::Duration};
use async_mutex::Mutex as AsyncMutex;
use async_trait::async_trait;
use log::debug;
use miette::Diagnostic;
use serde_json::json;
use thiserror::Error;
use url::Url;
use super::service_token::ServiceToken;
use crate::credentials::{
token_store::TokenStore, user_credentials::UserToken, AutoRefreshable, ClearTokenError,
Credentials, GetTokenError, TokenExpiry,
};
use crate::reqwest_client::create_client;
use crate::user_agent::get_user_agent;
pub struct ServiceUserCredentials<C: Credentials<Token = UserToken>> {
user_credentials: C,
cts_base_url: Url,
workspace_id: String,
token_store: AsyncMutex<TokenStore<ServiceToken>>,
client: reqwest_middleware::ClientWithMiddleware,
}
#[derive(Diagnostic, Error, Debug)]
pub enum AcquireTokenError {
#[error("Failed to acquire console token: {0}")]
GetTokenError(#[from] GetTokenError),
#[error("Failed to acquire token: {0}")]
RequestFailed(Box<dyn std::error::Error + Send + Sync>),
#[error("Failed to parse json response: {0}")]
BadResponse(Box<dyn std::error::Error + Sync + Send>),
}
impl<C: Credentials<Token = UserToken>> ServiceUserCredentials<C> {
pub fn new(
token_path: &Path,
user_credentials: C,
cts_base_url: &Url,
workspace_id: &str,
) -> Self {
Self {
user_credentials,
cts_base_url: cts_base_url.to_owned(),
workspace_id: workspace_id.to_string(),
token_store: AsyncMutex::new(TokenStore::new(token_path)),
client: create_client(),
}
}
async fn federate_token(&self) -> Result<ServiceToken, AcquireTokenError> {
debug!(target: "service_user_credentials", "Exchanging Access Token with CTS");
let url = self.cts_base_url.join("/api/federate").unwrap();
let user_token = self.user_credentials.get_token().await?;
let token: ServiceToken = self
.client
.post(url)
.json(&json!({
"accessToken": user_token.access_token(),
"workspaceId": self.workspace_id.clone(),
}))
.header("authorization", user_token.as_header())
.header("user-agent", get_user_agent())
.send()
.await
.map_err(|e| AcquireTokenError::RequestFailed(Box::new(e)))?
.error_for_status()
.map_err(|e| AcquireTokenError::RequestFailed(Box::new(e)))?
.json()
.await
.map_err(|e| AcquireTokenError::BadResponse(Box::new(e)))?;
debug!(target: "service_user_credentials",
"Access Token Acquired - expiry(epoch seconds): {}",
&token.expiry
);
Ok(token)
}
}
#[async_trait]
impl<C: Credentials<Token = UserToken>> Credentials for ServiceUserCredentials<C> {
type Token = ServiceToken;
async fn get_token(&self) -> Result<Self::Token, GetTokenError> {
debug!(target: "service_user_credentials", "getting token (waiting for lock)");
let mut token_store = self.token_store.lock().await;
debug!(target: "service_user_credentials", "getting token (got lock)");
if let Some(cached_token) = token_store.get() {
debug!(target: "service_user_credentials", "found cached token");
if !cached_token.is_expired() {
debug!(target: "service_user_credentials", "using cached token");
return Ok(cached_token);
}
debug!(target: "service_user_credentials", "cached token is expired");
}
debug!(target: "service_user_credentials", "fetching new token");
let new_token = self
.federate_token()
.await
.map_err(|e| GetTokenError::AcquireNewTokenFailed(Box::new(e)))?;
token_store
.set(&new_token)
.map_err(|e| GetTokenError::PersistTokenError(Box::new(e)))?;
Ok(new_token)
}
async fn clear_token(&self) -> Result<(), ClearTokenError> {
let mut token_store = self.token_store.lock().await;
token_store
.clear()
.map_err(|e| ClearTokenError(Box::new(e)))
}
}
#[async_trait]
impl<C: Credentials<Token = UserToken>> AutoRefreshable for ServiceUserCredentials<C> {
async fn refresh(&self) -> Duration {
let token = {
let mut token_store = self.token_store.lock().await;
token_store.get()
};
if let Some(cached_token) = token {
debug!(target: "service_user_credentials", "Found token on disk");
if !cached_token.should_refresh() {
debug!(target: "service_user_credentials", "Access token is still new");
return cached_token.refresh_interval();
}
}
debug!(target: "service_user_credentials", "Access token is missing or close to expiry, refreshing");
if let Ok(new_token) = self.federate_token().await {
let mut token_store = self.token_store.lock().await;
if token_store.set(&new_token).is_ok() {
debug!(target: "service_user_credentials", "Access token refreshed and saved to disk");
return new_token.refresh_interval();
}
}
Self::Token::min_refresh_interval()
}
}