use serde_json::Value;
use trust_tasks_rs::specs::auth::step_up::approver::enroll::v0_1 as enroll;
use trust_tasks_rs::specs::auth::step_up::approver::invite::v0_1 as invite;
use trust_tasks_rs::specs::auth::step_up::approver::list::v0_1 as list;
use trust_tasks_rs::specs::auth::step_up::approver::redeem::finish::v0_1 as redeem_finish;
use trust_tasks_rs::specs::auth::step_up::approver::redeem::start::v0_1 as redeem_start;
use trust_tasks_rs::specs::auth::step_up::approver::revoke::v0_1 as revoke;
use trust_tasks_rs::{RejectReason, StandardCode, TrustTask, TrustTaskCode};
use super::helpers::{
TrustTaskOutcome, app_error_to_reject, declared_code, reject_with, reject_with_code,
success_response,
};
use super::{JoinAuthCtx, admin_signer, parse_spec_payload};
use crate::acl::bound_step_up::{self, Gate};
use crate::server::AppState;
use crate::step_up_approver::ApproverTaskError;
pub(crate) const INVITE_TYPE: &str = <invite::Payload as trust_tasks_rs::Payload>::TYPE_URI;
pub(crate) const REDEEM_START_TYPE: &str =
<redeem_start::Payload as trust_tasks_rs::Payload>::TYPE_URI;
pub(crate) const REDEEM_FINISH_TYPE: &str =
<redeem_finish::Payload as trust_tasks_rs::Payload>::TYPE_URI;
pub(crate) const ENROLL_TYPE: &str = <enroll::Payload as trust_tasks_rs::Payload>::TYPE_URI;
pub(crate) const LIST_TYPE: &str = <list::Payload as trust_tasks_rs::Payload>::TYPE_URI;
pub(crate) const REVOKE_TYPE: &str = <revoke::Payload as trust_tasks_rs::Payload>::TYPE_URI;
pub(crate) const URIS: &[&str] = &[
INVITE_TYPE,
REDEEM_START_TYPE,
REDEEM_FINISH_TYPE,
ENROLL_TYPE,
LIST_TYPE,
REVOKE_TYPE,
];
pub(crate) const SECRET_RESPONSES: &[&str] = &[INVITE_TYPE, REDEEM_START_TYPE];
pub(super) async fn dispatch(
state: &AppState,
ctx: &JoinAuthCtx,
doc: TrustTask<Value>,
type_uri: &str,
) -> Option<TrustTaskOutcome> {
Some(match type_uri {
INVITE_TYPE => handle_invite(state, ctx, doc).await,
REDEEM_START_TYPE => handle_redeem_start(state, ctx, doc).await,
REDEEM_FINISH_TYPE => handle_redeem_finish(state, ctx, doc).await,
ENROLL_TYPE => handle_enroll(state, ctx, doc).await,
LIST_TYPE => handle_list(state, ctx, doc).await,
REVOKE_TYPE => handle_revoke(state, ctx, doc).await,
_ => return None,
})
}
fn refusal(doc: &TrustTask<Value>, e: &ApproverTaskError) -> TrustTaskOutcome {
match e {
ApproverTaskError::Refused {
code,
message,
details,
} => reject_with_code(doc, declared_code(code), message, details.clone()),
ApproverTaskError::StepUp(request) => reject_with_code(
doc,
TrustTaskCode::Standard(StandardCode::PermissionDenied),
"a step-up bound to this operation is required",
Some(bound_step_up::refusal_details(request)),
),
ApproverTaskError::App(e) => app_error_to_reject(doc, e),
}
}
fn signer_of(ctx: &JoinAuthCtx, doc: &TrustTask<Value>) -> Result<String, TrustTaskOutcome> {
ctx.verified_signer
.clone()
.ok_or_else(|| reject_with(doc, RejectReason::ProofRequired))
}
async fn gate(
state: &AppState,
actor: &str,
doc: &TrustTask<Value>,
reason: &str,
) -> Result<(), TrustTaskOutcome> {
match bound_step_up::redeem_or_request(
state,
actor,
&doc.type_uri.to_string(),
&doc.payload,
reason,
)
.await
{
Ok(Gate::Satisfied) => Ok(()),
Ok(Gate::Required(request)) => Err(refusal(doc, &ApproverTaskError::StepUp(request))),
Err(e) => Err(app_error_to_reject(doc, &e)),
}
}
async fn handle_invite(
state: &AppState,
ctx: &JoinAuthCtx,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
let actor = match admin_signer(state, ctx, &doc).await {
Ok(a) => a,
Err(reject) => return reject,
};
let payload: invite::Payload = match parse_spec_payload(&doc) {
Ok(p) => p,
Err(reject) => return reject,
};
if let Err(e) = crate::step_up_approver::check_invite(state, &actor.did, &payload).await {
return refusal(&doc, &e);
}
let reason = format!(
"Invite {} to enrol a step-up approver",
payload.subject.as_str()
);
if let Err(out) = gate(state, &actor.did, &doc, &reason).await {
return out;
}
match crate::step_up_approver::issue_invite(state, &actor.did, &payload).await {
Ok(response) => success_response(&doc, response),
Err(e) => refusal(&doc, &e),
}
}
async fn handle_redeem_start(
state: &AppState,
ctx: &JoinAuthCtx,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
let signer = match signer_of(ctx, &doc) {
Ok(s) => s,
Err(reject) => return reject,
};
let payload: redeem_start::Payload = match parse_spec_payload(&doc) {
Ok(p) => p,
Err(reject) => return reject,
};
match crate::step_up_approver::redeem_start(state, &signer, &payload).await {
Ok(response) => success_response(&doc, response),
Err(e) => refusal(&doc, &e),
}
}
async fn handle_redeem_finish(
state: &AppState,
ctx: &JoinAuthCtx,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
let signer = match signer_of(ctx, &doc) {
Ok(s) => s,
Err(reject) => return reject,
};
let payload: redeem_finish::Payload = match parse_spec_payload(&doc) {
Ok(p) => p,
Err(reject) => return reject,
};
let statement = doc.payload.get("statement").cloned().unwrap_or(Value::Null);
match crate::step_up_approver::redeem_finish(state, &signer, &payload, &statement).await {
Ok(response) => success_response(&doc, response),
Err(e) => refusal(&doc, &e),
}
}
async fn handle_enroll(
state: &AppState,
ctx: &JoinAuthCtx,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
let signer = match signer_of(ctx, &doc) {
Ok(s) => s,
Err(reject) => return reject,
};
let payload: enroll::Payload = match parse_spec_payload(&doc) {
Ok(p) => p,
Err(reject) => return reject,
};
match crate::step_up_approver::enroll(state, &signer, &doc.payload, &payload).await {
Ok(response) => success_response(&doc, response),
Err(e) => refusal(&doc, &e),
}
}
async fn identity_of(
state: &AppState,
ctx: &JoinAuthCtx,
doc: &TrustTask<Value>,
) -> Result<String, TrustTaskOutcome> {
let signer = signer_of(ctx, doc)?;
let has_own_row = crate::acl::get_acl_entry(&state.acl_ks, &signer)
.await
.map_err(|e| app_error_to_reject(doc, &e))?
.is_some();
if !has_own_row
&& let Some(delegation) =
crate::acl::console_key::resolve_delegated_admin(&state.console_keys_ks, &signer)
.await
.map_err(|e| app_error_to_reject(doc, &e))?
{
crate::acl::console_key::touch_last_used(&state.console_keys_ks, &delegation).await;
return Ok(delegation.admin_did);
}
Ok(signer)
}
async fn may_act_for(
state: &AppState,
ctx: &JoinAuthCtx,
doc: &TrustTask<Value>,
identity: &str,
subject: &str,
) -> Result<bool, TrustTaskOutcome> {
if identity == subject {
return Ok(true);
}
let Ok(actor) = admin_signer(state, ctx, doc).await else {
return Ok(false);
};
crate::step_up_approver::admin_covers(state, &actor, subject)
.await
.map_err(|e| app_error_to_reject(doc, &e))
}
async fn handle_list(
state: &AppState,
ctx: &JoinAuthCtx,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
let identity = match identity_of(state, ctx, &doc).await {
Ok(i) => i,
Err(reject) => return reject,
};
let payload: list::Payload = match parse_spec_payload(&doc) {
Ok(p) => p,
Err(reject) => return reject,
};
let subject = payload
.subject
.as_ref()
.map_or_else(|| identity.clone(), |s| s.to_string());
match may_act_for(state, ctx, &doc, &identity, &subject).await {
Ok(true) => {}
Ok(false) => {
return reject_with(
&doc,
RejectReason::PermissionDenied {
reason: "you may list only your own step-up approvers, or those of a member \
within your administrative authority"
.into(),
},
);
}
Err(reject) => return reject,
}
match crate::step_up_approver::list(state, &subject).await {
Ok(response) => success_response(&doc, response),
Err(e) => refusal(&doc, &e),
}
}
async fn handle_revoke(
state: &AppState,
ctx: &JoinAuthCtx,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
let identity = match identity_of(state, ctx, &doc).await {
Ok(i) => i,
Err(reject) => return reject,
};
let payload: revoke::Payload = match parse_spec_payload(&doc) {
Ok(p) => p,
Err(reject) => return reject,
};
let subject = payload
.subject
.as_ref()
.map_or_else(|| identity.clone(), |s| s.to_string());
let not_found = || {
refusal(
&doc,
&ApproverTaskError::Refused {
code: revoke::error_codes::NOT_FOUND.code,
message: "no binding of this approver that you may revoke".into(),
details: None,
},
)
};
match may_act_for(state, ctx, &doc, &identity, &subject).await {
Ok(true) => {}
Ok(false) => return not_found(),
Err(reject) => return reject,
}
match crate::step_up_approver::check_revoke(state, &subject, payload.approver_did.as_str())
.await
{
Ok(Some(_)) => {}
Ok(None) => {
let reason = if identity == subject {
format!(
"Revoke your step-up approver {}",
payload.approver_did.as_str()
)
} else {
format!(
"Revoke the step-up approver {} of {subject}",
payload.approver_did.as_str()
)
};
if let Err(out) = gate(state, &identity, &doc, &reason).await {
return out;
}
}
Err(e) => return refusal(&doc, &e),
}
match crate::step_up_approver::revoke(state, &identity, &subject, &payload).await {
Ok(response) => success_response(&doc, response),
Err(e) => refusal(&doc, &e),
}
}