use anyhow::{anyhow, Result};
use reqwest::Url;
use serde::{Deserialize, Serialize};
use tracing::{instrument, trace};
mod payload;
pub mod response;
#[derive(Deserialize, Serialize, Debug, PartialEq)]
#[serde(rename_all(deserialize = "SCREAMING_SNAKE_CASE"))]
pub enum ApprovalStatus {
Approved,
ApprovalRevoked,
Deleted,
KeyRevoked,
Pending,
Rejected,
UnknownKey,
}
#[derive(Deserialize, Debug, PartialEq)]
pub struct KeyStatus {
pub fingerprint: String,
pub status: ApprovalStatus,
}
#[derive(Debug)]
pub struct Portal {
base_url: Url,
client: reqwest::Client,
}
impl Portal {
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))
}
}
#[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)
}
#[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")]
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")]
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?
))
}
}
}