use serde_json::Value;
use vti_common::error::AppError;
use crate::auth::AuthClaims;
use crate::operations::persona::{PersonaKey, PersonaStores, Relationship, persona_key_for};
use crate::operations::room_issuance::VtaKeySigner;
use crate::operations::room_oracle::{self, Principal};
use crate::server::AppState;
use trust_tasks_rs::Payload as _;
use trust_tasks_rs::specs::rooms as spec;
pub type ActRow = (&'static str, Option<&'static str>);
pub const ACT_TABLE: &[ActRow] = &[
(spec::records::put::v0_1::Payload::TYPE_URI, Some("write")),
(spec::records::put::v0_2::Payload::TYPE_URI, Some("write")),
(spec::records::get::v0_1::Payload::TYPE_URI, Some("read")),
(spec::records::get::v0_2::Payload::TYPE_URI, Some("read")),
(spec::records::list::v0_1::Payload::TYPE_URI, Some("read")),
(spec::records::list::v0_2::Payload::TYPE_URI, Some("read")),
(
spec::records::curate::v0_1::Payload::TYPE_URI,
Some("curate"),
),
(spec::info::v0_1::Payload::TYPE_URI, Some("read")),
(spec::usage::v0_1::Payload::TYPE_URI, Some("admin")),
(spec::epoch::chain::v0_1::Payload::TYPE_URI, Some("read")),
(spec::epoch::commits::v0_1::Payload::TYPE_URI, Some("read")),
(spec::epoch::mint::v0_1::Payload::TYPE_URI, Some("admin")),
(spec::blobs::get::v0_1::Payload::TYPE_URI, Some("read")),
(spec::blobs::get::v0_2::Payload::TYPE_URI, Some("read")),
(spec::blobs::chunk::v0_1::Payload::TYPE_URI, None),
(
spec::blobs::upload::begin::v0_2::Payload::TYPE_URI,
Some("write"),
),
(spec::blobs::upload::commit::v0_2::Payload::TYPE_URI, None),
(spec::blobs::upload::abort::v0_2::Payload::TYPE_URI, None),
];
pub const OFFER_TYPES: &[&str] = &[
<trust_tasks_rs::specs::vtc::rooms::offers::list::v0_1::Payload as trust_tasks_rs::Payload>::TYPE_URI,
<trust_tasks_rs::specs::vtc::rooms::offers::accept::v0_1::Payload as trust_tasks_rs::Payload>::TYPE_URI,
];
pub fn is_offer_type(type_uri: &str) -> bool {
OFFER_TYPES.contains(&bare_type(type_uri))
}
pub fn is_offer_accept(type_uri: &str) -> bool {
bare_type(type_uri) == OFFER_TYPES[1]
}
pub fn bare_type(type_uri: &str) -> &str {
type_uri.strip_suffix("#request").unwrap_or(type_uri)
}
pub fn actable(type_uri: &str) -> Option<Option<&'static str>> {
let bare = bare_type(type_uri);
ACT_TABLE
.iter()
.find(|(t, _)| *t == bare)
.map(|(_, action)| *action)
}
pub async fn authentication_key(
state: &AppState,
auth: &AuthClaims,
did: &str,
) -> Result<PersonaKey, AppError> {
persona_key_for(
PersonaStores::from_state(state),
auth,
did,
Relationship::Authentication,
)
.await?
.ok_or_else(|| {
AppError::Forbidden(format!(
"this agent holds no key that `{did}` lists for `authentication` and that you may \
use, so it cannot send a room request as that member"
))
})
}
pub async fn sign_as(
state: &AppState,
auth: &AuthClaims,
did: &str,
mut document: Value,
) -> Result<Value, AppError> {
let key = authentication_key(state, auth, did).await?;
let obj = document
.as_object_mut()
.ok_or_else(|| AppError::Validation("a room request is a JSON object".into()))?;
obj.remove("proof");
obj.insert("issuer".into(), Value::String(did.to_string()));
let signer = VtaKeySigner::new(
crate::operations::room_issuance::SigningContext {
keys_ks: &state.keys_ks,
imported_ks: &state.imported_ks,
internal_ks: &state.internal_ks,
contexts_ks: &state.contexts_ks,
acl_ks: &state.acl_ks,
seed_store: &state.seed_store,
audit: &state.audit_sink,
auth,
},
key.key_id,
key.verification_method,
);
let proof = affinidi_data_integrity::DataIntegrityProof::sign(
&document,
&signer,
affinidi_data_integrity::SignOptions::new()
.with_proof_purpose(Relationship::Authentication.as_str()),
)
.await
.map_err(|e| {
let mut cause = String::new();
let mut src: Option<&(dyn std::error::Error + 'static)> = std::error::Error::source(&e);
while let Some(inner) = src {
cause = format!("{cause}: {inner}");
src = inner.source();
}
AppError::Internal(format!("sign the room request as `{did}`: {e}{cause}"))
})?;
document["proof"] = serde_json::to_value(proof)
.map_err(|e| AppError::Internal(format!("serialise the proof: {e}")))?;
Ok(document)
}
pub fn check_schema(document: &Value) -> Result<(), AppError> {
let type_uri = document
.get("type")
.and_then(Value::as_str)
.ok_or_else(|| AppError::Validation("a room request names its `type`".into()))?;
let bare = bare_type(type_uri);
let schema = trust_tasks_rs::schema_index::schema_for(bare).ok_or_else(|| {
AppError::Validation(format!(
"`{bare}` has no schema to validate the request against"
))
})?;
let payload = document.get("payload").cloned().unwrap_or(Value::Null);
trust_tasks_rs::validate::against_schema(schema, &payload).map_err(|e| {
AppError::Validation(format!(
"the `{bare}` payload does not validate against its schema: {}",
e.messages().join("; ")
))
})
}
#[derive(Debug, Clone)]
pub struct Completed {
pub document: Value,
pub expires_at: Option<String>,
}
pub async fn complete_as(
state: &AppState,
auth: &AuthClaims,
principal: &Principal,
room_id: &str,
mut document: Value,
action: Option<&str>,
) -> Result<Completed, AppError> {
let mut expires_at = None;
if let Some(action) = action {
let minted =
room_oracle::present_as(state, auth, principal, &principal.did, room_id, action)
.await?;
let payload = document
.get_mut("payload")
.and_then(Value::as_object_mut)
.ok_or_else(|| AppError::Validation("a room request carries a payload".into()))?;
payload.insert("presentation".into(), minted.presentation);
expires_at = Some(minted.expires_at);
}
check_schema(&document)?;
let document = sign_as(state, auth, &principal.did, document).await?;
Ok(Completed {
document,
expires_at,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_actable_type_has_a_schema() {
for (t, _) in ACT_TABLE {
assert!(
trust_tasks_rs::schema_index::schema_for(t).is_some(),
"{t} has no schema"
);
}
}
#[test]
fn the_offer_types_are_apart_from_the_host_bound_table() {
for t in OFFER_TYPES {
assert!(is_offer_type(t), "{t}");
assert_eq!(actable(t), None, "{t} is not in the 0.1/0.2 table");
assert!(
trust_tasks_rs::schema_index::schema_for(t).is_some(),
"{t} has no schema"
);
}
assert!(is_offer_accept(&format!("{}#request", OFFER_TYPES[1])));
assert!(!is_offer_accept(OFFER_TYPES[0]));
assert!(!is_offer_type(
"https://trusttasks.org/spec/vtc/rooms/community/members/add/0.1"
));
}
#[test]
fn the_absent_types_are_not_actable() {
for t in [
spec::blobs::upload::chunk::v0_2::Payload::TYPE_URI,
spec::blobs::upload::chunk::v0_1::Payload::TYPE_URI,
spec::blobs::upload::begin::v0_1::Payload::TYPE_URI,
spec::blobs::upload::commit::v0_1::Payload::TYPE_URI,
spec::blobs::upload::abort::v0_1::Payload::TYPE_URI,
spec::epoch::prune::v0_1::Payload::TYPE_URI,
spec::owner::transfer::v0_1::Payload::TYPE_URI,
spec::owner::claim::v0_1::Payload::TYPE_URI,
spec::create::v0_1::Payload::TYPE_URI,
"https://trusttasks.org/spec/keys/sign/0.1",
] {
assert_eq!(actable(t), None, "{t}");
}
assert_eq!(
actable(&format!(
"{}#request",
spec::records::put::v0_2::Payload::TYPE_URI
)),
Some(Some("write"))
);
}
}