use serde::de::DeserializeOwned;
use serde::Serialize;
use crate::error::TrustTaskCode;
use crate::type_uri::TypeUri;
pub trait Payload: Serialize + DeserializeOwned {
const TYPE_URI: &'static str;
const IS_BEARER: bool = false;
const IS_PROOF_REQUIRED: bool = false;
const IS_RECIPIENT_REQUIRED: bool = false;
const IS_ISSUED_AT_REQUIRED: bool = false;
const PAYLOAD_SCHEMA: Option<&'static str> = None;
fn type_uri() -> TypeUri {
Self::TYPE_URI
.parse()
.expect("TYPE_URI constant must be a valid Type URI")
}
fn extended_code(local: impl Into<String>) -> TrustTaskCode {
let slug = Self::type_uri().slug().to_string();
let local = local.into();
TrustTaskCode::new_extended(&slug, &local).unwrap_or_else(|e| {
panic!(
"Payload::extended_code({:?}) on slug {:?} failed validation: {e}",
local, slug
)
})
}
fn family_code(namespace: &str, local: impl Into<String>) -> TrustTaskCode {
let slug = Self::type_uri().slug().to_string();
let local = local.into();
let permitted = slug
.match_indices('/')
.map(|(i, _)| &slug[..i])
.chain(std::iter::once(slug.as_str()));
if !permitted.into_iter().any(|p| p == namespace) {
panic!(
"Payload::family_code({namespace:?}, {local:?}) on slug {slug:?}: \
namespace is neither the slug nor a path prefix of it \
(SPEC §8.5 rule 2)"
);
}
TrustTaskCode::new_extended(namespace, &local).unwrap_or_else(|e| {
panic!(
"Payload::family_code({:?}, {:?}) failed validation: {e}",
namespace, local
)
})
}
}
pub trait RequestPayload: Payload {
type Response: Payload;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::specs::acl::change_role::v0_1 as change_role;
use crate::specs::acl::grant::v0_1 as grant;
use crate::specs::trust_task_discovery::v0_1 as discovery;
#[test]
fn extended_code_sources_slug_from_type_uri() {
let code = grant::Payload::extended_code("role_not_recognized");
match code {
TrustTaskCode::Extended { slug, local } => {
assert_eq!(slug, "acl/grant");
assert_eq!(local, "role_not_recognized");
}
other => panic!("expected Extended, got {other:?}"),
}
let code = change_role::Payload::extended_code("last_authority_protected");
assert_eq!(code.to_string(), "acl/change-role:last_authority_protected");
}
#[test]
fn extended_code_works_for_single_segment_slug() {
let code = discovery::Payload::extended_code("filter_unsupported");
assert_eq!(code.to_string(), "trust-task-discovery:filter_unsupported");
}
#[test]
#[should_panic(expected = "failed validation")]
fn extended_code_panics_on_invalid_local() {
let _ = grant::Payload::extended_code("BadLocal");
}
#[test]
fn family_code_accepts_each_path_prefix_of_the_slug() {
let code = change_role::Payload::family_code("acl", "permissionDenied");
assert_eq!(code.to_string(), "acl:permissionDenied");
let code = change_role::Payload::family_code("acl/change-role", "lastAuthorityProtected");
assert_eq!(code.to_string(), "acl/change-role:lastAuthorityProtected");
}
#[test]
#[should_panic(expected = "neither the slug nor a path prefix")]
fn family_code_rejects_a_sibling_slug() {
let _ = grant::Payload::family_code("acl/revoke", "borrowedCode");
}
#[test]
#[should_panic(expected = "neither the slug nor a path prefix")]
fn family_code_rejects_an_unrelated_namespace() {
let _ = grant::Payload::family_code("vault", "somethingElse");
}
#[test]
#[should_panic(expected = "neither the slug nor a path prefix")]
fn family_code_rejects_a_partial_segment() {
let _ = grant::Payload::family_code("ac", "somethingElse");
}
#[test]
fn family_code_strips_response_fragment_before_checking() {
let code = grant::Response::family_code("acl", "permissionDenied");
assert_eq!(code.to_string(), "acl:permissionDenied");
}
#[test]
fn extended_code_strips_response_fragment_from_slug() {
let code = grant::Response::extended_code("role_not_recognized");
match code {
TrustTaskCode::Extended { slug, .. } => {
assert_eq!(slug, "acl/grant", "response variant must yield bare slug");
}
other => panic!("expected Extended, got {other:?}"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SpecPolicy {
pub is_bearer: bool,
pub is_proof_required: bool,
pub is_recipient_required: bool,
pub is_issued_at_required: bool,
}
impl SpecPolicy {
pub const fn of<P: Payload>() -> Self {
Self {
is_bearer: P::IS_BEARER,
is_proof_required: P::IS_PROOF_REQUIRED,
is_recipient_required: P::IS_RECIPIENT_REQUIRED,
is_issued_at_required: P::IS_ISSUED_AT_REQUIRED,
}
}
pub fn enforce<P>(&self, doc: &crate::TrustTask<P>) -> Result<(), crate::RejectReason> {
if doc.recipient.is_none() && self.is_recipient_required {
return Err(crate::RejectReason::MalformedRequest {
reason: "specification declares recipient REQUIRED but the document \
carries no in-band recipient"
.to_string(),
});
}
if doc.proof.is_none() && self.is_proof_required {
return Err(crate::RejectReason::ProofRequired);
}
if doc.issued_at.is_none() && self.is_issued_at_required {
return Err(crate::RejectReason::MalformedRequest {
reason: crate::freshness::ISSUED_AT_REQUIRED_BY_SPEC.to_string(),
});
}
if doc.proof.is_some() && doc.recipient.is_none() && !self.is_bearer {
return Err(crate::RejectReason::MalformedRequest {
reason: "proof present with no in-band recipient on a non-bearer specification \
(SPEC §4.8.2 audience binding)"
.to_string(),
});
}
Ok(())
}
}