use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::{
CollaborationActor, ContentHash,
thread_replication::{
SourceAuthor, ThreadOperation, ThreadOperationBody, integration::TrustedHostedExecutor,
metadata::ThreadControl,
},
};
use crate::error::{HeddleError, Result};
pub const FORMAT: &str = "heddle-thread-authority-admission-v3";
pub const MAX_BYTES: usize = 2048;
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum OriginalAuthoritySubject {
Operation(ContentHash),
OwnershipClaim(ContentHash),
OwnershipResolution(ContentHash),
}
impl OriginalAuthoritySubject {
pub fn id(&self) -> ContentHash {
match self {
Self::Operation(id) | Self::OwnershipClaim(id) | Self::OwnershipResolution(id) => *id,
}
}
pub fn operation_id(&self) -> Option<ContentHash> {
match self {
Self::Operation(id) => Some(*id),
Self::OwnershipClaim(_) | Self::OwnershipResolution(_) => None,
}
}
pub fn claim_id(&self) -> Option<ContentHash> {
match self {
Self::OwnershipClaim(id) => Some(*id),
Self::Operation(_) | Self::OwnershipResolution(_) => None,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct OriginalAuthorityBinding {
pub spool: Uuid,
pub actor: CollaborationActor,
pub authority_digest: ContentHash,
}
impl OriginalAuthorityBinding {
pub fn from_operation(operation: &ThreadOperation) -> Result<Option<Self>> {
if let ThreadOperationBody::Metadata(bytes) = &operation.body {
let control = ThreadControl::decode(bytes)?;
return Ok(Some(Self {
spool: control.spool,
actor: control.actor,
authority_digest: control.authority_digest,
}));
}
match operation.source_author()? {
Some(SourceAuthor::Account {
spool,
actor,
authority_digest,
authority,
}) => {
SourceAuthor::Account {
spool,
actor: actor.clone(),
authority_digest,
authority,
}
.validate()?;
Ok(Some(Self {
spool,
actor,
authority_digest,
}))
}
Some(SourceAuthor::LocalKey) | None => Ok(None),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ThreadAuthorityAdmission {
pub version: u16,
pub basis: super::original_boundary_acceptance::AdmissionBasis,
pub spool: Uuid,
pub spool_genesis: ContentHash,
pub thread: ContentHash,
pub subject: OriginalAuthoritySubject,
pub actor: CollaborationActor,
pub publisher: [u8; 32],
pub authority_digest: ContentHash,
pub executor: [u8; 32],
pub admitted_at_ms: i64,
}
impl ThreadAuthorityAdmission {
pub fn encode(&self) -> Result<Vec<u8>> {
if self.version != 3
|| self.spool.is_nil()
|| self.actor.principal_id.is_nil()
|| self.publisher == [0; 32]
|| self.executor == [0; 32]
|| self.admitted_at_ms < 0
|| self.actor.agent_id.as_ref().is_some_and(|id| {
id.is_empty() || id.len() > 256 || id.chars().any(char::is_control)
})
{
return Err(invalid("invalid original-author admission statement"));
}
let bytes = rmp_serde::to_vec_named(self)?;
if bytes.len() > MAX_BYTES {
return Err(invalid("authority admission statement exceeds byte bound"));
}
Ok(bytes)
}
pub fn decode(bytes: &[u8]) -> Result<Self> {
if bytes.is_empty() || bytes.len() > MAX_BYTES {
return Err(invalid("authority admission statement exceeds byte bound"));
}
let value: Self = rmp_serde::from_slice(bytes)?;
if value.encode()? != bytes {
return Err(invalid("noncanonical authority admission statement"));
}
Ok(value)
}
pub fn authorize(
&self,
operation: &ThreadOperation,
trust: &TrustedHostedExecutor,
) -> Result<()> {
self.authorize_with_acceptance(operation, trust, None)
}
pub fn authorize_with_acceptance(
&self,
operation: &ThreadOperation,
trust: &TrustedHostedExecutor,
evidence: Option<&super::original_boundary_acceptance::OriginalBoundaryAcceptance>,
) -> Result<()> {
self.basis.authorize_evidence(
evidence,
self.spool,
self.actor.principal_id,
operation
.source_author()?
.map(|_| super::original_boundary_acceptance::BoundaryOriginalKind::Source),
)?;
self.encode()?;
if self.spool != trust.spool
|| self.spool_genesis != trust.spool_genesis
|| self.executor != trust.executor
{
return Err(invalid(
"authority admission differs from independently pinned executor",
));
}
let binding = OriginalAuthorityBinding::from_operation(operation)?
.ok_or_else(|| invalid("account admission requires original authored account work"))?;
if self.subject != OriginalAuthoritySubject::Operation(operation.id()?)
|| self.thread != operation.thread
|| self.publisher != operation.publisher
|| self.actor != binding.actor
|| self.spool != binding.spool
|| self.authority_digest != binding.authority_digest
{
return Err(invalid(
"authority admission differs from original operation",
));
}
Ok(())
}
pub fn authorize_claim(
&self,
claim: &super::thread_replication::ownership_claim::ThreadOwnershipClaim,
genesis: &super::thread_replication::ThreadGenesis,
trust: &TrustedHostedExecutor,
) -> Result<()> {
self.authorize_claim_with_acceptance(claim, genesis, trust, None)
}
pub fn authorize_claim_with_acceptance(
&self,
claim: &super::thread_replication::ownership_claim::ThreadOwnershipClaim,
genesis: &super::thread_replication::ThreadGenesis,
trust: &TrustedHostedExecutor,
evidence: Option<&super::original_boundary_acceptance::OriginalBoundaryAcceptance>,
) -> Result<()> {
self.basis.authorize_evidence(
evidence,
self.spool,
self.actor.principal_id,
Some(super::original_boundary_acceptance::BoundaryOriginalKind::OwnershipClaim),
)?;
self.encode()?;
claim.validate_genesis(genesis)?;
let SourceAuthor::Account {
spool,
actor,
authority_digest,
..
} = &claim.acceptance
else {
return Err(invalid(
"claim admission requires signed account acceptance",
));
};
if self.spool != trust.spool
|| self.spool_genesis != trust.spool_genesis
|| self.executor != trust.executor
{
return Err(invalid(
"authority admission differs from independently pinned executor",
));
}
if self.subject != OriginalAuthoritySubject::OwnershipClaim(claim.id()?)
|| self.thread != claim.thread
|| self.publisher != claim.accepting_publisher
|| self.actor != *actor
|| self.spool != *spool
|| self.authority_digest != *authority_digest
{
return Err(invalid(
"authority admission differs from original ownership claim",
));
}
Ok(())
}
pub fn authorize_resolution_with_acceptance(
&self,
resolution: &super::thread_replication::ownership_resolution::ThreadOwnershipResolution,
genesis: &super::thread_replication::ThreadGenesis,
trust: &TrustedHostedExecutor,
evidence: Option<&super::original_boundary_acceptance::OriginalBoundaryAcceptance>,
) -> Result<()> {
self.basis.authorize_evidence(
evidence,
self.spool,
self.actor.principal_id,
Some(super::original_boundary_acceptance::BoundaryOriginalKind::OwnershipResolution),
)?;
self.encode()?;
resolution.validate_genesis(genesis)?;
let SourceAuthor::Account {
spool,
actor,
authority_digest,
..
} = &resolution.acceptance
else {
return Err(invalid("resolution admission requires account acceptance"));
};
if self.spool != trust.spool
|| self.spool_genesis != trust.spool_genesis
|| self.executor != trust.executor
|| self.subject != OriginalAuthoritySubject::OwnershipResolution(resolution.id()?)
|| self.thread != resolution.thread
|| self.publisher != resolution.accepting_publisher
|| self.actor != *actor
|| self.spool != *spool
|| self.authority_digest != *authority_digest
{
return Err(invalid(
"authority admission differs from original ownership resolution",
));
}
Ok(())
}
}
fn invalid(message: &str) -> HeddleError {
HeddleError::InvalidObject(message.into())
}