Skip to main content

coven_replication/sync/store/device_join/authorized_join/
admission.rs

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        // The commit that opened the attempt is what the challenge is
28        // authorized against; there is no separate attempt file to agree with.
29        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                        &current,
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                        &current,
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(&current, 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(&current, 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(&current, 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}