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