1use super::*;
2
3impl<'operation, 'storage> AuthorizedJoin<'operation, 'storage> {
4 fn sign_device_admission_approval(
5 &self,
6 request: DeviceProviderAccessRequest,
7 admission: DeviceProviderAdmission,
8 ) -> Result<DeviceProviderAdmissionApproval, DeviceJoinError> {
9 self.local_writer
10 .sign_device_admission_approval(request, admission, &self.verified_root)
11 }
12
13 async fn publish_cross_principal_challenge(
14 &mut self,
15 authorization: &DeviceJoinChallengePublicationAuthorization,
16 challenge: &CrossPrincipalProbeChallenge,
17 context: &coven_protocol::provider::CrossPrincipalChallengeContext,
18 store: &StoreProviderBinding,
19 attempt_owner: &StoreDeviceRegistration,
20 ) -> Result<CrossPrincipalProbeChallenge, DeviceJoinError> {
21 self.local_writer
22 .verify_cross_principal_challenge(challenge, context, store)
23 .map_err(DeviceJoinError::ProviderProbe)?;
24 if authorization.attempt_id != context.attempt_id {
25 return Err(DeviceJoinError::AttemptMismatch);
26 }
27 let activation = self
30 .join_history()
31 .load_commit(&authorization.attempt_activation)
32 .await?;
33 if activation.author() != attempt_owner
34 || !activation
35 .device_join_attempt_decisions()
36 .iter()
37 .any(|decision| {
38 matches!(
39 decision,
40 DeviceJoinAttemptDecisionRef::Attempt(opened)
41 if *opened == authorization.attempt_id
42 )
43 })
44 {
45 return Err(DeviceJoinError::AttemptMismatch);
46 }
47 self.storage
48 .settle_cross_principal_challenge(
49 &self.database,
50 authorization,
51 challenge,
52 context,
53 store,
54 )
55 .await
56 .map_err(DeviceJoinError::ProviderProbe)
57 }
58
59 pub(crate) async fn authorize_access(
60 &mut self,
61 request: DeviceProviderAccessRequest,
62 access_administrator: Option<&dyn DeviceProviderAccessAdministrator>,
63 ) -> Result<DeviceProviderAdmissionApproval, DeviceJoinError> {
64 let provider_admin = self.resolve_provider_admin(&request.offer.provider_admin.grant_id)?;
65 if provider_admin != *request.offer.provider_admin {
66 return Err(DeviceJoinError::OfferMismatch);
67 }
68 let owner = self
69 .join_history()
70 .load_registration(&request.offer.owner_registration)
71 .await?
72 .value;
73 request.verify(&owner)?;
74 if !self
75 .local_writer
76 .is_authored_by_registration(&provider_admin.administrator)
77 {
78 return Err(DeviceJoinError::ProviderAdministratorRequired);
79 }
80 let database = self.database.clone();
81 let journal = self.journal(request.offer.attempt_id);
82 let current = journal.current().await?;
83 let durable = match &*current.progress {
84 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Offered(_)) => {
85 journal
86 .advance(
87 ¤t,
88 OwnerJoinProgress::AccessRequested(request.clone()),
89 )
90 .await?
91 }
92 _ => current,
93 };
94 if provider_admin.provider == request.peer_provider {
95 return match &*durable.progress {
96 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::ApprovalPrepared(approval)) => {
97 Ok(approval.clone())
98 }
99 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::AccessRequested(
100 durable_request,
101 )) if durable_request == &request => {
102 let approval = self.sign_device_admission_approval(
103 request,
104 DeviceProviderAdmission::SamePrincipal,
105 )?;
106 journal
107 .advance(
108 &durable,
109 OwnerJoinProgress::ApprovalPrepared(approval.clone()),
110 )
111 .await?;
112 Ok(approval)
113 }
114 _ => Err(DeviceJoinError::JournalConflict),
115 };
116 }
117 let (grant, grant_ref, activation, activated_progress) = match &*durable.progress {
118 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::ApprovalPrepared(approval)) => {
119 return Ok(approval.clone())
120 }
121 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::AccessGrantActivated {
122 request: durable_request,
123 grant,
124 grant_ref,
125 activation,
126 }) if durable_request == &request => (
127 grant.clone(),
128 grant_ref.clone(),
129 activation.clone(),
130 durable.clone(),
131 ),
132 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::StorePublicationPrepared(
133 prepared,
134 )) if matches!(&prepared.operation, OwnerJoinPublication::ProviderAccessGrant { request: durable, .. } if durable == &request) =>
135 {
136 let OwnerJoinPublication::ProviderAccessGrant { grant, .. } = &prepared.operation
137 else {
138 unreachable!("matched provider access grant publication")
139 };
140 let grant_ref = prepared.candidate.commit.provider_access_grants()[0].clone();
141 let activation = self.publish_owner_publication(prepared.clone()).await?;
142 (
143 grant.clone(),
144 grant_ref,
145 activation,
146 journal.current().await?,
147 )
148 }
149 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::AccessRequested(durable_request))
150 if durable_request == &request =>
151 {
152 let administrator =
153 access_administrator.ok_or(DeviceJoinError::ProviderAdministratorRequired)?;
154 let locator = administrator
155 .grant_member_access(
156 &request.offer.member_pubkey,
157 self.membership
158 .current_member_provider_email(&request.offer.member_pubkey),
159 &request.peer_provider,
160 )
161 .await?;
162 let grant_id = ProviderAccessGrantId::from_random_bytes(
163 *ObjectHash::digest(database.new_store_write_id().as_str().as_bytes())
164 .as_bytes(),
165 );
166 let grant = self
167 .local_writer
168 .sign_provider_access_grant(
169 grant_id,
170 request.offer.member_pubkey.clone(),
171 request.peer_provider.clone(),
172 locator,
173 provider_admin.grant_id.clone(),
174 provider_admin.administrator.clone(),
175 &request.offer.provider,
176 )
177 .map_err(DeviceJoinError::ProviderProbe)?;
178 let context = coven_protocol::objects::ProtocolObjectContext::signed_plaintext(
179 request.offer.store_root.store_root_hash,
180 ProtocolObjectDomain::ProviderAccessGrant,
181 );
182 let prefix = coven_protocol::store_commit::provider_access_grant_semantic_prefix(
183 &grant.grant_id,
184 );
185 let slot = self
186 .storage
187 .allocate_protocol_slot(&context, &prefix, ".json")
188 .await?;
189 let prepared = self.storage.prepare_protocol_object(
190 &context,
191 slot,
192 &prefix,
193 grant.to_bytes(),
194 )?;
195 let grant_ref = StoreMemberProviderAccessGrantRef::from_grant(
196 &grant,
197 prepared.reference().clone(),
198 );
199 let plan = self.writer.prepare_plan().await?;
200 let publication = self
201 .prepare_owner_publication(
202 durable.clone(),
203 OwnerJoinPublication::ProviderAccessGrant {
204 request: request.clone(),
205 grant: grant.clone(),
206 },
207 plan,
208 crate::sync::store::commit_publication::operation::commit_plan::StoreOperationBatch::ProviderAccessGrant(
209 grant_ref.clone(),
210 ),
211 )
212 .await?;
213 let activation = self.publish_owner_publication(publication).await?;
214 (grant, grant_ref, activation, journal.current().await?)
215 }
216 _ => return Err(DeviceJoinError::JournalConflict),
217 };
218 let challenge_context = request.cross_challenge_context();
219 let probe_id = coven_protocol::provider::ProviderProbeId::from_bytes(
220 *ObjectHash::digest(database.new_store_write_id().as_str().as_bytes()).as_bytes(),
221 );
222 let challenge = self
223 .storage
224 .prepare_cross_principal_challenge(
225 &database,
226 probe_id,
227 &request.offer.provider,
228 &challenge_context,
229 self.local_writer.as_ref(),
230 )
231 .await
232 .map_err(DeviceJoinError::ProviderProbe)?;
233 let approval = self.sign_device_admission_approval(
234 request,
235 DeviceProviderAdmission::CrossPrincipal {
236 access_grant: Box::new(ActivatedStoreMemberProviderAccessGrant {
237 grant,
238 grant_ref,
239 activation,
240 }),
241 challenge,
242 },
243 )?;
244 journal
245 .advance(
246 &activated_progress,
247 OwnerJoinProgress::ApprovalPrepared(approval.clone()),
248 )
249 .await?;
250 Ok(approval)
251 }
252
253 pub(crate) async fn publish_challenge(
254 &mut self,
255 bootstrap: ProvisionalDeviceBootstrap,
256 ) -> Result<ProviderReadyDeviceBootstrap, DeviceJoinError> {
257 let offer = &bootstrap.request.approval().request.offer;
258 if &self.resolve_provider_admin(&offer.provider_admin.grant_id)?
259 != offer.provider_admin.as_ref()
260 {
261 return Err(DeviceJoinError::OfferMismatch);
262 }
263 let owner = self
264 .join_history()
265 .load_registration(&offer.owner_registration)
266 .await?
267 .value;
268 self.local_writer.verify_own_device_admission_approval(
269 bootstrap.request.approval(),
270 &self.verified_root,
271 )?;
272 let challenge_publication = match &bootstrap.request.approval().admission {
273 DeviceProviderAdmission::SamePrincipal => {
274 DeviceProviderChallengePublication::SamePrincipal
275 }
276 DeviceProviderAdmission::CrossPrincipal { challenge, .. } => {
277 let context = bootstrap
278 .request
279 .approval()
280 .request
281 .cross_challenge_context();
282 let authorization = DeviceJoinChallengePublicationAuthorization {
283 attempt_id: bootstrap.publication_authorization.attempt_id,
284 attempt_activation: bootstrap
285 .publication_authorization
286 .attempt_activation
287 .clone(),
288 };
289 let published = self
290 .publish_cross_principal_challenge(
291 &authorization,
292 challenge,
293 &context,
294 &offer.provider,
295 &owner,
296 )
297 .await?;
298 DeviceProviderChallengePublication::CrossPrincipal {
299 challenge: published,
300 }
301 }
302 };
303 let attempt_id = offer.attempt_id;
304 let ready = ProviderReadyDeviceBootstrap {
305 bootstrap: Box::new(bootstrap),
306 challenge_publication,
307 };
308 let journal = self.journal(attempt_id);
309 let current = journal.current().await?;
310 match &*current.progress {
311 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::ProviderReady(existing))
312 if existing == &ready =>
313 {
314 return Ok(ready)
315 }
316 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::AttemptActivated(bootstrap))
317 if bootstrap == ready.bootstrap.as_ref() =>
318 {
319 let intent = journal
320 .advance(
321 ¤t,
322 OwnerJoinProgress::ChallengeCreateIntent(*ready.bootstrap.clone()),
323 )
324 .await?;
325 journal
326 .advance(&intent, OwnerJoinProgress::ProviderReady(ready.clone()))
327 .await?;
328 }
329 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::ChallengeCreateIntent(bootstrap))
330 if bootstrap == ready.bootstrap.as_ref() =>
331 {
332 journal
333 .advance(¤t, OwnerJoinProgress::ProviderReady(ready.clone()))
334 .await?;
335 }
336 _ => return Err(DeviceJoinError::JournalConflict),
337 }
338 Ok(ready)
339 }
340
341 pub(super) async fn complete_admission(
342 &mut self,
343 readiness: DeviceJoinReadiness,
344 ) -> Result<DeviceProviderAdmissionCompletion, DeviceJoinError> {
345 let attempt_id = readiness.proof.attempt_id;
346 let database = self.database.clone();
347 let journal = self.journal(attempt_id);
348 let current = journal.current().await?;
349 if let DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Completed(existing)) =
350 &*current.progress
351 {
352 if matches!(
353 existing,
354 DeviceProviderAdmissionCompletion::CrossPrincipal {
355 readiness: durable,
356 ..
357 } if **durable == readiness
358 ) {
359 return Ok(existing.clone());
360 }
361 return Err(DeviceJoinError::JournalConflict);
362 }
363 let bootstrap = match &*current.progress {
364 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::ProviderReady(bootstrap)) => {
365 bootstrap.clone()
366 }
367 _ => return Err(DeviceJoinError::JournalConflict),
368 };
369 if readiness.proof.attempt_id != bootstrap.bootstrap.publication_authorization.attempt_id {
370 return Err(DeviceJoinError::AttemptMismatch);
371 }
372 let offer = &bootstrap.bootstrap.request.approval().request.offer;
373 let provider_admin = self.resolve_provider_admin(&offer.provider_admin.grant_id)?;
374 if &provider_admin != offer.provider_admin.as_ref() {
375 return Err(DeviceJoinError::ProviderAdministratorRequired);
376 }
377 let receipt = match (
378 &bootstrap.bootstrap.request.approval().admission,
379 &bootstrap.bootstrap.request.response(),
380 &readiness.provider,
381 ) {
382 (
383 DeviceProviderAdmission::CrossPrincipal { challenge, .. },
384 DeviceProviderResponseReservation::CrossPrincipal { response_slot },
385 DeviceProviderReadiness::CrossPrincipal(response),
386 ) => {
387 let context = coven_protocol::provider::CrossPrincipalResponseContext {
388 challenge: bootstrap
389 .bootstrap
390 .request
391 .approval()
392 .request
393 .cross_challenge_context(),
394 expected_registration_hash: bootstrap
395 .bootstrap
396 .request
397 .expected_registration()
398 .registration_hash(),
399 response_slot: response_slot.clone(),
400 };
401 self.storage
402 .complete_cross_principal_probe(
403 &database,
404 challenge,
405 response,
406 &context,
407 &offer.provider,
408 self.local_writer.as_ref(),
409 &offer.member_pubkey,
410 )
411 .await
412 .map_err(DeviceJoinError::ProviderProbe)?
413 }
414 _ => return Err(DeviceJoinError::AttemptMismatch),
415 };
416 let completion = DeviceProviderAdmissionCompletion::CrossPrincipal {
417 bootstrap: Box::new(bootstrap.clone()),
418 readiness: Box::new(readiness.clone()),
419 receipt,
420 };
421 let observed = journal
422 .advance(¤t, OwnerJoinProgress::ResponseObserved(readiness))
423 .await?;
424 journal
425 .advance(&observed, OwnerJoinProgress::Completed(completion.clone()))
426 .await?;
427 Ok(completion)
428 }
429
430 pub(crate) async fn complete_same_principal(
431 &mut self,
432 bootstrap: ProviderReadyDeviceBootstrap,
433 ) -> Result<DeviceProviderAdmissionCompletion, DeviceJoinError> {
434 let attempt_id = bootstrap.bootstrap.publication_authorization.attempt_id;
435 if !matches!(
436 (
437 &bootstrap.bootstrap.request.approval().admission,
438 bootstrap.bootstrap.request.response(),
439 &bootstrap.challenge_publication,
440 ),
441 (
442 DeviceProviderAdmission::SamePrincipal,
443 DeviceProviderResponseReservation::SamePrincipal,
444 DeviceProviderChallengePublication::SamePrincipal,
445 )
446 ) {
447 return Err(DeviceJoinError::AttemptMismatch);
448 }
449 let journal = self.journal(attempt_id);
450 let current = journal.current().await?;
451 if let DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Completed(existing)) =
452 &*current.progress
453 {
454 return match existing {
455 DeviceProviderAdmissionCompletion::SamePrincipal { bootstrap: durable }
456 if **durable == bootstrap =>
457 {
458 Ok(existing.clone())
459 }
460 _ => Err(DeviceJoinError::JournalConflict),
461 };
462 }
463 match &*current.progress {
464 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::ProviderReady(durable))
465 if durable == &bootstrap => {}
466 _ => return Err(DeviceJoinError::JournalConflict),
467 }
468 let completion = DeviceProviderAdmissionCompletion::SamePrincipal {
469 bootstrap: Box::new(bootstrap),
470 };
471 journal
472 .advance(¤t, OwnerJoinProgress::Completed(completion.clone()))
473 .await?;
474 Ok(completion)
475 }
476}
477
478impl Store {
479 #[doc(hidden)]
480 pub(crate) async fn authorize_device_provider_access(
481 &self,
482 request: DeviceProviderAccessRequest,
483 access_administrator: Option<&dyn DeviceProviderAccessAdministrator>,
484 ) -> Result<DeviceProviderAdmissionApproval, DeviceJoinError> {
485 let mut writer = self
486 .authorize_writer()
487 .await
488 .map_err(DeviceJoinError::from)?;
489 writer
490 .join_operation()
491 .authorize_access(request, access_administrator)
492 .await
493 }
494
495 #[doc(hidden)]
496 pub(crate) async fn publish_device_provider_challenge(
497 &self,
498 bootstrap: ProvisionalDeviceBootstrap,
499 ) -> Result<ProviderReadyDeviceBootstrap, DeviceJoinError> {
500 let mut writer = self
501 .authorize_writer()
502 .await
503 .map_err(DeviceJoinError::from)?;
504 writer.join_operation().publish_challenge(bootstrap).await
505 }
506
507 #[doc(hidden)]
508 pub(crate) async fn complete_device_provider_admission(
509 &self,
510 readiness: DeviceJoinReadiness,
511 ) -> Result<DeviceProviderAdmissionCompletion, DeviceJoinError> {
512 let mut writer = self
513 .authorize_writer()
514 .await
515 .map_err(DeviceJoinError::from)?;
516 writer.join_operation().complete_admission(readiness).await
517 }
518
519 #[doc(hidden)]
520 pub(crate) async fn complete_same_principal_device_admission(
521 &self,
522 bootstrap: ProviderReadyDeviceBootstrap,
523 ) -> Result<DeviceProviderAdmissionCompletion, DeviceJoinError> {
524 let mut writer = self
525 .authorize_writer()
526 .await
527 .map_err(DeviceJoinError::from)?;
528 writer
529 .join_operation()
530 .complete_same_principal(bootstrap)
531 .await
532 }
533}
534
535#[async_trait::async_trait]
536pub trait DeviceProviderAccessAdministrator: Send + Sync {
537 async fn grant_member_access(
538 &self,
539 member_pubkey: &str,
540 provider_account_email: Option<&str>,
541 peer: &ProviderDeviceBinding,
542 ) -> Result<coven_protocol::provider::ProviderAccessLocator, DeviceJoinError>;
543}