use super::helpers::TrustTaskOutcome;
use serde_json::Value;
use trust_tasks_rs::TrustTask;
use vta_sdk::protocols::backup_management::descriptors::{
AbortBundleBody, CompleteExportBody, FinalizeImportBody, InitiateExportBody, InitiateImportBody,
};
use crate::auth::AuthClaims;
use crate::operations::backup::descriptors;
use crate::server::AppState;
use super::helpers::{
TRANSPORT_TRUST_TASK, app_error_to_reject, parse_payload, reject_with_code, success_response,
};
const INITIATE_EXPORT_SLUG: &str = "vta/backup/initiate-export";
const INITIATE_IMPORT_SLUG: &str = "vta/backup/initiate-import";
fn transport_unavailable(doc: &TrustTask<Value>, slug: &str) -> TrustTaskOutcome {
tracing::warn!(
slug,
"backup descriptor refused: `public_url` is not configured, so the `stream` \
algorithm has no blob URL to publish"
);
let code = trust_tasks_rs::TrustTaskCode::new_extended(slug, "transportUnavailable")
.expect("backup extended code is grammar-valid");
reject_with_code(doc, code, descriptors::TRANSPORT_UNAVAILABLE_MESSAGE, None)
}
async fn initiate_precheck(
state: &AppState,
auth: &AuthClaims,
doc: &TrustTask<Value>,
slug: &str,
) -> Result<(), TrustTaskOutcome> {
auth.require_super_admin()
.map_err(|e| app_error_to_reject(doc, e))?;
if descriptors::blob_transport_base_url(&state.config)
.await
.is_none()
{
return Err(transport_unavailable(doc, slug));
}
Ok(())
}
async fn record_bundle_event(
state: &AppState,
auth: &AuthClaims,
action: &str,
bundle_id: &str,
detail: String,
) {
if let Err(e) = crate::audit::record_with_detail(
&state.audit_sink,
action,
&auth.did,
Some(bundle_id),
"success",
Some(TRANSPORT_TRUST_TASK),
None,
Some(&detail),
)
.await
{
tracing::warn!(error = %e, action, "audit record failed for {action}");
}
}
pub(super) async fn handle_initiate_export(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
let req: InitiateExportBody = match parse_payload(&doc) {
Ok(r) => r,
Err(resp) => return resp,
};
if let Err(resp) = initiate_precheck(state, auth, &doc, INITIATE_EXPORT_SLUG).await {
return resp;
}
let deps = crate::operations::descriptor_deps_from_app_state(state);
let include_audit = req.include_audit;
match descriptors::initiate_export(&deps, auth, req).await {
Ok(body) => {
record_bundle_event(
state,
auth,
"backup.initiate-export",
&body.descriptor.bundle_id,
format!(
"includeAudit={include_audit} bytes={} expires={}",
body.descriptor.expected_size_bytes, body.descriptor.expires_at
),
)
.await;
success_response(&doc, body)
}
Err(e) => app_error_to_reject(&doc, e),
}
}
pub(super) async fn handle_complete_export(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
let req: CompleteExportBody = match parse_payload(&doc) {
Ok(r) => r,
Err(resp) => return resp,
};
let deps = crate::operations::descriptor_deps_from_app_state(state);
match descriptors::complete_export(&deps, auth, req).await {
Ok(body) => {
record_bundle_event(
state,
auth,
"backup.complete-export",
&body.bundle_id,
format!("downloaded={}", body.downloaded),
)
.await;
success_response(&doc, body)
}
Err(e) => app_error_to_reject(&doc, e),
}
}
pub(super) async fn handle_initiate_import(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
let req: InitiateImportBody = match parse_payload(&doc) {
Ok(r) => r,
Err(resp) => return resp,
};
if let Err(resp) = initiate_precheck(state, auth, &doc, INITIATE_IMPORT_SLUG).await {
return resp;
}
let deps = crate::operations::descriptor_deps_from_app_state(state);
match descriptors::initiate_import(&deps, auth, req).await {
Ok(body) => {
record_bundle_event(
state,
auth,
"backup.initiate-import",
&body.descriptor.bundle_id,
format!(
"sha256={} bytes={} expires={}",
body.descriptor.expected_sha256,
body.descriptor.expected_size_bytes,
body.descriptor.expires_at
),
)
.await;
success_response(&doc, body)
}
Err(e) => app_error_to_reject(&doc, e),
}
}
pub(super) async fn handle_finalize_import(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
let req: FinalizeImportBody = match parse_payload(&doc) {
Ok(r) => r,
Err(resp) => return resp,
};
let deps = crate::operations::descriptor_deps_from_app_state(state);
match descriptors::finalize_import(&deps, auth, req).await {
Ok(body) => {
record_bundle_event(
state,
auth,
"backup.finalize-import",
&body.bundle_id,
format!(
"status={} source={} keys={} acls={} contexts={}",
body.status,
body.source_did.as_deref().unwrap_or("unknown"),
body.key_count,
body.acl_count,
body.context_count
),
)
.await;
success_response(&doc, body)
}
Err(e) => app_error_to_reject(&doc, e),
}
}
pub(super) async fn handle_abort(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
let req: AbortBundleBody = match parse_payload(&doc) {
Ok(r) => r,
Err(resp) => return resp,
};
let deps = crate::operations::descriptor_deps_from_app_state(state);
match descriptors::abort_bundle(&deps, auth, req).await {
Ok(body) => {
record_bundle_event(
state,
auth,
"backup.abort",
&body.bundle_id,
format!("aborted={}", body.aborted),
)
.await;
success_response(&doc, body)
}
Err(e) => app_error_to_reject(&doc, e),
}
}
#[cfg(test)]
mod tests {
use super::*;
use trust_tasks_rs::TypeUri;
fn doc(uri: &str) -> TrustTask<Value> {
let uri: TypeUri = uri.parse().expect("backup uri");
TrustTask::new("urn:uuid:test", uri, serde_json::json!({}))
}
#[test]
fn transport_unavailable_uses_the_specified_extended_code() {
for (uri, slug) in [
(
vta_sdk::trust_tasks::TASK_BACKUP_INITIATE_EXPORT_1_0,
INITIATE_EXPORT_SLUG,
),
(
vta_sdk::trust_tasks::TASK_BACKUP_INITIATE_IMPORT_1_0,
INITIATE_IMPORT_SLUG,
),
] {
assert!(
uri.contains(slug),
"{uri}: the slug constant must match the dispatched URI"
);
let outcome = transport_unavailable(&doc(uri), slug);
let parsed: Value = serde_json::from_slice(&outcome.body).expect("error doc");
assert_eq!(
parsed["payload"]["code"],
format!("{slug}:transportUnavailable"),
"{parsed}"
);
assert_ne!(
outcome.status,
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
"transportUnavailable is not an internal error"
);
assert!(
!parsed["payload"]["message"]
.as_str()
.unwrap_or_default()
.contains("public_url"),
"the wire message must not name configuration: {parsed}"
);
}
}
}