use serde_json::Value;
use trust_tasks_rs::TrustTask;
use trust_tasks_rs::specs::vtc::members::admit::v0_1 as admit;
use trust_tasks_rs::specs::vtc::members::make_membership::v0_1 as make;
use super::helpers::{TrustTaskOutcome, success_response, task_error_to_reject};
use super::{JoinAuthCtx, capable_signer, parse_spec_payload};
use crate::acl::admin_consent::Operation;
use crate::acl::{Capability, VtcRole};
use crate::ceremony::admission::{self, AdmissionRequest, Admitted, codes};
use crate::error::{AppError, TaskError};
use crate::server::AppState;
pub(crate) const ADMIT_TYPE: &str = <admit::Payload as trust_tasks_rs::Payload>::TYPE_URI;
pub(crate) const MAKE_MEMBERSHIP_TYPE: &str = <make::Payload as trust_tasks_rs::Payload>::TYPE_URI;
pub(crate) const URIS: &[&str] = &[ADMIT_TYPE, MAKE_MEMBERSHIP_TYPE];
macro_rules! admitted_response {
($name:ident, $m:ident) => {
pub(crate) fn $name(a: Admitted) -> Result<$m::Response, AppError> {
let label = a
.label
.map(|l| l.parse::<$m::AdmittedMembershipLabel>())
.transpose()
.map_err(|e| AppError::Internal(format!("admitted label: {e}")))?;
$m::AdmittedMembership::builder()
.subject(a.subject.as_str())
.role(a.role.to_string())
.label(label)
.status($m::AdmittedMembershipStatus::Admitted)
.returning(a.returning)
.approval(a.approval.as_str())
.status_list_index(u64::from(a.creds.status_list_index))
.vmc(credential_object(&a.creds.vmc)?)
.role_vac(credential_object(&a.creds.role_vac)?)
.try_into()
.map($m::Response)
.map_err(|e: $m::error::ConversionError| {
AppError::Internal(format!("admitted membership out of schema: {e}"))
})
}
};
}
admitted_response!(admit_response, admit);
admitted_response!(make_response, make);
fn credential_object(
vc: &affinidi_vc::VerifiableCredential,
) -> Result<serde_json::Map<String, Value>, AppError> {
match serde_json::to_value(vc) {
Ok(Value::Object(map)) => Ok(map),
Ok(_) => Err(AppError::Internal("credential is not a JSON object".into())),
Err(e) => Err(AppError::Internal(format!(
"serialise credential for response: {e}"
))),
}
}
pub(super) async fn dispatch(
state: &AppState,
ctx: &JoinAuthCtx,
doc: TrustTask<Value>,
type_uri: &str,
) -> Option<TrustTaskOutcome> {
if !URIS.contains(&type_uri) {
return None;
}
Some(handle(state, ctx, doc, type_uri).await)
}
fn read_request(
doc: &TrustTask<Value>,
type_uri: &str,
) -> Result<(AdmissionRequest, Option<String>), TrustTaskOutcome> {
if type_uri == ADMIT_TYPE {
let p: admit::Payload = parse_spec_payload(doc)?;
let role = p.role.parse::<VtcRole>().map_err(|_| {
task_error_to_reject(
doc,
&TaskError::declared(
codes::ADMIT_ROLE_NOT_PERMITTED,
AppError::Validation(format!("unknown role `{}`", *p.role)),
),
)
})?;
Ok((
AdmissionRequest::Add {
subject: p.subject.to_string(),
role,
label: p.label.map(String::from),
},
p.reason.map(String::from),
))
} else {
let p: make::Payload = parse_spec_payload(doc)?;
Ok((
AdmissionRequest::MakeMembership {
subject: p.subject.to_string(),
},
p.reason.map(String::from),
))
}
}
async fn handle(
state: &AppState,
ctx: &JoinAuthCtx,
doc: TrustTask<Value>,
type_uri: &str,
) -> TrustTaskOutcome {
let actor = match capable_signer(state, ctx, &doc, Capability::MembersManage, None).await {
Ok(a) => a,
Err(reject) => return reject,
};
let (request, reason) = match read_request(&doc, type_uri) {
Ok(r) => r,
Err(reject) => return reject,
};
let outcome = admission::admit_directly(
state,
&actor.did,
request,
Operation {
type_uri,
payload: &doc.payload,
},
reason.as_deref(),
)
.await;
let admitted = match outcome {
Ok(a) => a,
Err(e) => return task_error_to_reject(&doc, &e),
};
let response = if type_uri == ADMIT_TYPE {
admit_response(admitted).map(|r| success_response(&doc, r))
} else {
make_response(admitted).map(|r| success_response(&doc, r))
};
response.unwrap_or_else(|e| task_error_to_reject(&doc, &TaskError::from(e)))
}