use serde_json::Value;
use trust_tasks_rs::specs::git_ns::account::{
link::v0_1 as link, link_status::v0_1 as link_status, unlink::v0_1 as unlink,
};
use trust_tasks_rs::specs::git_ns::bridge::{
event::v0_1 as event, event::v0_2 as event2, event::v0_3 as event3, event::v0_4 as event4,
result::v0_1 as result,
};
use trust_tasks_rs::specs::git_ns::drift::resolve::{
v0_1 as drift_resolve, v0_3 as drift_resolve3,
};
use trust_tasks_rs::specs::git_ns::namespace::{
bind::v0_1 as bind, reseat::v0_3 as reseat, unbind::v0_1 as unbind,
};
use trust_tasks_rs::specs::git_ns::repo::{
adopt::v0_1 as adopt, archive::v0_1 as archive, create::v0_3 as create,
transfer::v0_1 as transfer,
};
use trust_tasks_rs::specs::git_ns::right::{
break_glass::v0_1 as break_glass, grant::v0_3 as grant, ratify::v0_1 as ratify,
revoke::v0_3 as revoke,
};
use trust_tasks_rs::specs::git_ns::roles::reproject::v0_1 as reproject;
use trust_tasks_rs::specs::git_ns::view::{v0_1 as view, v0_2 as view2, v0_4 as view4};
use trust_tasks_rs::{AsyncDispatcher, RejectReason, StandardCode, TrustTask, TrustTaskCode};
use super::admin_reads::{
account_list_v0_1, activity_list_v0_1, bridge_job_list_v0_1, bridge_job_list_v0_2,
projection_show_v0_1, right_issued_by_departed_v0_1, right_list_v0_1,
};
use crate::server::AppState;
use crate::trust_tasks::helpers::{
TrustTaskOutcome, app_error_to_reject, extended_code, reject_with, reject_with_code,
success_response,
};
use super::ops::{self, OpError};
use super::store::Snapshot;
static GIT_NS: std::sync::LazyLock<AsyncDispatcher<GitNsCtx, TrustTaskOutcome>> =
std::sync::LazyLock::new(dispatcher);
#[must_use]
pub(crate) fn serves(type_uri: &str) -> bool {
GIT_NS.registered_uris().contains(&type_uri)
}
#[must_use]
pub fn served_uris() -> Vec<&'static str> {
GIT_NS.registered_uris()
}
#[derive(Clone)]
pub(crate) struct GitNsCtx {
pub state: AppState,
pub signer: Option<String>,
pub sender: Option<String>,
}
pub(crate) async fn dispatch(
state: &AppState,
doc: TrustTask<Value>,
signer: Option<&str>,
sender: Option<&str>,
) -> TrustTaskOutcome {
let ctx = GitNsCtx {
state: state.clone(),
signer: signer.map(str::to_string),
sender: sender.map(str::to_string),
};
match GIT_NS
.dispatch_or_reject(doc, ctx, format!("urn:uuid:{}", uuid::Uuid::new_v4()))
.await
{
Ok(outcome) => outcome,
Err(err_doc) => crate::trust_tasks::helpers::error_response(err_doc),
}
}
pub(crate) fn dispatcher() -> AsyncDispatcher<GitNsCtx, TrustTaskOutcome> {
AsyncDispatcher::new()
.on_async(handle_bind)
.on_async(handle_unbind)
.on_async(handle_create)
.on_async(handle_adopt)
.on_async(handle_transfer)
.on_async(handle_archive)
.on_async(handle_grant)
.on_async(handle_revoke)
.on_async(handle_break_glass)
.on_async(handle_ratify)
.on_async(handle_view)
.on_async(handle_view_v2)
.on_async(handle_view_v4)
.on_async(handle_view_v5)
.on_async(handle_namespace_list)
.on_async(handle_repo_list)
.on_async(handle_drift_resolve)
.on_async(handle_drift_resolve_v3)
.on_async(handle_reseat)
.on_async(handle_reproject)
.on_async(handle_link)
.on_async(handle_link_status)
.on_async(handle_unlink)
.on_async(handle_result)
.on_async(handle_event)
.on_async(handle_event_v2)
.on_async(handle_event_v3)
.on_async(handle_event_v4)
.on_async(handle_right_list)
.on_async(handle_right_issued_by_departed)
.on_async(handle_bridge_job_list)
.on_async(handle_bridge_job_list_v2)
.on_async(handle_projection_show)
.on_async(handle_account_list)
.on_async(handle_activity_list)
}
fn respond<P, R: serde::Serialize>(doc: &TrustTask<P>, r: Result<R, OpError>) -> TrustTaskOutcome {
match r {
Ok(body) => success_response(doc, body),
Err(OpError::Declared { code, message }) => {
reject_with_code(doc, extended_code(code), message, None)
}
Err(OpError::PermissionDenied(reason)) => {
reject_with(doc, RejectReason::PermissionDenied { reason })
}
Err(OpError::Malformed(reason)) => {
reject_with(doc, RejectReason::MalformedRequest { reason })
}
Err(OpError::StepUpRequired { message, request }) => reject_with_code(
doc,
TrustTaskCode::Standard(StandardCode::PermissionDenied),
message,
Some(crate::acl::bound_step_up::refusal_details(&request)),
),
Err(OpError::Unavailable(message)) => reject_with_code(
doc,
TrustTaskCode::Standard(StandardCode::Unavailable),
message,
None,
),
Err(OpError::UnsupportedVersion(message)) => reject_with_code(
doc,
TrustTaskCode::Standard(StandardCode::UnsupportedVersion),
message,
None,
),
Err(OpError::Internal(e)) => app_error_to_reject(doc, &e),
}
}
fn signer<P>(doc: &TrustTask<P>, ctx: &GitNsCtx) -> Result<String, TrustTaskOutcome> {
ctx.signer
.clone()
.ok_or_else(|| reject_with(doc, RejectReason::ProofRequired))
}
fn caller<P>(doc: &TrustTask<P>, ctx: &GitNsCtx) -> Result<String, TrustTaskOutcome> {
ctx.signer
.clone()
.or_else(|| ctx.sender.clone())
.ok_or_else(|| {
reject_with(
doc,
RejectReason::PermissionDenied {
reason:
"nobody identified: sign the document, or send it over a transport that \
authenticates you"
.into(),
},
)
})
}
async fn acting_as(state: &AppState, signer: &str) -> Result<String, OpError> {
let actor = acting_as_unchecked(state, signer).await?;
if let Some(s) = crate::acl::storage::get_suspension(&state.acl_ks, &actor).await? {
return Err(OpError::PermissionDenied(s.message(&actor)));
}
Ok(actor)
}
async fn acting_as_unchecked(state: &AppState, signer: &str) -> Result<String, OpError> {
if crate::acl::get_acl_entry(&state.acl_ks, signer)
.await?
.is_some()
{
return Ok(signer.to_string());
}
match crate::acl::console_key::resolve_delegated_admin(&state.console_keys_ks, signer).await? {
Some(delegation) => {
crate::acl::console_key::touch_last_used(&state.console_keys_ks, &delegation).await;
tracing::info!(
console_did = %signer,
admin_did = %delegation.admin_did,
"authorizing a git-ns document under a console-key delegation"
);
Ok(delegation.admin_did)
}
None => Ok(signer.to_string()),
}
}
macro_rules! signed_handler {
($name:ident, $payload:ty, $op:path) => {
pub(crate) async fn $name(doc: TrustTask<$payload>, ctx: GitNsCtx) -> TrustTaskOutcome {
let signer = match signer(&doc, &ctx) {
Ok(a) => a,
Err(r) => return r,
};
let actor = match acting_as(&ctx.state, &signer).await {
Ok(a) => a,
Err(e) => return respond::<_, ()>(&doc, Err(e)),
};
let r = $op(&ctx.state, &actor, doc.payload.clone()).await;
respond(&doc, r)
}
};
}
macro_rules! bridge_handler {
($name:ident, $payload:ty, $op:path) => {
pub(crate) async fn $name(doc: TrustTask<$payload>, ctx: GitNsCtx) -> TrustTaskOutcome {
let actor = match signer(&doc, &ctx) {
Ok(a) => a,
Err(r) => return r,
};
let r = $op(&ctx.state, &actor, doc.payload.clone()).await;
respond(&doc, r)
}
};
}
signed_handler!(handle_bind, bind::Payload, ops::bind);
signed_handler!(handle_unbind, unbind::Payload, ops::unbind);
signed_handler!(handle_create, create::Payload, ops::repo_create);
signed_handler!(handle_adopt, adopt::Payload, ops::repo_adopt);
signed_handler!(handle_transfer, transfer::Payload, ops::repo_transfer);
signed_handler!(handle_archive, archive::Payload, ops::repo_archive);
signed_handler!(handle_grant, grant::Payload, ops::right_grant);
signed_handler!(handle_revoke, revoke::Payload, ops::right_revoke);
signed_handler!(
handle_break_glass,
break_glass::Payload,
super::break_glass::right_break_glass
);
signed_handler!(
handle_ratify,
ratify::Payload,
super::break_glass::right_ratify
);
signed_handler!(handle_link, link::Payload, ops::account_link);
signed_handler!(handle_unlink, unlink::Payload, ops::account_unlink);
bridge_handler!(handle_result, result::Payload, super::bridge::handle_result);
signed_handler!(
handle_drift_resolve,
drift_resolve::Payload,
super::drift::drift_resolve_v1
);
signed_handler!(
handle_drift_resolve_v3,
drift_resolve3::Payload,
super::drift::drift_resolve
);
signed_handler!(handle_reseat, reseat::Payload, ops::namespace_reseat);
signed_handler!(
handle_view_v5,
super::admin_reads::view_v0_5::Payload,
super::admin_reads::view_v5
);
signed_handler!(
handle_namespace_list,
super::admin_reads::namespace_list_v0_1::Payload,
super::admin_reads::namespace_list
);
signed_handler!(
handle_repo_list,
super::admin_reads::repo_list_v0_1::Payload,
super::admin_reads::repo_list
);
signed_handler!(
handle_right_list,
right_list_v0_1::Payload,
super::admin_reads::right_list
);
signed_handler!(
handle_right_issued_by_departed,
right_issued_by_departed_v0_1::Payload,
super::admin_reads::right_issued_by_departed
);
signed_handler!(
handle_bridge_job_list,
bridge_job_list_v0_1::Payload,
super::admin_reads::bridge_job_list
);
signed_handler!(
handle_bridge_job_list_v2,
bridge_job_list_v0_2::Payload,
super::admin_reads::bridge_job_list_v2
);
signed_handler!(
handle_projection_show,
projection_show_v0_1::Payload,
super::admin_reads::projection_show
);
signed_handler!(
handle_account_list,
account_list_v0_1::Payload,
super::admin_reads::account_list
);
signed_handler!(
handle_activity_list,
activity_list_v0_1::Payload,
super::admin_reads::activity_list
);
signed_handler!(
handle_reproject,
reproject::Payload,
super::reproject::roles_reproject
);
async fn event_as_v4<P, R>(
state: &crate::server::AppState,
issuer: &str,
issued_at: chrono::DateTime<chrono::Utc>,
p: P,
) -> Result<R, OpError>
where
P: serde::Serialize,
R: serde::de::DeserializeOwned,
{
let v4: event4::Payload = serde_json::from_value(
serde_json::to_value(&p).map_err(vti_common::error::AppError::from)?,
)
.map_err(|e| OpError::Malformed(format!("bridge/event payload: {e}")))?;
let ack = super::bridge::handle_event(state, issuer, issued_at, v4).await?;
Ok(serde_json::from_value(
serde_json::to_value(&ack).map_err(vti_common::error::AppError::from)?,
)
.map_err(vti_common::error::AppError::from)?)
}
async fn event_v1(
state: &crate::server::AppState,
issuer: &str,
issued_at: chrono::DateTime<chrono::Utc>,
p: event::Payload,
) -> Result<event::Response, OpError> {
event_as_v4(state, issuer, issued_at, p).await
}
async fn event_v2(
state: &crate::server::AppState,
issuer: &str,
issued_at: chrono::DateTime<chrono::Utc>,
p: event2::Payload,
) -> Result<event2::Response, OpError> {
event_as_v4(state, issuer, issued_at, p).await
}
async fn event_v3(
state: &crate::server::AppState,
issuer: &str,
issued_at: chrono::DateTime<chrono::Utc>,
p: event3::Payload,
) -> Result<event3::Response, OpError> {
event_as_v4(state, issuer, issued_at, p).await
}
macro_rules! event_handler {
($name:ident, $payload:ty, $op:path) => {
pub(crate) async fn $name(doc: TrustTask<$payload>, ctx: GitNsCtx) -> TrustTaskOutcome {
let actor = match signer(&doc, &ctx) {
Ok(a) => a,
Err(r) => return r,
};
let r = match doc.issued_at {
Some(at) => $op(&ctx.state, &actor, at, doc.payload.clone()).await,
None => Err(OpError::Malformed(
"a bridge event carries `issuedAt`".into(),
)),
};
respond(&doc, r)
}
};
}
event_handler!(handle_event, event::Payload, event_v1);
event_handler!(handle_event_v2, event2::Payload, event_v2);
event_handler!(handle_event_v3, event3::Payload, event_v3);
event_handler!(
handle_event_v4,
event4::Payload,
super::bridge::handle_event
);
pub(crate) async fn handle_view(doc: TrustTask<view::Payload>, ctx: GitNsCtx) -> TrustTaskOutcome {
let who = match signer(&doc, &ctx) {
Ok(w) => w,
Err(r) => return r,
};
let r = async {
let who = acting_as(&ctx.state, &who).await?;
let standing = ops::standing(&ctx.state, &who).await?;
if !standing.member {
return Err(OpError::PermissionDenied(
"git-ns/view answers members of this community".into(),
));
}
let filter = match &doc.payload.resource {
Some(r) => Some(super::model::Resource::parse(r).map_err(OpError::Malformed)?),
None => None,
};
let snap = Snapshot::load(&ctx.state.git_ns).await?;
Ok(super::view::for_member(&snap, &who, filter.as_ref())?)
}
.await;
respond(&doc, r)
}
pub(crate) async fn handle_view_v2(
doc: TrustTask<view2::Payload>,
ctx: GitNsCtx,
) -> TrustTaskOutcome {
let who = match signer(&doc, &ctx) {
Ok(w) => w,
Err(r) => return r,
};
let r = async {
let who = acting_as(&ctx.state, &who).await?;
let standing = ops::standing(&ctx.state, &who).await?;
if !standing.member {
return Err(OpError::PermissionDenied(
"git-ns/view answers members of this community".into(),
));
}
let filter = match &doc.payload.resource {
Some(r) => Some(super::model::Resource::parse(r).map_err(OpError::Malformed)?),
None => None,
};
let snap = Snapshot::load(&ctx.state.git_ns).await?;
let member = crate::members::get_member(&ctx.state.members_ks, &who).await?;
Ok(super::view::for_member_v2(
&snap,
&who,
filter.as_ref(),
member.as_ref(),
)?)
}
.await;
respond(&doc, r)
}
pub(crate) async fn handle_view_v4(
doc: TrustTask<view4::Payload>,
ctx: GitNsCtx,
) -> TrustTaskOutcome {
let who = match signer(&doc, &ctx) {
Ok(w) => w,
Err(r) => return r,
};
let r = async {
let who = acting_as(&ctx.state, &who).await?;
let standing = ops::standing(&ctx.state, &who).await?;
if !standing.member {
return Err(OpError::PermissionDenied(
"git-ns/view answers members of this community".into(),
));
}
let filter = match &doc.payload.resource {
Some(r) => Some(super::model::Resource::parse(r).map_err(OpError::Malformed)?),
None => None,
};
let snap = Snapshot::load(&ctx.state.git_ns).await?;
let member = crate::members::get_member(&ctx.state.members_ks, &who).await?;
Ok(super::view::for_member_v4(
&snap,
&who,
standing.community_admin,
filter.as_ref(),
member.as_ref(),
)?)
}
.await;
respond(&doc, r)
}
pub(crate) async fn handle_link_status(
doc: TrustTask<link_status::Payload>,
ctx: GitNsCtx,
) -> TrustTaskOutcome {
let who = match caller(&doc, &ctx) {
Ok(w) => w,
Err(r) => return r,
};
let r = match acting_as(&ctx.state, &who).await {
Ok(who) => ops::account_link_status(&ctx.state, &who, doc.payload.clone()).await,
Err(e) => Err(e),
};
respond(&doc, r)
}