cipherstash-client 0.12.5

The official CipherStash SDK
Documentation
//! Module for the CipherStash Token Service client library

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>> {
    // Use the client without retry strategy since these commands are used from the CLI
    client: reqwest::Client,
    base_url: Url,
    credentials: C,
}

impl<C> CTSClient<C>
where
    C: Credentials<Token = UserToken>,
{
    /// Create a new instance of the [`CTSClient`].
    ///
    /// ```rust
    /// # use cipherstash_client::{ ConsoleConfig, CtsConfig, CTSClient };
    /// let console_config = ConsoleConfig::builder()
    ///   .with_env()
    ///   .build()
    ///   .expect("failed to build config");
    ///
    /// let cts_config = CtsConfig::builder()
    ///   .with_env()
    ///   .build()
    ///   .expect("failed to build config");
    ///
    /// let client = CTSClient::new(cts_config.base_url(), console_config.credentials());
    /// ```
    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)
    }

    /// Create a new access key used to access ZeroKMS
    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)
    }

    /// List all the access keys in the provided workspace created by the current user
    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)
    }

    /// Revoke a certain access key by name in the specified workspace
    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)
    }

    /// Create a new DB Token in the specified workspace
    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)
    }

    /// Show the identity provider for the specified workspace
    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)
    }

    /// Modify the identity provider for the specified workspace
    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(())
    }
}