#![allow(clippy::result_large_err)]
mod helpers;
use serde_json::Value;
use trust_tasks_rs::{RejectReason, TrustTask};
use vti_common::error::AppError;
use vta_sdk::protocols::join_requests::{
self as jr, JoinRequestStatusBody, JoinRequestSubmitBody, VerdictResponse,
};
use crate::join::{JoinSubmitOutcome, JoinTransport};
use crate::server::AppState;
pub(crate) use helpers::TrustTaskOutcome;
use helpers::{
app_error_to_reject, body_parse_error_response, parse_payload, reject_with, success_response,
verdict_response, verify_trust_task_proof,
};
pub(crate) struct JoinAuthCtx {
pub transport: JoinTransport,
pub sender_did: Option<String>,
}
impl JoinAuthCtx {
pub fn didcomm(sender_did: String) -> Self {
Self {
transport: JoinTransport::DIDComm,
sender_did: Some(sender_did),
}
}
#[allow(dead_code)]
pub fn rest() -> Self {
Self {
transport: JoinTransport::Rest,
sender_did: None,
}
}
}
pub(crate) async fn dispatch_trust_task_core(
state: &AppState,
ctx: &JoinAuthCtx,
body: &[u8],
) -> TrustTaskOutcome {
let doc: TrustTask<Value> = match serde_json::from_slice(body) {
Ok(d) => d,
Err(e) => return body_parse_error_response(&e.to_string()),
};
if let Some(vtc_did) = state.config.read().await.vtc_did.clone()
&& let Err(reason) = doc.validate_basic(chrono::Utc::now(), &vtc_did)
{
return reject_with(&doc, reason);
}
let type_uri = doc.type_uri.to_string();
match type_uri.as_str() {
jr::JOIN_REQUEST_SUBMIT_TYPE => handle_submit(state, ctx, doc).await,
jr::JOIN_REQUEST_ACCEPT_TYPE => handle_accept(state, ctx, doc).await,
jr::JOIN_REQUEST_MANIFEST_TYPE => handle_manifest(state, doc).await,
jr::JOIN_REQUEST_STATUS_TYPE => handle_status(state, ctx, doc).await,
other => reject_with(
&doc,
RejectReason::UnsupportedType {
type_uri: other.to_string(),
},
),
}
}
#[cfg_attr(not(test), allow(dead_code))]
pub(crate) const DISPATCHED_URIS: &[&str] = &[
jr::JOIN_REQUEST_SUBMIT_TYPE,
jr::JOIN_REQUEST_ACCEPT_TYPE,
jr::JOIN_REQUEST_MANIFEST_TYPE,
jr::JOIN_REQUEST_STATUS_TYPE,
];
async fn resolve_holder(
ctx: &JoinAuthCtx,
doc: &TrustTask<Value>,
) -> Result<String, TrustTaskOutcome> {
let proven = match &ctx.sender_did {
Some(did) => did.clone(),
None => match verify_trust_task_proof(doc).await {
Ok(did) => did,
Err(e) => return Err(app_error_to_reject(doc, &e)),
},
};
if let Some(issuer) = doc.issuer.as_deref() {
let issuer_base = issuer.split('#').next().unwrap_or(issuer);
if issuer_base != proven {
return Err(reject_with(
doc,
RejectReason::PermissionDenied {
reason: format!(
"document issuer ({issuer_base}) does not match the authenticated holder ({proven})"
),
},
));
}
}
Ok(proven)
}
async fn handle_submit(
state: &AppState,
ctx: &JoinAuthCtx,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
let applicant_did = match resolve_holder(ctx, &doc).await {
Ok(did) => did,
Err(reject) => return reject,
};
let body: JoinRequestSubmitBody = match parse_payload(&doc) {
Ok(b) => b,
Err(reject) => return reject,
};
let outcome = match crate::join::submit_inner(
state,
applicant_did,
body.vp,
body.registry_consent,
body.extensions,
None,
ctx.transport,
)
.await
{
Ok(o) => o,
Err(e) => return app_error_to_reject(&doc, &e),
};
match outcome_to_verdict(&outcome) {
Ok(v) => verdict_response(&doc, v),
Err(e) => app_error_to_reject(&doc, &e),
}
}
fn outcome_to_verdict(outcome: &JoinSubmitOutcome) -> Result<VerdictResponse, AppError> {
use crate::ceremony::verdict::Verdict as PolicyVerdict;
let request_id = outcome.request.id;
if let Some(admit) = &outcome.admit {
let role = outcome
.request
.policy_decision
.clone()
.and_then(|pd| serde_json::from_value::<PolicyVerdict>(pd).ok())
.and_then(|v| match v {
PolicyVerdict::Allow(a) => a.role,
_ => None,
});
let vmc = serde_json::to_value(&admit.vmc)
.map_err(|e| AppError::Internal(format!("serialise VMC: {e}")))?;
let role_vec = serde_json::to_value(&admit.role_vec)
.map_err(|e| AppError::Internal(format!("serialise role VEC: {e}")))?;
return Ok(VerdictResponse::allow(
request_id,
role,
Some(vmc),
Some(role_vec),
));
}
let decision = outcome
.request
.policy_decision
.clone()
.and_then(|pd| serde_json::from_value::<PolicyVerdict>(pd).ok());
let verdict = match decision {
Some(PolicyVerdict::RequestMore(rm)) => VerdictResponse {
request_id,
verdict: jr::Verdict {
effect: jr::VerdictEffect::RequestMore,
with: jr::VerdictWith {
needs: rm.needs,
presentation_definition: Some(rm.presentation_definition),
..Default::default()
},
},
},
Some(PolicyVerdict::Deny(d)) => VerdictResponse {
request_id,
verdict: jr::Verdict {
effect: jr::VerdictEffect::Deny,
with: jr::VerdictWith {
code: Some(d.code),
reason: d.reason,
..Default::default()
},
},
},
Some(PolicyVerdict::Refer(r)) => {
VerdictResponse::refer(request_id, r.queue, r.reason.unwrap_or_default())
}
_ => VerdictResponse::refer(
request_id,
"admin-review",
"queued for an admin decision (approve/reject)",
),
};
Ok(verdict)
}
async fn handle_accept(
state: &AppState,
ctx: &JoinAuthCtx,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
let member_did = match resolve_holder(ctx, &doc).await {
Ok(did) => did,
Err(reject) => return reject,
};
let body: jr::JoinRequestAcceptBody = match parse_payload(&doc) {
Ok(b) => b,
Err(reject) => return reject,
};
let outcome = match crate::routes::join_requests::accept::accept_inner(
state,
body.request_id,
member_did,
body.vmc_id,
body.vc,
None,
ctx.transport,
)
.await
{
Ok(o) => o,
Err(e) => return app_error_to_reject(&doc, &e),
};
success_response(
&doc,
jr::JoinRequestAcceptReceiptBody {
request_id: outcome.request_id,
status: "accepted".to_string(),
reciprocal_vc_id: outcome.reciprocal_vc_id,
},
)
}
async fn handle_manifest(state: &AppState, doc: TrustTask<Value>) -> TrustTaskOutcome {
match crate::routes::join_requests::manifest::manifest_inner(state).await {
Ok(body) => success_response(&doc, body),
Err(e) => app_error_to_reject(&doc, &e),
}
}
async fn handle_status(
state: &AppState,
ctx: &JoinAuthCtx,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
let applicant_did = match resolve_holder(ctx, &doc).await {
Ok(did) => did,
Err(reject) => return reject,
};
let body: JoinRequestStatusBody = match parse_payload(&doc) {
Ok(b) => b,
Err(reject) => return reject,
};
match crate::routes::join_requests::status::status_inner(
state,
body.request_id,
applicant_did,
None,
)
.await
{
Ok(resp) => success_response(&doc, resp),
Err(e) => app_error_to_reject(&doc, &e),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dispatcher_routes_every_join_uri() {
let sdk = [
jr::JOIN_REQUEST_SUBMIT_TYPE,
jr::JOIN_REQUEST_ACCEPT_TYPE,
jr::JOIN_REQUEST_MANIFEST_TYPE,
jr::JOIN_REQUEST_STATUS_TYPE,
];
for u in DISPATCHED_URIS {
assert!(sdk.contains(u), "dispatched URI not a known join URI: {u}");
}
assert_eq!(DISPATCHED_URIS.len(), sdk.len());
}
#[test]
fn join_uris_are_canonical_type_uris() {
for u in DISPATCHED_URIS {
let parsed: Result<trust_tasks_rs::TypeUri, _> = u.parse();
assert!(parsed.is_ok(), "join URI is not a canonical TypeUri: {u}");
}
}
}