use std::collections::HashMap;
use super::helpers::TrustTaskOutcome;
use serde_json::Value;
use trust_tasks_rs::TrustTask;
use vta_sdk::did_templates::TemplateVars;
use vta_sdk::protocols::did_template_management::{
create::CreateDidTemplateBody,
delete::{DeleteDidTemplateBody, DeleteDidTemplateResultBody},
get::GetDidTemplateBody,
list::{ListDidTemplatesBody, ListDidTemplatesResultBody},
render::{RenderDidTemplateBody, RenderDidTemplateResultBody},
update::UpdateDidTemplateBody,
};
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, success_response,
};
use trust_tasks_rs::RejectReason;
pub(super) async fn handle_list(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
let req: ListDidTemplatesBody = match parse_payload(&doc) {
Ok(r) => r,
Err(resp) => return resp,
};
let result = match req.context_id {
Some(context_id) => {
operations::did_templates::list_context(
&state.did_templates_ks,
auth,
&context_id,
TRANSPORT_TRUST_TASK,
)
.await
}
None => {
operations::did_templates::list_global(
&state.did_templates_ks,
auth,
TRANSPORT_TRUST_TASK,
)
.await
}
};
match result {
Ok(templates) => {
if let Some(reject) = check_record_schema_version(
&doc,
templates.iter().map(|t| t.template.schema_version),
) {
return reject;
}
success_response(&doc, ListDidTemplatesResultBody { templates })
}
Err(e) => app_error_to_reject(&doc, e),
}
}
fn max_template_schema_version(type_uri: &str) -> u32 {
if type_uri.ends_with("/3.0") { 2 } else { 1 }
}
fn check_template_schema_version(
doc: &TrustTask<Value>,
template: &vta_sdk::did_templates::DidTemplate,
) -> Option<TrustTaskOutcome> {
let ceiling = max_template_schema_version(&doc.type_uri.to_string());
if template.schema_version <= ceiling {
return None;
}
Some(reject_with(
doc,
RejectReason::MalformedRequest {
reason: format!(
"template declares schemaVersion {} but this task version accepts at most {} \
— send it to the 3.0 task URI, which is the version that can express it",
template.schema_version, ceiling,
),
},
))
}
fn check_record_schema_version(
doc: &TrustTask<Value>,
records: impl IntoIterator<Item = u32>,
) -> Option<TrustTaskOutcome> {
let ceiling = max_template_schema_version(&doc.type_uri.to_string());
let highest = records.into_iter().max().unwrap_or(0);
if highest <= ceiling {
return None;
}
Some(reject_with(
doc,
RejectReason::TaskFailed {
reason: format!(
"a stored template declares schemaVersion {highest}, which this task version \
cannot return — read it through the 3.0 task URI, which can"
),
details: Some(serde_json::json!({ "reason": "schemaVersionTooHigh" })),
},
))
}
pub(super) async fn handle_create(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
let req: CreateDidTemplateBody = match parse_payload(&doc) {
Ok(r) => r,
Err(resp) => return resp,
};
if let Some(reject) = check_template_schema_version(&doc, &req.template) {
return reject;
}
let result = match req.context_id {
Some(context_id) => {
operations::did_templates::create_context(
&state.did_templates_ks,
&state.contexts_ks,
&state.audit_sink,
auth,
&context_id,
req.template,
TRANSPORT_TRUST_TASK,
)
.await
}
None => {
operations::did_templates::create_global(
&state.did_templates_ks,
&state.audit_sink,
auth,
req.template,
TRANSPORT_TRUST_TASK,
)
.await
}
};
match result {
Ok(record) => success_response(&doc, record),
Err(e) => app_error_to_reject(&doc, e),
}
}
pub(super) async fn handle_get(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
let req: GetDidTemplateBody = match parse_payload(&doc) {
Ok(r) => r,
Err(resp) => return resp,
};
let result = match req.context_id {
Some(context_id) => {
operations::did_templates::get_context(
&state.did_templates_ks,
auth,
&context_id,
&req.name,
TRANSPORT_TRUST_TASK,
)
.await
}
None => {
operations::did_templates::get_global(
&state.did_templates_ks,
auth,
&req.name,
TRANSPORT_TRUST_TASK,
)
.await
}
};
match result {
Ok(record) => {
if let Some(reject) =
check_record_schema_version(&doc, [record.template.schema_version])
{
return reject;
}
success_response(&doc, record)
}
Err(e) => app_error_to_reject(&doc, e),
}
}
pub(super) async fn handle_update(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
let req: UpdateDidTemplateBody = match parse_payload(&doc) {
Ok(r) => r,
Err(resp) => return resp,
};
if let Some(reject) = check_template_schema_version(&doc, &req.template) {
return reject;
}
let result = match req.context_id {
Some(context_id) => {
operations::did_templates::update_context(
&state.did_templates_ks,
&state.audit_sink,
auth,
&context_id,
&req.name,
req.template,
TRANSPORT_TRUST_TASK,
)
.await
}
None => {
operations::did_templates::update_global(
&state.did_templates_ks,
&state.audit_sink,
auth,
&req.name,
req.template,
TRANSPORT_TRUST_TASK,
)
.await
}
};
match result {
Ok(record) => success_response(&doc, record),
Err(e) => app_error_to_reject(&doc, e),
}
}
pub(super) async fn handle_delete(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
let req: DeleteDidTemplateBody = match parse_payload(&doc) {
Ok(r) => r,
Err(resp) => return resp,
};
let result = match &req.context_id {
Some(context_id) => {
operations::did_templates::delete_context(
&state.did_templates_ks,
&state.audit_sink,
auth,
context_id,
&req.name,
TRANSPORT_TRUST_TASK,
)
.await
}
None => {
operations::did_templates::delete_global(
&state.did_templates_ks,
&state.audit_sink,
auth,
&req.name,
TRANSPORT_TRUST_TASK,
)
.await
}
};
match result {
Ok(()) => success_response(
&doc,
DeleteDidTemplateResultBody {
name: req.name,
deleted: true,
},
),
Err(e) => app_error_to_reject(&doc, e),
}
}
pub(super) async fn handle_render(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
let req: RenderDidTemplateBody = match parse_payload(&doc) {
Ok(r) => r,
Err(resp) => return resp,
};
let caller_vars = vars_from_hashmap(req.vars);
let config_guard = state.config.read().await;
let result = match req.context_id {
Some(context_id) => {
operations::did_templates::render_context(
&state.did_templates_ks,
&state.contexts_ks,
&config_guard,
auth,
&context_id,
&req.name,
caller_vars,
TRANSPORT_TRUST_TASK,
)
.await
}
None => {
operations::did_templates::render_global(
&state.did_templates_ks,
&config_guard,
auth,
&req.name,
caller_vars,
TRANSPORT_TRUST_TASK,
)
.await
}
};
match result {
Ok(document) => success_response(&doc, RenderDidTemplateResultBody { document }),
Err(e) => app_error_to_reject(&doc, e),
}
}
fn vars_from_hashmap(map: HashMap<String, Value>) -> TemplateVars {
let mut vars = TemplateVars::new();
for (k, v) in map {
vars.insert(k, v);
}
vars
}
#[cfg(test)]
mod schema_ceiling_tests {
use super::max_template_schema_version;
#[test]
fn a_task_version_accepts_only_the_template_schema_it_can_express() {
for uri in [
"https://trusttasks.org/spec/vta/did-templates/create/2.0",
"https://trusttasks.org/spec/vta/did-templates/update/2.0",
] {
assert_eq!(
max_template_schema_version(uri),
1,
"{uri} must not accept a v2 template — its schema pins schemaVersion to 1"
);
}
for uri in [
"https://trusttasks.org/spec/vta/did-templates/create/3.0",
"https://trusttasks.org/spec/vta/did-templates/update/3.0",
] {
assert_eq!(
max_template_schema_version(uri),
2,
"{uri} is the version that exists to carry a keys block"
);
}
}
#[test]
fn an_unrecognised_version_defaults_to_the_narrow_ceiling() {
let stem = vta_sdk::trust_tasks::TASK_DID_TEMPLATES_CREATE_3_0
.rsplit_once('/')
.expect("a task URI ends in /<version>")
.0;
let unknown = format!("{stem}/9.9");
assert_eq!(max_template_schema_version(&unknown), 1);
}
}