cipherstash-client 0.12.5

The official CipherStash SDK
Documentation
//! Module for the client library for managing customer hosted resources

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
    }

    // create methods

    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
    }
}