sdk-rust 0.1.1

Canonical Rust core for the Lattix metadata-only control-plane SDK
Documentation
use std::collections::BTreeMap;

use serde::{Serialize, de::DeserializeOwned};

use crate::{
    builder::ClientBuilder,
    error::SdkError,
    models::{
        CallerIdentityResponse, SdkArtifactRegisterRequest, SdkArtifactRegisterResponse,
        SdkBootstrapResponse, SdkCapabilitiesResponse, SdkEvidenceIngestRequest,
        SdkEvidenceIngestResponse, SdkKeyAccessPlanRequest, SdkKeyAccessPlanResponse,
        SdkPolicyResolveRequest, SdkPolicyResolveResponse, SdkProtectionPlanRequest,
        SdkProtectionPlanResponse,
    },
    providers::ManagedSymmetricKeyProviderRegistry,
};

pub(crate) enum ClientAuthStrategy {
    StaticBearer(String),
}

pub struct Client {
    pub(crate) base_url: String,
    pub(crate) agent: ureq::Agent,
    pub(crate) default_headers: BTreeMap<String, String>,
    pub(crate) auth_strategy: Option<ClientAuthStrategy>,
    pub(crate) managed_symmetric_key_provider_registry: ManagedSymmetricKeyProviderRegistry,
}

impl Client {
    pub(crate) fn new(
        base_url: String,
        agent: ureq::Agent,
        default_headers: BTreeMap<String, String>,
        auth_strategy: Option<ClientAuthStrategy>,
        managed_symmetric_key_provider_registry: ManagedSymmetricKeyProviderRegistry,
    ) -> Self {
        Self {
            base_url,
            agent,
            default_headers,
            auth_strategy,
            managed_symmetric_key_provider_registry,
        }
    }

    pub fn builder(base_url: impl Into<String>) -> ClientBuilder {
        ClientBuilder::new(base_url)
    }

    pub fn base_url(&self) -> &str {
        &self.base_url
    }

    pub fn capabilities(&self) -> Result<SdkCapabilitiesResponse, SdkError> {
        self.get_json("/v1/sdk/capabilities")
    }

    pub fn whoami(&self) -> Result<CallerIdentityResponse, SdkError> {
        self.get_json("/v1/sdk/whoami")
    }

    pub fn bootstrap(&self) -> Result<SdkBootstrapResponse, SdkError> {
        self.get_json("/v1/sdk/bootstrap")
    }

    pub fn protection_plan(
        &self,
        request: &SdkProtectionPlanRequest,
    ) -> Result<SdkProtectionPlanResponse, SdkError> {
        self.post_json("/v1/sdk/protection-plan", request)
    }

    pub fn policy_resolve(
        &self,
        request: &SdkPolicyResolveRequest,
    ) -> Result<SdkPolicyResolveResponse, SdkError> {
        self.post_json("/v1/sdk/policy-resolve", request)
    }

    pub fn key_access_plan(
        &self,
        request: &SdkKeyAccessPlanRequest,
    ) -> Result<SdkKeyAccessPlanResponse, SdkError> {
        self.post_json("/v1/sdk/key-access-plan", request)
    }

    pub fn artifact_register(
        &self,
        request: &SdkArtifactRegisterRequest,
    ) -> Result<SdkArtifactRegisterResponse, SdkError> {
        self.post_json("/v1/sdk/artifact-register", request)
    }

    pub fn evidence(
        &self,
        request: &SdkEvidenceIngestRequest,
    ) -> Result<SdkEvidenceIngestResponse, SdkError> {
        self.post_json("/v1/sdk/evidence", request)
    }

    fn get_json<T>(&self, path: &str) -> Result<T, SdkError>
    where
        T: DeserializeOwned,
    {
        let response = self
            .apply_headers(self.agent.get(&self.endpoint(path)))?
            .call()
            .map_err(map_ureq_error)?;
        decode_response(response)
    }

    fn post_json<TReq, TRes>(&self, path: &str, payload: &TReq) -> Result<TRes, SdkError>
    where
        TReq: Serialize,
        TRes: DeserializeOwned,
    {
        let payload_json = serde_json::to_string(payload).map_err(|error| {
            SdkError::Serialization(format!("failed to serialize request payload: {error}"))
        })?;
        let response = self
            .apply_headers(
                self.agent
                    .post(&self.endpoint(path))
                    .set("Content-Type", "application/json"),
            )?
            .send_string(&payload_json)
            .map_err(map_ureq_error)?;
        decode_response(response)
    }

    fn endpoint(&self, path: &str) -> String {
        format!("{}{}", self.base_url, path)
    }

    fn apply_headers(&self, mut request: ureq::Request) -> Result<ureq::Request, SdkError> {
        for (name, value) in &self.default_headers {
            request = request.set(name, value);
        }

        if let Some(authorization_header) = self.resolve_authorization_header()? {
            request = request.set("Authorization", &authorization_header);
        }

        Ok(request)
    }

    fn resolve_authorization_header(&self) -> Result<Option<String>, SdkError> {
        match self.auth_strategy.as_ref() {
            Some(ClientAuthStrategy::StaticBearer(header)) => Ok(Some(header.clone())),
            None => Ok(None),
        }
    }
}

fn decode_response<T>(response: ureq::Response) -> Result<T, SdkError>
where
    T: DeserializeOwned,
{
    let body = response.into_string().map_err(|error| {
        SdkError::Connection(format!("failed to read HTTP response body: {error}"))
    })?;
    serde_json::from_str(&body).map_err(|error| {
        SdkError::Serialization(format!("failed to decode JSON response body: {error}"))
    })
}

fn map_ureq_error(error: ureq::Error) -> SdkError {
    match error {
        ureq::Error::Status(status, response) => {
            let body = response.into_string().unwrap_or_default();
            SdkError::Server(format!("HTTP {status}: {body}"))
        }
        ureq::Error::Transport(transport) => SdkError::Connection(transport.to_string()),
    }
}