sett 0.4.0

Rust port of sett (data compression, encryption and transfer tool).
Documentation
//! Portal API
pub mod error;
mod payload;
pub mod response;

use reqwest::Url;
use serde::{Deserialize, Serialize};
use tracing::{instrument, trace};

/// 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, error::CreateError> {
        let url = Url::parse(base_url.as_ref()).map_err(error::UrlParseError::from)?;
        if url.scheme() == "https" || url.scheme() == "http" {
            Ok(Portal {
                base_url: url,
                client: reqwest::Client::builder().use_rustls_tls().build()?,
            })
        } else {
            Err(error::CreateError::InvalidScheme(url.as_ref().to_string()))
        }
    }

    /// 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<T>(
        &self,
        fingerprints: &[T],
    ) -> Result<Vec<KeyStatus>, error::Error>
    where
        T: AsRef<str> + std::fmt::Debug,
    {
        const PGPKEY_STATUS_ENDPOINT: &str = "/backend/open-pgp-keys/status/";
        let answer = self
            .client
            .post(
                self.base_url
                    .join(PGPKEY_STATUS_ENDPOINT)
                    .map_err(error::UrlParseError::from)?,
            )
            .json(&payload::KeyStatusPayload { 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, error::Error<error::InvalidPackage>> {
        const CHECK_PACKAGE_ENDPOINT: &str = "/backend/data-package/check/";
        let response = self
            .client
            .post(
                self.base_url
                    .join(CHECK_PACKAGE_ENDPOINT)
                    .map_err(error::UrlParseError::from)?,
            )
            .json(&payload::CheckPackage {
                metadata: 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(error::InvalidPackage(payload.detail).into())
        } else {
            Err(error::InvalidPackage(response.text().await?).into())
        }
    }

    #[cfg(feature = "auth")]
    /// Fetches S3 connection info for a given Data Transfer Request (DTR).
    ///
    /// The retrieved connection info contains:
    ///
    ///   - Host URL (endpoint)
    ///   - Credentials (S3 STS tokens)
    ///   - Bucket (DTR dependent)
    ///
    /// This data is retrieved via an authenticated API call to Portal, and
    /// therefore requires a portal authentication `token`.
    ///
    /// Arguments
    ///
    /// * `permission`: the type of access permission requested.
    /// * `token`: portal authentication token.
    pub async fn get_s3_connection_info(
        &self,
        permission: &super::remote::s3::AccessPermission,
        token: impl AsRef<str>,
    ) -> Result<super::remote::s3::ConnectionInfo, error::Error<error::FetchS3Credentials>> {
        use super::remote::s3::AccessPermission;

        const STS_READ_ENDPOINT: &str = "/api/v2/sts/read";
        const STS_WRITE_ENDPOINT: &str = "/api/v2/sts/write";
        let endpoint = match permission {
            AccessPermission::GetObject(_) => STS_READ_ENDPOINT,
            AccessPermission::ListObjects(_) => STS_READ_ENDPOINT,
            AccessPermission::PutObject(_) => STS_WRITE_ENDPOINT,
        };
        let response = self
            .client
            .post(
                self.base_url
                    .join(endpoint)
                    .map_err(error::UrlParseError::from)?,
            )
            .header(
                reqwest::header::AUTHORIZATION,
                format!("Bearer {}", token.as_ref()),
            )
            .json(&payload::Sts::from_permission(permission))
            .send()
            .await?;
        let status = response.status();

        // Return STS credentials and destination information.
        if status.is_success() {
            Ok(response
                .json::<response::S3ConnectionInfoRaw>()
                .await?
                .into())
        } else if status.is_client_error() {
            let payload = response.json::<response::PortalError>().await?;
            Err(error::FetchS3Credentials(payload.detail).into())
        } else {
            Err(error::FetchS3Credentials(response.text().await?).into())
        }
    }

    #[cfg(feature = "auth")]
    /// Gets Data Transfer Requests (DTR).
    pub async fn get_data_transfers(
        &self,
        token: impl AsRef<str>,
    ) -> Result<Vec<response::DataTransfer>, error::Error<error::FetchDataTransfer>> {
        const DTR_ENDPOINT: &str = "/backend/data-transfer/";
        let response = self
            .client
            .get(
                self.base_url
                    .join(DTR_ENDPOINT)
                    .map_err(error::UrlParseError::from)?,
            )
            .header(
                reqwest::header::AUTHORIZATION,
                format!("Bearer {}", token.as_ref()),
            )
            .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(error::FetchDataTransfer(payload.detail).into())
        } else {
            Err(error::FetchDataTransfer(response.text().await?).into())
        }
    }
}