use std::sync::Arc;
use reqwest::header::{ACCEPT, AUTHORIZATION};
use serde_json::json;
use tokio::sync::RwLock;
use crate::config::{Config, IamClientBuilder};
use crate::error::IamError;
use crate::types::{Claims, Decision, DecisionQuery, Resource, Subject};
use crate::wire::{self, Jwks};
#[derive(Clone)]
pub struct IamClient {
http: reqwest::Client,
config: Arc<Config>,
jwks_cache: Arc<RwLock<Option<Jwks>>>,
}
impl IamClient {
#[must_use]
pub fn builder() -> IamClientBuilder {
IamClientBuilder::default()
}
fn from_config(config: Config) -> Result<Self, IamError> {
let http = reqwest::Client::builder()
.timeout(config.timeout)
.build()
.map_err(|e| IamError::Network(e.to_string()))?;
Ok(Self {
http,
config: Arc::new(config),
jwks_cache: Arc::new(RwLock::new(None)),
})
}
pub async fn check(&self, query: DecisionQuery) -> Result<Decision, IamError> {
let response = self
.send_json(&wire::check_url(&self.config.base_url), &query)
.await?;
let (status, body) = read(response).await?;
wire::parse_decision(status, &body)
}
pub async fn list_resources(
&self,
subject: Subject,
relation: impl AsRef<str>,
) -> Result<Vec<Resource>, IamError> {
let payload = json!({ "subject": subject, "relation": relation.as_ref() });
let response = self
.send_json(&wire::list_resources_url(&self.config.base_url), &payload)
.await?;
let (status, body) = read(response).await?;
wire::parse_resources(status, &body)
}
pub async fn verify_token(&self, jwt: &str) -> Result<Claims, IamError> {
let kid = wire::token_kid(jwt)?;
let jwks = self.jwks_for_kid(&kid).await?;
wire::verify_jwt(
jwt,
&jwks,
self.config.issuer.as_deref(),
self.config.audience.as_deref(),
)
}
async fn jwks_for_kid(&self, kid: &str) -> Result<Jwks, IamError> {
if let Some(jwks) = self.jwks_cache.read().await.as_ref() {
if wire::jwks_has_kid(jwks, kid) {
return Ok(jwks.clone());
}
}
let fetched = self.fetch_jwks().await?;
*self.jwks_cache.write().await = Some(fetched.clone());
Ok(fetched)
}
async fn fetch_jwks(&self) -> Result<Jwks, IamError> {
let response = self
.http
.get(wire::jwks_url(&self.config.base_url))
.header(ACCEPT, "application/json")
.send()
.await
.map_err(map_reqwest_error)?;
let (status, body) = read(response).await?;
if let Some(err) = wire::status_error(status) {
return Err(err);
}
wire::parse_jwks(&body)
}
async fn send_json<T: serde::Serialize>(
&self,
url: &str,
body: &T,
) -> Result<reqwest::Response, IamError> {
let mut request = self.http.post(url).header(ACCEPT, "application/json");
if let Some(token) = &self.config.token {
request = request.header(AUTHORIZATION, format!("Bearer {token}"));
}
request.json(body).send().await.map_err(map_reqwest_error)
}
}
impl IamClientBuilder {
pub fn build(self) -> Result<IamClient, IamError> {
IamClient::from_config(self.finish()?)
}
}
async fn read(response: reqwest::Response) -> Result<(u16, Vec<u8>), IamError> {
let status = response.status().as_u16();
let body = response.bytes().await.map_err(map_reqwest_error)?;
Ok((status, body.to_vec()))
}
#[allow(clippy::needless_pass_by_value)] pub(crate) fn map_reqwest_error(error: reqwest::Error) -> IamError {
if error.is_timeout() {
IamError::Timeout
} else {
IamError::Network(error.to_string())
}
}