use tokio::sync::mpsc::UnboundedSender;
use super::diagnostics::{DiagCheck, DiagStatus};
use super::event::VtaEvent;
use crate::client::VtaClient;
use crate::error::VtaError;
pub(super) async fn verify_authorization(
client: &VtaClient,
setup_did: &str,
vta_did: &str,
required_task: Option<&str>,
tx: &UnboundedSender<VtaEvent>,
) -> Result<(), String> {
let _ = tx.send(VtaEvent::CheckStart(DiagCheck::VerifyAuthorization));
let probe = client.clone().trusting_unsigned_replies();
let served = match probe.supported_trust_tasks(&["*"]).await {
Ok(resp) => resp.supported_types,
Err(VtaError::Forbidden(detail)) => {
let msg = format!(
"{setup_did} is not authorized on {vta_did}. Run \
`pnm acl create --did {setup_did} --role admin` against that VTA — an \
ACL grant is per-VTA and one made on a different VTA does not carry — \
and confirm {vta_did} is the VTA you meant. ({detail})"
);
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::VerifyAuthorization,
DiagStatus::Failed(msg.clone()),
));
return Err(msg);
}
Err(VtaError::UnsupportedTaskType { .. }) => {
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::VerifyAuthorization,
DiagStatus::Skipped(
"this VTA does not serve trust-task-discovery/0.1, so its task list \
could not be checked in advance"
.into(),
),
));
return Ok(());
}
Err(e) => {
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::VerifyAuthorization,
DiagStatus::Skipped(format!("could not ask {vta_did} what it serves: {e}")),
));
return Ok(());
}
};
if let Some(required) = required_task
&& !served.iter().any(|uri| uri == required)
{
let msg = version_skew_message(required, &served, vta_did);
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::VerifyAuthorization,
DiagStatus::Failed(msg.clone()),
));
return Err(msg);
}
let _ = tx.send(VtaEvent::CheckDone(
DiagCheck::VerifyAuthorization,
DiagStatus::Ok(format!(
"{setup_did} is authorized on {vta_did}; it serves {} task types",
served.len()
)),
));
Ok(())
}
fn task_family(type_uri: &str) -> Option<&str> {
type_uri.rsplit_once('/').map(|(family, _version)| family)
}
fn version_skew_message(required: &str, served: &[String], vta_did: &str) -> String {
let family = task_family(required);
let mut siblings: Vec<&str> = served
.iter()
.map(String::as_str)
.filter(|uri| family.is_some() && task_family(uri) == family)
.collect();
siblings.sort_unstable();
if siblings.is_empty() {
return format!(
"{vta_did} does not serve {required}, or anything else in that family. \
Either it is built without the feature that provides it, or it is not the \
VTA you meant to provision against."
);
}
format!(
"{vta_did} serves {} but this client dispatches {required}. That is a version \
skew, not a broken VTA: upgrade the VTA to a release that serves {required}, \
or point this client at one that does.",
siblings.join(", "),
)
}
#[cfg(test)]
mod tests {
use super::*;
const V0_3: &str = "https://trusttasks.org/spec/provision/integration/0.3";
const V0_2: &str = "https://trusttasks.org/spec/provision/integration/0.2";
#[test]
fn a_sibling_version_is_named_as_a_skew() {
let msg = version_skew_message(V0_3, &[V0_2.to_string()], "did:webvh:example");
assert!(
msg.contains(V0_2),
"must name what the VTA does serve: {msg}"
);
assert!(msg.contains(V0_3), "must name what we asked for: {msg}");
assert!(msg.contains("version skew"), "{msg}");
}
#[test]
fn an_absent_family_is_not_reported_as_a_skew() {
let served = vec!["https://trusttasks.org/spec/vault/list/0.1".to_string()];
let msg = version_skew_message(V0_3, &served, "did:webvh:example");
assert!(!msg.contains("version skew"), "{msg}");
assert!(msg.contains("not the VTA you meant"), "{msg}");
}
#[test]
fn only_same_family_versions_are_offered() {
const WANT: &str = "https://tasks.example/spec/provision/integration/0.3";
const HAVE: &str = "https://tasks.example/spec/provision/integration/0.2";
const COUSIN: &str = "https://tasks.example/spec/provision/other/0.9";
let served = vec![COUSIN.to_string(), HAVE.to_string()];
let msg = version_skew_message(WANT, &served, "did:webvh:example");
assert!(msg.contains(HAVE), "{msg}");
assert!(
!msg.contains(COUSIN),
"a neighbouring family is not a version of this one: {msg}"
);
}
#[test]
fn task_family_strips_only_the_version_segment() {
assert_eq!(
task_family(V0_3),
Some("https://trusttasks.org/spec/provision/integration")
);
assert_eq!(task_family("no-slashes"), None);
}
}