use serde_json::Value;
use trust_tasks_rs::specs::did_management::did::register::v0_1 as did_register;
use trust_tasks_rs::specs::policy::{
activate::v0_1 as policy_activate, active::v0_1 as policy_active, get::v0_1 as policy_get,
list::v0_2 as policy_list, upsert::v0_2 as policy_upsert,
};
use trust_tasks_rs::specs::vtc::policies::test::v0_1 as policy_test;
use trust_tasks_rs::{Payload, TrustTask};
use uuid::Uuid;
use vti_common::auth::extractor::AuthClaims;
use super::helpers::{
TrustTaskOutcome, app_error_to_reject, extended_code, parse_payload, reject_with_code,
success_response, task_error_to_reject,
};
use super::{JoinAuthCtx, admin_signer, parse_spec_payload};
use crate::acl::Capability;
use crate::error::AppError;
use crate::policy::PolicyPurpose;
use crate::routes::policies::{admin as policy_admin, read as policy_read};
use crate::server::AppState;
pub(crate) const POLICY_LIST_TYPE: &str = <policy_list::Payload as Payload>::TYPE_URI;
pub(crate) const POLICY_GET_TYPE: &str = <policy_get::Payload as Payload>::TYPE_URI;
pub(crate) const POLICY_ACTIVE_TYPE: &str = <policy_active::Payload as Payload>::TYPE_URI;
pub(crate) const POLICY_UPSERT_TYPE: &str = <policy_upsert::Payload as Payload>::TYPE_URI;
pub(crate) const POLICY_ACTIVATE_TYPE: &str = <policy_activate::Payload as Payload>::TYPE_URI;
pub(crate) const POLICY_TEST_TYPE: &str = <policy_test::Payload as Payload>::TYPE_URI;
pub(crate) const DID_REGISTER_TYPE: &str = <did_register::Payload as Payload>::TYPE_URI;
pub(crate) const URIS: &[&str] = &[
POLICY_LIST_TYPE,
POLICY_GET_TYPE,
POLICY_ACTIVE_TYPE,
POLICY_UPSERT_TYPE,
POLICY_ACTIVATE_TYPE,
POLICY_TEST_TYPE,
DID_REGISTER_TYPE,
];
pub(super) async fn dispatch(
state: &AppState,
ctx: &JoinAuthCtx,
doc: TrustTask<Value>,
type_uri: &str,
) -> Option<TrustTaskOutcome> {
Some(match type_uri {
POLICY_LIST_TYPE => handle_list(state, ctx, doc).await,
POLICY_GET_TYPE => handle_get(state, ctx, doc).await,
POLICY_ACTIVE_TYPE => handle_active(state, ctx, doc).await,
POLICY_UPSERT_TYPE => handle_upsert(state, ctx, doc).await,
POLICY_ACTIVATE_TYPE => handle_activate(state, ctx, doc).await,
POLICY_TEST_TYPE => handle_test(state, ctx, doc).await,
DID_REGISTER_TYPE => handle_did_register(state, ctx, doc).await,
_ => return None,
})
}
async fn admin_with<P>(
state: &AppState,
ctx: &JoinAuthCtx,
doc: &TrustTask<Value>,
) -> Result<(AuthClaims, P), TrustTaskOutcome>
where
P: trust_tasks_rs::validate::ValidatedPayload + serde::de::DeserializeOwned,
{
let actor = admin_signer(state, ctx, doc).await?;
let payload = parse_spec_payload::<P>(doc)?;
Ok((actor, payload))
}
fn revision_id(doc: &TrustTask<Value>, raw: &str) -> Result<Uuid, TrustTaskOutcome> {
raw.parse().map_err(|_| {
app_error_to_reject(
doc,
&AppError::Validation(format!("policy id {raw:?} is not a UUID")),
)
})
}
async fn handle_list(
state: &AppState,
ctx: &JoinAuthCtx,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
if let Err(reject) = admin_with::<policy_list::Payload>(state, ctx, &doc).await {
return reject;
}
let mut query: policy_read::ListPoliciesQuery = match parse_payload(&doc) {
Ok(q) => q,
Err(reject) => return reject,
};
if let Some(raw) = doc.payload["ext"][policy_read::PURPOSE_EXT_KEY].as_str() {
match serde_json::from_value::<PolicyPurpose>(Value::String(raw.to_owned())) {
Ok(purpose) => query.purpose = Some(purpose),
Err(e) => {
return app_error_to_reject(
&doc,
&AppError::Validation(format!(
"ext.{} {raw:?} is not a known purpose: {e}",
policy_read::PURPOSE_EXT_KEY
)),
);
}
}
}
match policy_read::list_policies_inner(state, query).await {
Ok(response) => success_response(&doc, response),
Err(e) => app_error_to_reject(&doc, &e),
}
}
async fn handle_get(
state: &AppState,
ctx: &JoinAuthCtx,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
let (_, payload) = match admin_with::<policy_get::Payload>(state, ctx, &doc).await {
Ok(p) => p,
Err(reject) => return reject,
};
let id = match revision_id(&doc, payload.id.as_str()) {
Ok(id) => id,
Err(reject) => return reject,
};
match policy_read::show_policy_inner(state, id).await {
Ok(response) => success_response(&doc, response),
Err(AppError::NotFound(message)) => reject_with_code(
&doc,
extended_code(policy_get::error_codes::NOT_FOUND.code),
message,
None,
),
Err(e) => app_error_to_reject(&doc, &e),
}
}
async fn handle_active(
state: &AppState,
ctx: &JoinAuthCtx,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
if let Err(reject) = admin_with::<policy_active::Payload>(state, ctx, &doc).await {
return reject;
}
let query: policy_read::ActiveQuery = match parse_payload(&doc) {
Ok(q) => q,
Err(reject) => return reject,
};
match policy_read::active_policies(state, query).await {
Ok(response) => success_response(&doc, response),
Err(e) => app_error_to_reject(&doc, &e),
}
}
async fn handle_upsert(
state: &AppState,
ctx: &JoinAuthCtx,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
let (actor, _) = match admin_with::<policy_upsert::Payload>(state, ctx, &doc).await {
Ok(p) => p,
Err(reject) => return reject,
};
if let Err(e) =
crate::acl::require_any_capability(&state.acl_ks, &actor.did, Capability::PolicyAdmin).await
{
return app_error_to_reject(&doc, &e);
}
let body: policy_admin::UploadBody = match parse_payload(&doc) {
Ok(b) => b,
Err(reject) => return reject,
};
let purpose = match policy_admin::precheck_upload(&body) {
Ok(p) => p,
Err(e) => return app_error_to_reject(&doc, &e),
};
if let Err(e) = require_policy_admin(state, &actor.did, purpose).await {
return app_error_to_reject(&doc, &e);
}
if purpose.requires_consent() {
match policy_admin::check_upload(state, &body).await {
Ok(()) => {}
Err(AppError::Conflict(message)) => {
return reject_with_code(
&doc,
extended_code(policy_upsert::error_codes::VERSION_CONFLICT.code),
message,
None,
);
}
Err(e) => return app_error_to_reject(&doc, &e),
}
}
if let Err(refusal) = gate_authority_policy(state, &actor, &doc, purpose, "Replace").await {
return refusal;
}
match policy_admin::upload_inner(state, &actor.did, body).await {
Ok(response) => success_response(&doc, response),
Err(AppError::Conflict(message)) => reject_with_code(
&doc,
extended_code(policy_upsert::error_codes::VERSION_CONFLICT.code),
message,
None,
),
Err(e) => app_error_to_reject(&doc, &e),
}
}
async fn require_policy_admin(
state: &AppState,
did: &str,
purpose: PolicyPurpose,
) -> Result<(), AppError> {
crate::acl::require_capability(
&state.acl_ks,
did,
Capability::PolicyAdmin,
Some(&crate::acl::ResourceQualifier::Policy(purpose)),
)
.await
.map(|_| ())
}
async fn gate_authority_policy(
state: &AppState,
actor: &AuthClaims,
doc: &TrustTask<Value>,
purpose: PolicyPurpose,
verb: &str,
) -> Result<(), TrustTaskOutcome> {
use crate::acl::admin_consent::Act;
let (act, summary) = if purpose.decides_authority() {
(
Act::ChangeAuthorityPolicy(purpose),
format!(
"{verb} the {} policy, which decides authority in this community",
purpose.as_str()
),
)
} else if purpose.decides_recognition() {
(
Act::ChangeRecognitionPolicy(purpose),
format!(
"{verb} the {} policy, which decides who this community recognises as a person",
purpose.as_str()
),
)
} else {
return Ok(());
};
super::acl_tasks::settle_consent_gate(
state,
actor,
doc,
act,
&format!("policy:{}", purpose.as_str()),
&summary,
&summary,
)
.await
}
async fn handle_activate(
state: &AppState,
ctx: &JoinAuthCtx,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
let (actor, payload) = match admin_with::<policy_activate::Payload>(state, ctx, &doc).await {
Ok(p) => p,
Err(reject) => return reject,
};
if let Err(e) =
crate::acl::require_any_capability(&state.acl_ks, &actor.did, Capability::PolicyAdmin).await
{
return app_error_to_reject(&doc, &e);
}
if payload.context_id.is_some() {
return app_error_to_reject(
&doc,
&AppError::Validation(
"this maintainer does not implement contextId: a VTC is a single community and \
its policy bindings are community-wide"
.into(),
),
);
}
let id = match revision_id(&doc, payload.id.as_str()) {
Ok(id) => id,
Err(reject) => return reject,
};
let purpose =
match serde_json::from_value::<PolicyPurpose>(Value::String(payload.purpose.to_string())) {
Ok(p) => p,
Err(e) => {
return app_error_to_reject(
&doc,
&AppError::Validation(format!(
"purpose {:?} is not one this community decides: {e}",
payload.purpose.to_string()
)),
);
}
};
let refuse = |doc: &TrustTask<Value>, e: AppError| match e {
AppError::NotFound(message) => reject_with_code(
doc,
extended_code(policy_activate::error_codes::NOT_FOUND.code),
message,
None,
),
AppError::Conflict(message) => reject_with_code(
doc,
extended_code(policy_activate::error_codes::ALREADY_ACTIVE.code),
message,
None,
),
e => app_error_to_reject(doc, &e),
};
let decides = match policy_admin::precheck_activate(state, id, Some(purpose)).await {
Ok(p) => p,
Err(e) => return refuse(&doc, e),
};
if let Err(e) = require_policy_admin(state, &actor.did, decides).await {
return app_error_to_reject(&doc, &e);
}
if let Err(refusal) = gate_authority_policy(state, &actor, &doc, decides, "Activate").await {
return refusal;
}
match policy_admin::activate_inner(state, &actor.did, id, Some(purpose)).await {
Ok(response) => success_response(&doc, response),
Err(e) => refuse(&doc, e),
}
}
async fn handle_test(
state: &AppState,
ctx: &JoinAuthCtx,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
let (actor, payload) = match admin_with::<policy_test::Payload>(state, ctx, &doc).await {
Ok(p) => p,
Err(reject) => return reject,
};
if let Err(e) =
crate::acl::require_any_capability(&state.acl_ks, &actor.did, Capability::PolicyAdmin).await
{
return app_error_to_reject(&doc, &e);
}
let id = match revision_id(&doc, payload.id.as_str()) {
Ok(id) => id,
Err(reject) => return reject,
};
let body: policy_admin::TestBody = match parse_payload(&doc) {
Ok(b) => b,
Err(reject) => return reject,
};
match policy_admin::test(state, &actor.did, id, body).await {
Ok(response) => success_response(&doc, response),
Err(e) => task_error_to_reject(&doc, &e),
}
}
async fn handle_did_register(
state: &AppState,
ctx: &JoinAuthCtx,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
let actor = match super::capable_signer(state, ctx, &doc, Capability::DidAdmin, None).await {
Ok(a) => a,
Err(reject) => return reject,
};
let payload = match parse_spec_payload::<did_register::Payload>(&doc) {
Ok(p) => p,
Err(reject) => return reject,
};
match crate::routes::admin::did_register::register_inner(state, &actor.did, payload).await {
Ok(response) => success_response(&doc, response),
Err(e) => app_error_to_reject(&doc, &e),
}
}