use reqwest::{Client as RequestClient, RequestBuilder};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use miette::Diagnostic;
use serde_json::json;
use thiserror::Error;
use tracing::debug;
use url::{ParseError as UrlParseError, Url};
use uuid::Uuid;
use crate::{
credentials::{user_credentials::UserToken, Credentials, GetTokenError},
user_agent::get_user_agent,
};
pub struct ManagementClient<C> {
pub base_url: Url,
pub credentials: C,
pub is_cipherstash_instance: bool,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Workspace {
pub id: String,
pub name: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Host {
pub id: Uuid,
pub fqdn: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Membership {
pub id: Uuid,
pub member_id: Uuid,
pub workspace_id: String,
}
impl Workspace {
pub fn name(&self) -> String {
if let Some(name) = &self.name {
name.to_string()
} else {
"(empty)".to_string()
}
}
pub fn id(&self) -> String {
self.id.to_string()
}
}
fn encode_uri_component(c: &str) -> String {
percent_encoding::percent_encode(c.as_bytes(), percent_encoding::NON_ALPHANUMERIC).to_string()
}
#[derive(Diagnostic, Error, Debug)]
pub enum ManagementClientError {
#[error(transparent)]
UrlParseError(#[from] UrlParseError),
#[error(transparent)]
Reqwest(#[from] reqwest::Error),
#[error(transparent)]
Credentials(#[from] GetTokenError),
#[error("ErrorResponse: {body} - {error}")]
ErrorResponse {
body: String,
#[source]
error: reqwest::Error,
},
}
impl<C> ManagementClient<C>
where
C: Credentials<Token = UserToken>,
{
pub fn new(base_url: &Url, credentials: C, is_cipherstash_instance: bool) -> Self {
Self {
base_url: base_url.to_owned(),
credentials,
is_cipherstash_instance,
}
}
pub fn is_cipherstash_instance(&self) -> bool {
self.is_cipherstash_instance
}
async fn send_reqwest<R: DeserializeOwned>(
&self,
callback: impl FnOnce(RequestClient) -> RequestBuilder,
) -> Result<R, ManagementClientError> {
let client = reqwest::Client::new();
let user_token = self.credentials.get_token().await?;
let response = callback(client)
.header("authorization", user_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(ManagementClientError::ErrorResponse { body, error });
}
Ok(response.json().await?)
}
async fn get<R: DeserializeOwned>(&self, path: &str) -> Result<R, ManagementClientError> {
let url = self.base_url.join(path)?;
self.send_reqwest(|client| client.get(url)).await
}
async fn delete<R: DeserializeOwned>(&self, path: &str) -> Result<R, ManagementClientError> {
let url = self.base_url.join(path)?;
self.send_reqwest(|client| client.delete(url)).await
}
async fn post<R: DeserializeOwned>(
&self,
path: &str,
body: impl Serialize,
) -> Result<R, ManagementClientError> {
let url = self.base_url.join(path)?;
self.send_reqwest(|client| client.post(url).json(&body))
.await
}
pub async fn list_workspaces_for_current_user(
&self,
) -> Result<Vec<Workspace>, ManagementClientError> {
self.get("/api/meta/workspaces").await
}
pub async fn list_all_workspaces(&self) -> Result<Vec<Workspace>, ManagementClientError> {
self.get("/api/meta/admin/workspaces").await
}
pub async fn list_hosts(&self) -> Result<Vec<Host>, ManagementClientError> {
self.get("/api/meta/admin/hosts").await
}
pub async fn list_memberships(&self) -> Result<Vec<Membership>, ManagementClientError> {
self.get("/api/meta/admin/memberships").await
}
pub async fn list_memberships_for_workspace(
&self,
ws_id: &str,
) -> Result<Vec<Membership>, ManagementClientError> {
self.get(&format!(
"/api/meta/admin/workspaces/{}/memberships",
encode_uri_component(ws_id)
))
.await
}
pub async fn create_workspace(
&self,
host_id: Uuid,
) -> Result<Workspace, ManagementClientError> {
let workspace = self
.post(
"/api/meta/admin/workspaces",
json!({
"hostId": host_id
}),
)
.await?;
Ok(workspace)
}
pub async fn create_host(&self, fqdn: &str) -> Result<Host, ManagementClientError> {
let host = self
.post(
"/api/meta/admin/hosts",
json!({
"fqdn": fqdn
}),
)
.await?;
Ok(host)
}
pub async fn create_membership(
&self,
user_id: &str,
workspace_id: &str,
) -> Result<Membership, ManagementClientError> {
let host = self
.post(
"/api/meta/admin/memberships",
json!({
"userId": user_id,
"workspaceId": workspace_id,
}),
)
.await?;
Ok(host)
}
pub async fn delete_membership(
&self,
membership_id: Uuid,
) -> Result<Membership, ManagementClientError> {
self.delete(&format!(
"/api/meta/admin/memberships/{}",
encode_uri_component(&membership_id.to_string())
))
.await
}
pub async fn delete_workspace(
&self,
workspace_id: &str,
) -> Result<Workspace, ManagementClientError> {
self.delete(&format!(
"/api/meta/admin/workspaces/{}",
encode_uri_component(workspace_id)
))
.await
}
pub async fn delete_host(&self, host_id: Uuid) -> Result<Host, ManagementClientError> {
self.delete(&format!(
"/api/meta/admin/hosts/{}",
encode_uri_component(&host_id.to_string())
))
.await
}
}