use std::fmt;
use std::time::Duration;
use reqwest::{Client, Error as ReqwestError, Response, StatusCode};
use serde::Deserialize;
use url::Url;
use super::AuthProxyError;
use super::registry::{IssuedToken, TokenRequest};
const ADMIN_TIMEOUT: Duration = Duration::from_secs(30);
const MAX_ERROR_CHARS: usize = 200;
#[derive(Deserialize)]
struct RevokedCount {
revoked: usize,
}
#[derive(Clone)]
pub struct AuthProxyClient {
base: String,
admin_key: String,
http: Client,
}
impl fmt::Debug for AuthProxyClient {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AuthProxyClient")
.field("base", &self.base)
.field("admin_key", &"<redacted>")
.finish_non_exhaustive()
}
}
impl AuthProxyClient {
pub fn new(base_url: &str, admin_key: &str) -> Self {
Self {
base: base_url.trim_end_matches('/').to_string(),
admin_key: admin_key.trim().to_string(),
http: Client::new(),
}
}
fn url(&self, segments: &[&str]) -> Result<Url, AuthProxyError> {
let mut url = Url::parse(&self.base)
.map_err(|e| AuthProxyError::InvalidRequest(format!("invalid auth proxy URL: {e}")))?;
url.path_segments_mut()
.map_err(|()| {
AuthProxyError::InvalidRequest("auth proxy URL cannot carry a path".to_string())
})?
.pop_if_empty()
.extend(segments);
Ok(url)
}
pub async fn issue(&self, req: &TokenRequest) -> Result<IssuedToken, AuthProxyError> {
let url = self.url(&["admin", "v1", "tokens"])?;
let resp = self
.http
.post(url)
.bearer_auth(&self.admin_key)
.timeout(ADMIN_TIMEOUT)
.json(req)
.send()
.await
.map_err(transport)?;
if resp.status() != StatusCode::CREATED {
return Err(admin_error(resp).await);
}
resp.json::<IssuedToken>().await.map_err(transport)
}
pub async fn revoke(&self, id: &str) -> Result<(), AuthProxyError> {
let url = self.url(&["admin", "v1", "tokens", id])?;
let resp = self
.http
.delete(url)
.bearer_auth(&self.admin_key)
.timeout(ADMIN_TIMEOUT)
.send()
.await
.map_err(transport)?;
match resp.status() {
StatusCode::NO_CONTENT | StatusCode::NOT_FOUND => Ok(()),
_ => Err(admin_error(resp).await),
}
}
pub async fn revoke_run(&self, run_id: &str) -> Result<usize, AuthProxyError> {
let url = self.url(&["admin", "v1", "runs", run_id, "tokens"])?;
let resp = self
.http
.delete(url)
.bearer_auth(&self.admin_key)
.timeout(ADMIN_TIMEOUT)
.send()
.await
.map_err(transport)?;
if resp.status() != StatusCode::OK {
return Err(admin_error(resp).await);
}
let count = resp.json::<RevokedCount>().await.map_err(transport)?;
Ok(count.revoked)
}
}
fn transport(e: ReqwestError) -> AuthProxyError {
AuthProxyError::Transport(e.without_url().to_string())
}
async fn admin_error(resp: Response) -> AuthProxyError {
let status = resp.status().as_u16();
let message = match resp.text().await {
Ok(body) => body.chars().take(MAX_ERROR_CHARS).collect(),
Err(e) => format!("unreadable response body: {}", e.without_url()),
};
AuthProxyError::Admin { status, message }
}