use std::sync::Arc;
use affinidi_tdk::messaging::ATM;
use affinidi_tdk::messaging::profiles::ATMProfile;
use sha2::{Digest, Sha256};
use tracing::{debug, info, warn};
use trust_tasks_rs::specs::messaging::account;
pub async fn set_client_acl_on_connection(
atm: &ATM,
client_did: &str,
mediator_did: &str,
channel: &str,
client_name: &str,
) {
let atm = atm.clone();
let client_did = client_did.to_string();
let mediator_did = mediator_did.to_string();
let channel = channel.to_string();
let client_name = client_name.to_string();
tokio::spawn(async move {
if let Err(e) =
set_client_acl_internal(&atm, &client_did, &mediator_did, &channel, &client_name).await
{
warn!(
channel,
error = %e,
client = client_name,
"failed to set client ACL on mediator (startup continues)"
);
}
});
}
async fn set_client_acl_internal(
atm: &ATM,
client_did: &str,
mediator_did: &str,
channel: &str,
client_name: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let atm_profile = ATMProfile::new(
atm,
None,
client_did.to_string(),
Some(mediator_did.to_string()),
)
.await
.map_err(|e| format!("failed to create ATM profile: {e}"))?;
let client_did_hash = client_acl_hash(client_did);
let acl = build_allow_all_acl();
let atm_profile_arc = Arc::new(atm_profile);
match atm
.trust_tasks()
.account_update(
&atm_profile_arc,
Some(client_did_hash),
None,
Some(acl),
None,
)
.await
{
Ok(_) => {
info!(
channel,
client_did = %client_did,
client = client_name,
"client ACL configured on mediator"
);
}
Err(e) => {
debug!(
channel,
client_did = %client_did,
error = %e,
client = client_name,
"client ACL request error (mediator may still process asynchronously)"
);
}
}
Ok(())
}
pub async fn set_client_acl_with_profile(
atm: &ATM,
profile: &Arc<ATMProfile>,
client_did: &str,
channel: &str,
client_name: &str,
) {
let client_did_hash = client_acl_hash(client_did);
let acl = build_allow_all_acl();
const ACL_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
match tokio::time::timeout(
ACL_TIMEOUT,
atm.trust_tasks()
.account_update(profile, Some(client_did_hash), None, Some(acl), None),
)
.await
{
Ok(Ok(_)) => info!(
channel,
client_did = %client_did,
client = client_name,
"client ACL configured on mediator"
),
Ok(Err(e)) => debug!(
channel,
client_did = %client_did,
error = %e,
client = client_name,
"client ACL request error (mediator may still process asynchronously)"
),
Err(_) => debug!(
channel,
client_did = %client_did,
client = client_name,
"client ACL request timed out (mediator may still process asynchronously)"
),
}
}
#[cfg(feature = "tsp")]
pub async fn set_client_acl_over_tsp(
atm: &ATM,
profile: &Arc<ATMProfile>,
client_did: &str,
mediator_did: &str,
channel: &str,
client_name: &str,
) {
let doc = match build_account_update_document(client_did, mediator_did) {
Ok(doc) => doc,
Err(e) => {
debug!(
channel,
client_did = %client_did,
error = %e,
client = client_name,
"could not build the account/update document for TSP (ACL unchanged)"
);
return;
}
};
match atm.tsp().send(profile, mediator_did, &doc).await {
Ok(()) => debug!(
channel,
client_did = %client_did,
client = client_name,
"sent account/update to the mediator over TSP (delivery not confirmed)"
),
Err(e) => debug!(
channel,
client_did = %client_did,
error = %e,
client = client_name,
"could not send account/update over TSP (ACL unchanged)"
),
}
}
#[cfg(feature = "tsp")]
fn build_account_update_document(
client_did: &str,
mediator_did: &str,
) -> Result<Vec<u8>, Box<dyn std::error::Error + Send + Sync>> {
use trust_tasks_rs::TrustTask;
let payload: account::update::v0_1::Payload = account::update::v0_1::Payload::builder()
.did(client_acl_hash(client_did))
.acl(Some(build_allow_all_acl()))
.try_into()
.map_err(|e| format!("account/update payload: {e:?}"))?;
let mut doc = TrustTask::for_payload(format!("urn:uuid:{}", uuid::Uuid::new_v4()), payload);
doc.issuer = Some(client_did.to_string());
doc.recipient = Some(mediator_did.to_string());
Ok(serde_json::to_vec(&doc)?)
}
fn client_acl_hash(did: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(did);
hasher
.finalize()
.iter()
.map(|b| format!("{:02x}", b))
.collect()
}
fn build_allow_all_acl() -> account::update::v0_1::MediatorAcl {
account::update::v0_1::MediatorAcl::builder()
.blocked(Some(false))
.local(Some(true))
.send_messages(Some(true))
.receive_messages(Some(true))
.send_forwarded(Some(true))
.receive_forwarded(Some(true))
.create_invites(Some(true))
.anon_receive(Some(true))
.access_list_mode(Some(
account::update::v0_1::MediatorAclAccessListMode::ExplicitDeny,
))
.try_into()
.expect("MediatorAcl has no required member")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn client_acl_hash_matches_mediator_account_key_convention() {
assert_eq!(
client_acl_hash("did:key:z6MkovnNkdRq64BNcpZqpCnQGDhPe3g2cHeB35A5e7k4sNkS"),
"30a923cb69a99f8247469b72ea5b45b534e9f52a09200f92ce72f44e16714136"
);
}
#[test]
fn client_acl_hash_is_64_char_lowercase_hex() {
let h = client_acl_hash("did:webvh:QmExample:vta.example.com");
assert_eq!(h.len(), 64);
assert!(
h.chars()
.all(|c| c.is_ascii_hexdigit() && !c.is_ascii_uppercase())
);
}
#[test]
fn allow_all_acl_opens_everything_but_blocked() {
let v = serde_json::to_value(build_allow_all_acl()).expect("MediatorAcl serializes");
let obj = v.as_object().expect("acl serializes to a JSON object");
let mut saw_forwarded = false;
for (field, value) in obj {
let Some(b) = value.as_bool() else { continue };
if field.to_ascii_lowercase().contains("block") {
assert!(!b, "`{field}` must be false in an allow-all ACL");
} else {
assert!(b, "`{field}` must be true in an allow-all ACL");
}
if field.to_ascii_lowercase().contains("forwarded") {
saw_forwarded = true;
}
}
assert!(
saw_forwarded,
"allow-all ACL must set the forwarded-delivery flags"
);
}
}