use super::helpers::TrustTaskOutcome;
use serde_json::Value;
use trust_tasks_rs::TrustTask;
use vta_sdk::protocols::context_management::create::CreateContextBody;
use vta_sdk::protocols::context_management::delete::{DeleteContextBody, DeleteContextPreviewBody};
use vta_sdk::protocols::context_management::get::GetContextBody;
use vta_sdk::protocols::context_management::list::ListContextsBody;
use vta_sdk::protocols::context_management::update::UpdateContextBody;
use vta_sdk::protocols::context_management::update_did::UpdateContextDidBody;
use crate::auth::AuthClaims;
use crate::operations;
use crate::server::AppState;
use super::helpers::{
TRANSPORT_TRUST_TASK, app_error_to_reject, parse_payload, reject_with_code, success_response,
};
fn slug_from_doc(doc: &TrustTask<Value>) -> String {
doc.type_uri
.to_string()
.strip_prefix("https://trusttasks.org/spec/")
.and_then(|rest| rest.rsplit_once('/'))
.map(|(slug, _ver)| slug.to_string())
.unwrap_or_else(|| "vta/contexts/delete".to_string())
}
fn ext(slug: &str, local: &str) -> trust_tasks_rs::TrustTaskCode {
trust_tasks_rs::TrustTaskCode::new_extended(slug, local)
.expect("contexts extended code is grammar-valid")
}
fn reject_context_error(
doc: &TrustTask<Value>,
e: operations::contexts::ContextError,
) -> TrustTaskOutcome {
use operations::contexts::ContextError;
let slug = slug_from_doc(doc);
match e {
ContextError::Unreachable => reject_with_code(
doc,
ext(&slug, "notFound"),
"no context with that id is reachable by this caller",
None,
),
ContextError::ParentUnreachable => reject_with_code(
doc,
ext(&slug, "parentNotFound"),
"no context with that parent id is reachable by this caller",
None,
),
ContextError::NotEmpty(holds) => reject_with_code(
doc,
ext(&slug, "notEmpty"),
format!(
"context holds {}; retry with force to delete the whole subtree, or preview it \
first",
holds.summary()
),
Some(serde_json::json!({
"subContexts": holds.sub_contexts,
"keys": holds.keys,
"webvhDids": holds.webvh_dids,
"aclEntries": holds.acl_entries,
"didTemplates": holds.did_templates,
})),
),
ContextError::Other(e) => app_error_to_reject(doc, e),
}
}
pub(super) async fn handle_list(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
let _req: ListContextsBody = match parse_payload(&doc) {
Ok(r) => r,
Err(resp) => return resp,
};
match operations::contexts::list_contexts(&state.contexts_ks, auth, TRANSPORT_TRUST_TASK).await
{
Ok(body) => success_response(&doc, body),
Err(e) => app_error_to_reject(&doc, e),
}
}
pub(super) async fn handle_create(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
if let Err(e) = auth.require_admin() {
return app_error_to_reject(&doc, e);
}
let req: CreateContextBody = match parse_payload(&doc) {
Ok(r) => r,
Err(resp) => return resp,
};
let audit_id = req.id.clone();
match operations::contexts::create_context(
&state.contexts_ks,
auth,
&req.id,
req.name,
req.description,
req.parent,
TRANSPORT_TRUST_TASK,
)
.await
{
Ok(body) => {
if let Err(e) = crate::audit::record_with_detail(
&state.audit_sink,
"contexts.create",
&auth.did,
Some(&audit_id),
"success",
Some(TRANSPORT_TRUST_TASK),
Some(&audit_id),
None,
)
.await
{
tracing::warn!(error = %e, "audit record failed for contexts.create");
}
success_response(&doc, body)
}
Err(e) => reject_context_error(&doc, e),
}
}
pub(super) async fn handle_get(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
let req: GetContextBody = match parse_payload(&doc) {
Ok(r) => r,
Err(resp) => return resp,
};
match operations::contexts::get_context_op(
&state.contexts_ks,
auth,
&req.id,
TRANSPORT_TRUST_TASK,
)
.await
{
Ok(body) => success_response(&doc, body),
Err(e) => reject_context_error(&doc, e),
}
}
pub(super) async fn handle_secrets(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
let req: vta_sdk::protocols::context_management::secrets::GetContextSecretsBody =
match parse_payload(&doc) {
Ok(r) => r,
Err(resp) => return resp,
};
let deps = operations::export::ExportDeps {
keys_ks: &state.keys_ks,
contexts_ks: &state.contexts_ks,
imported_ks: &state.imported_ks,
audit: &state.audit_sink,
acl_ks: &state.acl_ks,
#[cfg(feature = "webvh")]
webvh_ks: &state.webvh_ks,
seed_store: &state.seed_store,
};
match operations::export::get_context_secrets(&deps, auth, &req.id, TRANSPORT_TRUST_TASK).await
{
Ok(bundle) => success_response(
&doc,
vta_sdk::protocols::context_management::secrets::ContextSecretsResultBody::from(bundle),
),
Err(e) => app_error_to_reject(&doc, e),
}
}
pub(super) async fn handle_update(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
if let Err(e) = auth.require_super_admin() {
return app_error_to_reject(&doc, e);
}
let req: UpdateContextBody = match parse_payload(&doc) {
Ok(r) => r,
Err(resp) => return resp,
};
match operations::contexts::update_context(
&state.contexts_ks,
auth,
&req.id,
operations::contexts::UpdateContextParams {
name: req.name,
did: req.did,
description: req.description,
context_policy: req.context_policy,
},
TRANSPORT_TRUST_TASK,
)
.await
{
Ok(body) => success_response(&doc, body),
Err(e) => reject_context_error(&doc, e),
}
}
pub(super) async fn handle_update_did(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
if let Err(e) = auth.require_admin() {
return app_error_to_reject(&doc, e);
}
let req: UpdateContextDidBody = match parse_payload(&doc) {
Ok(r) => r,
Err(resp) => return resp,
};
match operations::contexts::update_context_did(
&state.contexts_ks,
auth,
&req.id,
req.did,
TRANSPORT_TRUST_TASK,
)
.await
{
Ok(body) => success_response(&doc, body),
Err(e) => reject_context_error(&doc, e),
}
}
pub(super) async fn handle_preview_delete(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
if let Err(e) = auth.require_admin() {
return app_error_to_reject(&doc, e);
}
let req: DeleteContextPreviewBody = match parse_payload(&doc) {
Ok(r) => r,
Err(resp) => return resp,
};
match operations::contexts::preview_delete_context(
&state.contexts_ks,
&state.keys_ks,
&state.acl_ks,
&state.did_templates_ks,
#[cfg(feature = "webvh")]
&state.webvh_ks,
auth,
&req.id,
TRANSPORT_TRUST_TASK,
)
.await
{
Ok(body) => success_response(&doc, body),
Err(e) => reject_context_error(&doc, e),
}
}
pub(super) async fn handle_delete(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
if let Err(e) = auth.require_admin() {
return app_error_to_reject(&doc, e);
}
let req: DeleteContextBody = match parse_payload(&doc) {
Ok(r) => r,
Err(resp) => return resp,
};
let ks = operations::keyspaces_from_app_state(state);
#[cfg(feature = "webvh")]
let outcome = {
let vta_did = state.config.read().await.vta_did.clone();
match state.did_resolver.as_ref() {
Some(did_resolver) => {
let deps = operations::did_webvh::WebvhDeps::from_app_state(state, did_resolver);
let cleanup = operations::contexts::ContextDidCleanup {
deps: &deps,
vta_did: vta_did.as_deref(),
};
operations::contexts::delete_context(
&ks,
auth,
&req.id,
req.force,
TRANSPORT_TRUST_TASK,
Some(&cleanup),
)
.await
}
None => {
operations::contexts::delete_context(
&ks,
auth,
&req.id,
req.force,
TRANSPORT_TRUST_TASK,
None,
)
.await
}
}
};
#[cfg(not(feature = "webvh"))]
let outcome =
operations::contexts::delete_context(&ks, auth, &req.id, req.force, TRANSPORT_TRUST_TASK)
.await;
match outcome {
Ok(body) => success_response(&doc, body),
Err(e) => reject_context_error(&doc, e),
}
}
#[cfg(test)]
mod secrets_gate_tests {
use super::*;
use crate::acl::Role;
use crate::test_support::build_signing_test_app_state;
use serde_json::json;
use trust_tasks_rs::TypeUri;
use vti_common::acl::{AclEntry, store_acl_entry};
#[tokio::test]
async fn vti_vta_003_refusal_is_permission_denied_and_names_the_fix() {
let (state, _dir) = build_signing_test_app_state().await;
let did = "did:key:zRoomHost";
store_acl_entry(
&state.acl_ks,
&AclEntry::new(did, Role::Application, "did:key:zRoot")
.with_contexts(vec!["rooms".to_string()]),
)
.await
.expect("store the caller's entry");
let auth = AuthClaims {
did: did.into(),
role: Role::Application,
allowed_contexts: vec!["rooms".to_string()],
session_id: "test-session".into(),
access_expires_at: 0,
issued_at: 0,
amr: Vec::new(),
acr: String::new(),
};
let uri: TypeUri = vta_sdk::trust_tasks::TASK_CONTEXTS_SECRETS_1_0
.parse()
.expect("contexts/secrets uri");
let doc = TrustTask::new(
format!("urn:uuid:{}", uuid::Uuid::new_v4()),
uri,
json!({ "id": "rooms" }),
);
let out = handle_secrets(&state, &auth, doc).await;
let body: Value = serde_json::from_slice(&out.body).expect("response is JSON");
assert_eq!(
body.pointer("/payload/code").and_then(Value::as_str),
Some("permissionDenied"),
"{body}"
);
let message = body.to_string();
assert!(
message.contains(
"pnm acl change-role --did did:key:zRoomHost --from application --to admin"
),
"the fix command must reach the caller: {message}"
);
}
}