use super::helpers::TrustTaskOutcome;
use serde_json::{Value, json};
use trust_tasks_rs::{RejectReason, TrustTask};
use vta_sdk::protocols::acl_management::change_role::ChangeRoleBody;
use vta_sdk::protocols::acl_management::create::CreateAclBody;
use vta_sdk::protocols::acl_management::delete::DeleteAclBody;
use vta_sdk::protocols::acl_management::get::GetAclBody;
use vta_sdk::protocols::acl_management::list::ListAclBody;
use vta_sdk::protocols::acl_management::swap::SwapKeyBody;
use vta_sdk::protocols::acl_management::update::UpdateAclBody;
use crate::auth::AuthClaims;
use crate::error::AppError;
use crate::operations;
use crate::operations::step_up::{StepUpDecision, op, resolve_step_up};
use crate::server::AppState;
use super::helpers::{
TRANSPORT_TRUST_TASK, app_error_to_reject, parse_payload, reject_with, success_response,
};
pub(super) async fn handle_list(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
if let Err(e) = auth.require_manage() {
return app_error_to_reject(&doc, e);
}
let req: ListAclBody = match parse_payload(&doc) {
Ok(r) => r,
Err(resp) => return resp,
};
match operations::acl::list_acl(
&state.acl_ks,
auth,
req.scope.as_deref(),
req.direction.unwrap_or_default(),
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_manage() {
return app_error_to_reject(&doc, e);
}
let req: CreateAclBody = match parse_payload(&doc) {
Ok(r) => r,
Err(resp) => return resp,
};
match operations::acl::grant_from_entry(
&state.acl_ks,
&state.audit_ks,
&state.contexts_ks,
auth,
req.entry,
TRANSPORT_TRUST_TASK,
)
.await
{
Ok(body) => success_response(&doc, body),
Err(e) => app_error_to_reject(&doc, e),
}
}
pub(super) async fn handle_get(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
if let Err(e) = auth.require_manage() {
return app_error_to_reject(&doc, e);
}
let req: GetAclBody = match parse_payload(&doc) {
Ok(r) => r,
Err(resp) => return resp,
};
match operations::acl::get_acl(&state.acl_ks, auth, &req.subject, TRANSPORT_TRUST_TASK).await {
Ok(body) => success_response(&doc, body),
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_admin() {
return app_error_to_reject(&doc, e);
}
let req: UpdateAclBody = match parse_payload(&doc) {
Ok(r) => r,
Err(resp) => return resp,
};
let role = None;
match operations::acl::update_from_params(
&state.acl_ks,
&state.audit_ks,
&state.contexts_ks,
auth,
&req.did,
operations::acl::UpdateAclParams {
role,
label: req.label,
allowed_contexts: req.allowed_contexts,
step_up_approver: req.step_up_approver,
step_up_require: req.step_up_require,
approve_scope: req.approve_scope,
allowed_keys: req
.allowed_keys
.map(|r| r.map(|keys| keys.into_iter().collect())),
},
TRANSPORT_TRUST_TASK,
)
.await
{
Ok(body) => success_response(&doc, body),
Err(e) => app_error_to_reject(&doc, e),
}
}
pub(super) async fn handle_change_role(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
if let Err(e) = auth.require_admin() {
return app_error_to_reject(&doc, e);
}
let req: ChangeRoleBody = match parse_payload(&doc) {
Ok(r) => r,
Err(resp) => return resp,
};
match operations::acl::change_role(
&state.acl_ks,
&state.audit_ks,
auth,
&req.subject,
&req.from_role,
&req.to_role,
req.reason.as_deref(),
TRANSPORT_TRUST_TASK,
)
.await
{
Ok(body) => success_response(&doc, body),
Err(e) => app_error_to_reject(&doc, e),
}
}
pub(super) async fn handle_delete(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
if let Err(e) = auth.require_manage() {
return app_error_to_reject(&doc, e);
}
let req: DeleteAclBody = match parse_payload(&doc) {
Ok(r) => r,
Err(resp) => return resp,
};
match operations::acl::revoke_by_subject(
&state.acl_ks,
&state.audit_ks,
auth,
&req.subject,
req.scopes,
TRANSPORT_TRUST_TASK,
)
.await
{
Ok(body) => success_response(&doc, body),
Err(e) => app_error_to_reject(&doc, e),
}
}
pub(super) async fn handle_swap_key(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
let req: SwapKeyBody = match parse_payload(&doc) {
Ok(r) => r,
Err(resp) => return resp,
};
if req.current_subject != auth.did {
return reject_with(
&doc,
RejectReason::MalformedRequest {
reason: format!(
"acl/swap-key: currentSubject {} does not equal authenticated caller {}",
req.current_subject, auth.did
),
},
);
}
if !matches!(
resolve_step_up(
&state.config,
&state.acl_ks,
op::ACL_SWAP_KEY,
&auth.did,
true, )
.await,
StepUpDecision::Allow
) {
return reject_with(
&doc,
RejectReason::TaskFailed {
reason: "auth:step_up_required".to_string(),
details: Some(json!({
"requiredAcr": "aal2",
"reason": "acl/swap-key requires a stepped-up (AAL2) session under this \
VTA's step-up policy. Sender-authenticated transports \
(DIDComm/TSP) are AAL1 and cannot be elevated in-band — perform \
this self-service rotation over the authenticated REST session.",
})),
},
);
}
let did_resolver = match state.did_resolver.as_ref() {
Some(r) => r,
None => {
return app_error_to_reject(
&doc,
AppError::Internal("DID resolver not available".into()),
);
}
};
let vta_did = match state.config.read().await.vta_did.clone() {
Some(v) => v,
None => {
return app_error_to_reject(&doc, AppError::Internal("VTA DID not configured".into()));
}
};
match operations::acl::swap_acl(
&state.acl_ks,
&state.audit_ks,
auth,
&req.link_proof,
did_resolver,
&vta_did,
TRANSPORT_TRUST_TASK,
)
.await
{
Ok(result) => {
if req.new_subject != result.did {
return reject_with(
&doc,
RejectReason::MalformedRequest {
reason: format!(
"acl/swap-key: newSubject {} does not match verified VP holder {}",
req.new_subject, result.did
),
},
);
}
success_response(&doc, result)
}
Err(e) => app_error_to_reject(&doc, e),
}
}
#[cfg(test)]
mod tests {
use super::*;
use trust_tasks_rs::specs::acl::change_role::v0_1 as canonical_change_role;
#[test]
fn change_role_conforms_and_update_carries_no_role() {
let change = ChangeRoleBody {
subject: "did:key:z6MkSubject".into(),
from_role: "reader".into(),
to_role: "application".into(),
reason: Some("promoted".into()),
};
let json = serde_json::to_value(&change).expect("serialize");
serde_json::from_value::<canonical_change_role::Payload>(json.clone())
.unwrap_or_else(|e| panic!("not canonical `acl/change-role/0.1`: {e}\n{json:#}"));
let base = serde_json::to_value(UpdateAclBody {
did: "did:key:z6MkSubject".into(),
label: None,
allowed_contexts: None,
step_up_approver: None,
step_up_require: None,
approve_scope: None,
allowed_keys: None,
})
.expect("serialize");
assert!(
base.get("role").is_none(),
"acl/update must not carry a role: {base:#}"
);
}
}