use crate::{
credentials::{user_credentials::UserToken, Credentials, GetTokenError},
user_agent::get_user_agent,
};
use miette::Diagnostic;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tracing::debug;
use url::Url;
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AccessKey {
pub key_id: String,
pub workspace_id: String,
pub key_name: String,
pub created_at: String,
pub last_used_at: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct CreateAccessKeyInput {
workspace_id: String,
key_name: String,
}
#[derive(Diagnostic, Error, Debug)]
pub enum CtsClientError {
#[error(transparent)]
GetToken(#[from] GetTokenError),
#[error(transparent)]
Reqwest(#[from] reqwest::Error),
#[error("Request failed: {body} - {error}")]
ErrorResponse {
body: String,
#[source]
error: reqwest::Error,
},
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct RevokeAccessKeyInput {
workspace_id: String,
key_name: String,
}
#[derive(Debug, Deserialize)]
struct RevokeAccessKeyResponse {
message: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct CreateDbTokenInput {
workspace_id: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CreateDbTokenResponse {
db_token: String,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct IdentityProvider {
pub workspace_id: String,
pub issuers: Option<Vec<String>>,
pub audiences: Option<Vec<String>>,
}
pub struct CTSClient<C: Credentials<Token = UserToken>> {
client: reqwest::Client,
base_url: Url,
credentials: C,
}
impl<C> CTSClient<C>
where
C: Credentials<Token = UserToken>,
{
pub fn new(base_url: Url, credentials: C) -> Self {
Self {
client: reqwest::Client::new(),
base_url,
credentials,
}
}
async fn send_reqwest(
&self,
callback: impl FnOnce(&reqwest::Client) -> reqwest::RequestBuilder,
) -> Result<reqwest::Response, CtsClientError> {
let token = self.credentials.get_token().await?;
let response = callback(&self.client)
.header("authorization", token.as_header())
.header("user-agent", get_user_agent())
.send()
.await?;
if let Err(error) = response.error_for_status_ref() {
let body = response.text().await.unwrap_or_else(|e| {
debug!("Failed to extract error response body: {e}");
String::new()
});
return Err(CtsClientError::ErrorResponse { body, error });
}
Ok(response)
}
pub async fn create_access_key(
&self,
name: &str,
workspace_id: &str,
) -> Result<String, CtsClientError> {
let url = self.base_url.join("/api/access-key").expect("Invalid url");
let body = CreateAccessKeyInput {
workspace_id: workspace_id.into(),
key_name: name.into(),
};
let response = self
.send_reqwest(|client| client.post(url).json(&body))
.await?;
let access_key: String = response.text().await?;
Ok(access_key)
}
pub async fn list_access_keys(
&self,
workspace_id: Option<&str>,
) -> Result<Vec<AccessKey>, CtsClientError> {
let endpoint = match &workspace_id {
None => "/api/access-keys".to_string(),
Some(workspace_id) => format!("/api/access-keys/{workspace_id}"),
};
let url = self.base_url.join(&endpoint).expect("Invalid url");
let response = self.send_reqwest(|client| client.get(url)).await?;
let access_keys: Vec<AccessKey> = response.json().await?;
Ok(access_keys)
}
pub async fn revoke_access_key(
&self,
name: &str,
workspace_id: &str,
) -> Result<String, CtsClientError> {
let url = self.base_url.join("/api/access-key").expect("Invalid url");
let body = RevokeAccessKeyInput {
workspace_id: workspace_id.into(),
key_name: name.into(),
};
let response = self
.send_reqwest(|client| client.delete(url).json(&body))
.await?;
let revoke_ak_response: RevokeAccessKeyResponse = response.json().await?;
Ok(revoke_ak_response.message)
}
pub async fn create_db_token(&self, workspace_id: &str) -> Result<String, CtsClientError> {
let url = self.base_url.join("/api/db-token").expect("Invalid url");
let body = CreateDbTokenInput {
workspace_id: workspace_id.to_string(),
};
let response = self
.send_reqwest(|client| client.post(url).json(&body))
.await?;
let create_db_token_response: CreateDbTokenResponse = response.json().await?;
Ok(create_db_token_response.db_token)
}
pub async fn show_provider(
&self,
workspace_id: &str,
) -> Result<IdentityProvider, CtsClientError> {
let endpoint = format!("/api/identify/providers/{workspace_id}");
let url = self.base_url.join(&endpoint).expect("Invalid url");
let response = self.send_reqwest(|client| client.get(url)).await?;
let provider: IdentityProvider = response.json().await?;
Ok(provider)
}
pub async fn modify_provider(
&self,
workspace_id: &str,
issuers: Option<Vec<String>>,
audiences: Option<Vec<String>>,
) -> Result<(), CtsClientError> {
let endpoint = format!("/api/identify/providers/{workspace_id}");
let url = self.base_url.join(&endpoint).expect("Invalid url");
let body = IdentityProvider {
workspace_id: workspace_id.to_string(),
issuers,
audiences,
};
self.send_reqwest(|client| client.post(url).json(&body))
.await?;
Ok(())
}
}