sett 0.3.0

Rust port of sett (data compression, encryption and transfer tool).
Documentation
//! Portal API
use anyhow::{anyhow, Result};
use reqwest::Url;
use serde::{Deserialize, Serialize};
use tracing::{instrument, trace};

mod payload;
pub mod response;

/// Key approval status
#[derive(Deserialize, Serialize, Debug, PartialEq)]
#[serde(rename_all(deserialize = "SCREAMING_SNAKE_CASE"))]
pub enum ApprovalStatus {
    /// Key approved by key authority (analogous to signature)
    Approved,
    /// Approval was revoked by key authority
    ApprovalRevoked,
    /// Key was deleted from keyserver (user ID is removed)
    Deleted,
    /// Key was revoked by user
    KeyRevoked,
    /// Key info was added to database but key not yet approved
    Pending,
    /// Key was never approved and is not trusted
    Rejected,
    /// Key info is not present in the portal
    UnknownKey,
}

#[derive(Deserialize, Debug, PartialEq)]
/// Approval status of a key on Portal.
pub struct KeyStatus {
    /// The associated fingerprint
    pub fingerprint: String,
    /// Returned status of given `fingerprint`
    pub status: ApprovalStatus,
}

/// A client for interacting with the Portal API
#[derive(Debug)]
pub struct Portal {
    base_url: Url,
    client: reqwest::Client,
}

impl Portal {
    /// Creates a new `Portal` client.
    ///
    /// `base_url` must contain a scheme "https://" or "http://".
    pub fn new(base_url: impl AsRef<str>) -> Result<Portal> {
        let url = Url::parse(base_url.as_ref())?;
        if url.scheme() == "https" || url.scheme() == "http" {
            Ok(Portal {
                base_url: url,
                client: reqwest::Client::builder().use_rustls_tls().build()?,
            })
        } else {
            Err(anyhow!("Invalid URL '{}'", url))
        }
    }

    /// Get the approval status of one or more OpenPGP keys.
    ///
    /// Keys are identified by their fingerprints.
    #[instrument(skip(fingerprints))]
    pub async fn get_key_status(&self, fingerprints: &[impl AsRef<str>]) -> Result<Vec<KeyStatus>> {
        const PGPKEY_STATUS_ENDPOINT: &str = "/backend/open-pgp-keys/status/";
        let fingerprints = fingerprints
            .iter()
            .map(payload::SequoiaFingerprint::new)
            .collect::<Result<Vec<_>>>()?;
        let answer = self
            .client
            .post(self.base_url.join(PGPKEY_STATUS_ENDPOINT)?)
            .json(&payload::KeyStatusPayload {
                fingerprints: &fingerprints,
            })
            .send()
            .await?
            .json()
            .await?;
        trace!(?answer, ?fingerprints, "key status response");
        Ok(answer)
    }

    /// Checks a package's metadata and transfer status using the Portal API.
    ///
    /// Verification succeeds if:
    /// * The metadata is correct.
    /// * The data transfer is authorized.
    /// * The package has never been transferred before.
    #[instrument]
    pub async fn check_package(
        &self,
        package_metadata: &crate::package::Metadata,
        package_name: &str,
    ) -> Result<response::CheckPackage> {
        const CHECK_PACKAGE_ENDPOINT: &str = "/backend/data-package/check/";
        let response = self
            .client
            .post(self.base_url.join(CHECK_PACKAGE_ENDPOINT)?)
            .json(&payload::CheckPackage {
                metadata: &serde_json::to_string(package_metadata)?,
                file_name: package_name,
            })
            .send()
            .await?;
        let status = response.status();
        if status.is_success() {
            Ok(response.json().await?)
        } else if status.is_client_error() {
            let payload = response.json::<response::PortalError>().await?;
            Err(anyhow!("Failed to verify package: {}", payload.detail))
        } else {
            Err(anyhow!(
                "Failed to verify package: {}",
                response.text().await?
            ))
        }
    }

    #[cfg(feature = "auth")]
    /// Gets S3 destination for a given Data Transfer Request (DTR).
    ///
    /// Both the S3 credentials and the bucket name are fetched from Portal.
    pub async fn get_package_destination(
        &self,
        dtr: u32,
        token: &crate::auth::AccessToken,
    ) -> Result<crate::destination::S3> {
        const STS_ENDPOINT: &str = "/backend/sts/";
        let response = self
            .client
            .post(self.base_url.join(STS_ENDPOINT)?)
            .header(
                reqwest::header::AUTHORIZATION,
                format!("Bearer {}", token.0),
            )
            .json(&payload::Sts { dtr })
            .send()
            .await?;
        let status = response.status();
        if status.is_success() {
            let cred: response::StsCredentials = response.json().await?;
            Ok(crate::destination::S3::new(
                crate::remote::s3::Client::builder()
                    .endpoint(Some(cred.endpoint.trim_end_matches('/')))
                    .access_key(Some(cred.access_key_id))
                    .secret_key(Some(cred.secret_access_key))
                    .session_token(Some(cred.session_token))
                    .build()
                    .await?,
                cred.bucket,
            ))
        } else if status.is_client_error() {
            let payload = response.json::<response::PortalError>().await?;
            Err(anyhow!(
                "Failed to fetch s3 credentials: {}",
                payload.detail
            ))
        } else {
            Err(anyhow!(
                "Failed to fetch S3 credentials: {}",
                response.text().await?
            ))
        }
    }

    #[cfg(feature = "auth")]
    /// Gets Data Transfer Requests (DTR).
    pub async fn get_data_transfers(
        &self,
        token: &crate::auth::AccessToken,
    ) -> Result<Vec<response::DataTransfer>> {
        const DTR_ENDPOINT: &str = "/backend/data-transfer/";
        let response = self
            .client
            .get(self.base_url.join(DTR_ENDPOINT)?)
            .header(
                reqwest::header::AUTHORIZATION,
                format!("Bearer {}", token.0),
            )
            .send()
            .await?;
        let status = response.status();
        if status.is_success() {
            let data_transfers: Vec<response::DataTransfer> = response.json().await?;
            Ok(data_transfers)
        } else if status.is_client_error() {
            let payload = response.json::<response::PortalError>().await?;
            Err(anyhow!(
                "Failed to fetch data transfers: {}",
                payload.detail
            ))
        } else {
            Err(anyhow!(
                "Failed to fetch data transfers: {}",
                response.text().await?
            ))
        }
    }
}