heddle_object_model/object/
thread_authority_admission.rs1use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5
6use super::{
7 CollaborationActor, ContentHash,
8 thread_replication::{
9 SourceAuthor, ThreadOperation, ThreadOperationBody, integration::TrustedHostedExecutor,
10 metadata::ThreadControl,
11 },
12};
13use crate::error::{HeddleError, Result};
14
15pub const FORMAT: &str = "heddle-thread-authority-admission-v3";
16pub const MAX_BYTES: usize = 2048;
17
18#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
21pub enum OriginalAuthoritySubject {
22 Operation(ContentHash),
23 OwnershipClaim(ContentHash),
24 OwnershipResolution(ContentHash),
25}
26impl OriginalAuthoritySubject {
27 pub fn id(&self) -> ContentHash {
28 match self {
29 Self::Operation(id) | Self::OwnershipClaim(id) | Self::OwnershipResolution(id) => *id,
30 }
31 }
32 pub fn operation_id(&self) -> Option<ContentHash> {
33 match self {
34 Self::Operation(id) => Some(*id),
35 Self::OwnershipClaim(_) | Self::OwnershipResolution(_) => None,
36 }
37 }
38 pub fn claim_id(&self) -> Option<ContentHash> {
39 match self {
40 Self::OwnershipClaim(id) => Some(*id),
41 Self::Operation(_) | Self::OwnershipResolution(_) => None,
42 }
43 }
44}
45
46#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
49#[serde(deny_unknown_fields)]
50pub struct OriginalAuthorityBinding {
51 pub spool: Uuid,
52 pub actor: CollaborationActor,
53 pub authority_digest: ContentHash,
54}
55impl OriginalAuthorityBinding {
56 pub fn from_operation(operation: &ThreadOperation) -> Result<Option<Self>> {
57 if let ThreadOperationBody::Metadata(bytes) = &operation.body {
58 let control = ThreadControl::decode(bytes)?;
59 return Ok(Some(Self {
60 spool: control.spool,
61 actor: control.actor,
62 authority_digest: control.authority_digest,
63 }));
64 }
65 match operation.source_author()? {
66 Some(SourceAuthor::Account {
67 spool,
68 actor,
69 authority_digest,
70 authority,
71 }) => {
72 SourceAuthor::Account {
73 spool,
74 actor: actor.clone(),
75 authority_digest,
76 authority,
77 }
78 .validate()?;
79 Ok(Some(Self {
80 spool,
81 actor,
82 authority_digest,
83 }))
84 }
85 Some(SourceAuthor::LocalKey) | None => Ok(None),
86 }
87 }
88}
89
90#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
91#[serde(deny_unknown_fields)]
92pub struct ThreadAuthorityAdmission {
93 pub version: u16,
94 pub basis: super::original_boundary_acceptance::AdmissionBasis,
95 pub spool: Uuid,
96 pub spool_genesis: ContentHash,
97 pub thread: ContentHash,
98 pub subject: OriginalAuthoritySubject,
99 pub actor: CollaborationActor,
100 pub publisher: [u8; 32],
101 pub authority_digest: ContentHash,
102 pub executor: [u8; 32],
103 pub admitted_at_ms: i64,
106}
107impl ThreadAuthorityAdmission {
108 pub fn encode(&self) -> Result<Vec<u8>> {
109 if self.version != 3
110 || self.spool.is_nil()
111 || self.actor.principal_id.is_nil()
112 || self.publisher == [0; 32]
113 || self.executor == [0; 32]
114 || self.admitted_at_ms < 0
115 || self.actor.agent_id.as_ref().is_some_and(|id| {
116 id.is_empty() || id.len() > 256 || id.chars().any(char::is_control)
117 })
118 {
119 return Err(invalid("invalid original-author admission statement"));
120 }
121 let bytes = rmp_serde::to_vec_named(self)?;
122 if bytes.len() > MAX_BYTES {
123 return Err(invalid("authority admission statement exceeds byte bound"));
124 }
125 Ok(bytes)
126 }
127 pub fn decode(bytes: &[u8]) -> Result<Self> {
128 if bytes.is_empty() || bytes.len() > MAX_BYTES {
129 return Err(invalid("authority admission statement exceeds byte bound"));
130 }
131 let value: Self = rmp_serde::from_slice(bytes)?;
132 if value.encode()? != bytes {
133 return Err(invalid("noncanonical authority admission statement"));
134 }
135 Ok(value)
136 }
137 pub fn authorize(
140 &self,
141 operation: &ThreadOperation,
142 trust: &TrustedHostedExecutor,
143 ) -> Result<()> {
144 self.authorize_with_acceptance(operation, trust, None)
145 }
146 pub fn authorize_with_acceptance(
147 &self,
148 operation: &ThreadOperation,
149 trust: &TrustedHostedExecutor,
150 evidence: Option<&super::original_boundary_acceptance::OriginalBoundaryAcceptance>,
151 ) -> Result<()> {
152 self.basis.authorize_evidence(
153 evidence,
154 self.spool,
155 self.actor.principal_id,
156 operation
157 .source_author()?
158 .map(|_| super::original_boundary_acceptance::BoundaryOriginalKind::Source),
159 )?;
160 self.encode()?;
161 if self.spool != trust.spool
162 || self.spool_genesis != trust.spool_genesis
163 || self.executor != trust.executor
164 {
165 return Err(invalid(
166 "authority admission differs from independently pinned executor",
167 ));
168 }
169 let binding = OriginalAuthorityBinding::from_operation(operation)?
170 .ok_or_else(|| invalid("account admission requires original authored account work"))?;
171 if self.subject != OriginalAuthoritySubject::Operation(operation.id()?)
172 || self.thread != operation.thread
173 || self.publisher != operation.publisher
174 || self.actor != binding.actor
175 || self.spool != binding.spool
176 || self.authority_digest != binding.authority_digest
177 {
178 return Err(invalid(
179 "authority admission differs from original operation",
180 ));
181 }
182 Ok(())
183 }
184 pub fn authorize_claim(
185 &self,
186 claim: &super::thread_replication::ownership_claim::ThreadOwnershipClaim,
187 genesis: &super::thread_replication::ThreadGenesis,
188 trust: &TrustedHostedExecutor,
189 ) -> Result<()> {
190 self.authorize_claim_with_acceptance(claim, genesis, trust, None)
191 }
192 pub fn authorize_claim_with_acceptance(
193 &self,
194 claim: &super::thread_replication::ownership_claim::ThreadOwnershipClaim,
195 genesis: &super::thread_replication::ThreadGenesis,
196 trust: &TrustedHostedExecutor,
197 evidence: Option<&super::original_boundary_acceptance::OriginalBoundaryAcceptance>,
198 ) -> Result<()> {
199 self.basis.authorize_evidence(
200 evidence,
201 self.spool,
202 self.actor.principal_id,
203 Some(super::original_boundary_acceptance::BoundaryOriginalKind::OwnershipClaim),
204 )?;
205 self.encode()?;
206 claim.validate_genesis(genesis)?;
207 let SourceAuthor::Account {
208 spool,
209 actor,
210 authority_digest,
211 ..
212 } = &claim.acceptance
213 else {
214 return Err(invalid(
215 "claim admission requires signed account acceptance",
216 ));
217 };
218 if self.spool != trust.spool
219 || self.spool_genesis != trust.spool_genesis
220 || self.executor != trust.executor
221 {
222 return Err(invalid(
223 "authority admission differs from independently pinned executor",
224 ));
225 }
226 if self.subject != OriginalAuthoritySubject::OwnershipClaim(claim.id()?)
227 || self.thread != claim.thread
228 || self.publisher != claim.accepting_publisher
229 || self.actor != *actor
230 || self.spool != *spool
231 || self.authority_digest != *authority_digest
232 {
233 return Err(invalid(
234 "authority admission differs from original ownership claim",
235 ));
236 }
237 Ok(())
238 }
239 pub fn authorize_resolution_with_acceptance(
240 &self,
241 resolution: &super::thread_replication::ownership_resolution::ThreadOwnershipResolution,
242 genesis: &super::thread_replication::ThreadGenesis,
243 trust: &TrustedHostedExecutor,
244 evidence: Option<&super::original_boundary_acceptance::OriginalBoundaryAcceptance>,
245 ) -> Result<()> {
246 self.basis.authorize_evidence(
247 evidence,
248 self.spool,
249 self.actor.principal_id,
250 Some(super::original_boundary_acceptance::BoundaryOriginalKind::OwnershipResolution),
251 )?;
252 self.encode()?;
253 resolution.validate_genesis(genesis)?;
254 let SourceAuthor::Account {
255 spool,
256 actor,
257 authority_digest,
258 ..
259 } = &resolution.acceptance
260 else {
261 return Err(invalid("resolution admission requires account acceptance"));
262 };
263 if self.spool != trust.spool
264 || self.spool_genesis != trust.spool_genesis
265 || self.executor != trust.executor
266 || self.subject != OriginalAuthoritySubject::OwnershipResolution(resolution.id()?)
267 || self.thread != resolution.thread
268 || self.publisher != resolution.accepting_publisher
269 || self.actor != *actor
270 || self.spool != *spool
271 || self.authority_digest != *authority_digest
272 {
273 return Err(invalid(
274 "authority admission differs from original ownership resolution",
275 ));
276 }
277 Ok(())
278 }
279}
280fn invalid(message: &str) -> HeddleError {
281 HeddleError::InvalidObject(message.into())
282}