Skip to main content

coven_protocol/provider/
access.rs

1use super::probe::*;
2use super::*;
3
4#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
5#[serde(transparent)]
6pub struct ProviderAdminGrantId(pub ObjectHash);
7
8impl ProviderAdminGrantId {
9    pub fn from_random_bytes(bytes: [u8; 32]) -> Self {
10        Self(ObjectHash::from_digest(bytes))
11    }
12}
13
14#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
15#[serde(transparent)]
16pub struct ProviderAccessGrantId(pub ObjectHash);
17
18impl ProviderAccessGrantId {
19    pub fn from_random_bytes(bytes: [u8; 32]) -> Self {
20        Self(ObjectHash::from_digest(bytes))
21    }
22}
23
24/// Stable provider authority that can be withdrawn without rediscovering a
25/// member by mutable account metadata.
26#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(rename_all = "snake_case", deny_unknown_fields)]
28pub enum ProviderAccessLocator {
29    S3SharedCredentialGeneration {
30        generation: u64,
31        access_key_id_hash: ObjectHash,
32    },
33    GoogleDrivePermission {
34        drive_id: String,
35        permission_id: String,
36    },
37    DropboxSharedFolderMember {
38        namespace_id: String,
39        account_id: String,
40    },
41    OneDrivePermission {
42        drive_id: String,
43        item_id: String,
44        permission_id: String,
45    },
46    CloudKitPrivateZoneOwner {
47        owner_name: String,
48        zone_name: String,
49        owner_record_name: String,
50    },
51    CloudKitParticipant {
52        share_record_name: String,
53        owner_name: String,
54        zone_name: String,
55        participant_record_name: String,
56    },
57}
58
59impl ProviderAccessLocator {
60    pub fn for_current_administrator(
61        binding: &crate::objects::ResolvedProviderBinding,
62    ) -> Result<Self, StorageError> {
63        binding.validate()?;
64        match (&binding.store, &binding.device.principal) {
65            (
66                StoreProviderBinding::S3 { .. },
67                crate::objects::ProviderPrincipalId::CustomS3Credential { access_key_id_hash },
68            ) => Ok(Self::S3SharedCredentialGeneration {
69                generation: 1,
70                access_key_id_hash: *access_key_id_hash,
71            }),
72            (
73                StoreProviderBinding::GoogleDrive {
74                    corpus: crate::objects::GoogleDriveCorpus::SharedDrive { drive_id, .. },
75                },
76                crate::objects::ProviderPrincipalId::GoogleDrive { permission_id },
77            ) => Ok(Self::GoogleDrivePermission {
78                drive_id: drive_id.clone(),
79                permission_id: permission_id.clone(),
80            }),
81            (
82                StoreProviderBinding::Dropbox { namespace_id },
83                crate::objects::ProviderPrincipalId::Dropbox { account_id },
84            ) => Ok(Self::DropboxSharedFolderMember {
85                namespace_id: namespace_id.clone(),
86                account_id: account_id.clone(),
87            }),
88            (
89                StoreProviderBinding::CloudKit {
90                    owner_name,
91                    zone_name,
92                    ..
93                },
94                crate::objects::ProviderPrincipalId::CloudKitPrivateZoneOwner { record_name },
95            ) => Ok(Self::CloudKitPrivateZoneOwner {
96                owner_name: owner_name.clone(),
97                zone_name: zone_name.clone(),
98                owner_record_name: record_name.clone(),
99            }),
100            _ => Err(StorageError::Configuration(
101                "provider adapter did not expose the administrator's exact access locator"
102                    .to_string(),
103            )),
104        }
105    }
106
107    pub fn validate_for(
108        &self,
109        store: &StoreProviderBinding,
110        provider: &ProviderDeviceBinding,
111    ) -> Result<(), StorageError> {
112        provider.validate_for(store)?;
113        let valid = match (store, &provider.principal, self) {
114            (
115                StoreProviderBinding::S3 { .. },
116                crate::objects::ProviderPrincipalId::CustomS3Credential {
117                    access_key_id_hash: provider_hash,
118                },
119                Self::S3SharedCredentialGeneration {
120                    generation,
121                    access_key_id_hash,
122                },
123            ) => *generation > 0 && provider_hash == access_key_id_hash,
124            (
125                StoreProviderBinding::S3 { .. },
126                crate::objects::ProviderPrincipalId::Aws { .. },
127                Self::S3SharedCredentialGeneration { generation, .. },
128            ) => *generation > 0,
129            (
130                StoreProviderBinding::GoogleDrive {
131                    corpus: crate::objects::GoogleDriveCorpus::SharedDrive { drive_id, .. },
132                },
133                crate::objects::ProviderPrincipalId::GoogleDrive { permission_id },
134                Self::GoogleDrivePermission {
135                    drive_id: locator_drive,
136                    permission_id: locator_permission,
137                },
138            ) => drive_id == locator_drive && permission_id == locator_permission,
139            (
140                StoreProviderBinding::Dropbox { namespace_id },
141                crate::objects::ProviderPrincipalId::Dropbox { account_id },
142                Self::DropboxSharedFolderMember {
143                    namespace_id: locator_namespace,
144                    account_id: locator_account,
145                },
146            ) => namespace_id == locator_namespace && account_id == locator_account,
147            (
148                StoreProviderBinding::OneDrive {
149                    drive_id,
150                    folder_id,
151                },
152                crate::objects::ProviderPrincipalId::OneDrive { .. },
153                Self::OneDrivePermission {
154                    drive_id: locator_drive,
155                    item_id,
156                    permission_id,
157                },
158            ) => drive_id == locator_drive && folder_id == item_id && !permission_id.is_empty(),
159            (
160                StoreProviderBinding::CloudKit {
161                    owner_name,
162                    zone_name,
163                    ..
164                },
165                crate::objects::ProviderPrincipalId::CloudKitPrivateZoneOwner { record_name },
166                Self::CloudKitPrivateZoneOwner {
167                    owner_name: locator_owner,
168                    zone_name: locator_zone,
169                    owner_record_name,
170                },
171            ) => {
172                owner_name == locator_owner
173                    && zone_name == locator_zone
174                    && record_name == owner_record_name
175            }
176            (
177                StoreProviderBinding::CloudKit {
178                    owner_name,
179                    zone_name,
180                    ..
181                },
182                crate::objects::ProviderPrincipalId::CloudKitSharedZoneParticipant { record_name },
183                Self::CloudKitParticipant {
184                    share_record_name,
185                    owner_name: locator_owner,
186                    zone_name: locator_zone,
187                    participant_record_name,
188                },
189            ) => {
190                !share_record_name.is_empty()
191                    && owner_name == locator_owner
192                    && zone_name == locator_zone
193                    && record_name == participant_record_name
194            }
195            _ => false,
196        };
197        if valid {
198            Ok(())
199        } else {
200            Err(StorageError::Configuration(
201                "provider access locator differs from its Store and provider binding".to_string(),
202            ))
203        }
204    }
205}
206
207/// The wire body of one member's provider access grant. Every field here is
208/// signed.
209#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
210#[serde(deny_unknown_fields)]
211pub struct StoreMemberProviderAccessGrantBody {
212    pub grant_id: ProviderAccessGrantId,
213    pub member_pubkey: String,
214    pub provider: ProviderDeviceBinding,
215    pub locator: ProviderAccessLocator,
216    pub administrator_grant: ProviderAdminGrantId,
217    pub administrator: StoreDeviceRegistrationRef,
218}
219
220impl crate::store_commit::SignedBody for StoreMemberProviderAccessGrantBody {
221    const DOMAIN: &'static [u8] = MEMBER_ACCESS_GRANT_DOMAIN;
222}
223
224pub type StoreMemberProviderAccessGrant =
225    crate::store_commit::Signed<StoreMemberProviderAccessGrantBody>;
226
227impl StoreMemberProviderAccessGrant {
228    #[allow(clippy::too_many_arguments)]
229    pub fn signed(
230        grant_id: ProviderAccessGrantId,
231        member_pubkey: String,
232        provider: ProviderDeviceBinding,
233        locator: ProviderAccessLocator,
234        administrator_grant: ProviderAdminGrantId,
235        administrator: StoreDeviceRegistrationRef,
236        store: &StoreProviderBinding,
237        administrator_registration: &StoreDeviceRegistration,
238        administrator_signer: &dyn coven_keys::keys::DeviceSigningAuthority,
239    ) -> Result<Self, ProviderProbeError> {
240        administrator.verify_registration(administrator_registration)?;
241        if administrator_signer.public_key_hex() != administrator_registration.device_signing_pubkey
242        {
243            return invalid("provider access grant signer is not the administrator device");
244        }
245        locator.validate_for(store, &provider)?;
246        Ok(crate::store_commit::Signed::sign_by_device(
247            StoreMemberProviderAccessGrantBody {
248                grant_id,
249                member_pubkey,
250                provider,
251                locator,
252                administrator_grant,
253                administrator,
254            },
255            administrator_signer,
256        ))
257    }
258
259    pub fn grant_hash(&self) -> ObjectHash {
260        self.hash()
261    }
262
263    pub fn verify(
264        &self,
265        store: &StoreProviderBinding,
266        administrator: &StoreDeviceRegistration,
267    ) -> Result<(), ProviderProbeError> {
268        self.administrator.verify_registration(administrator)?;
269        self.provider
270            .validate_for(store)
271            .map_err(ProviderProbeError::Storage)?;
272        self.locator
273            .validate_for(store, &self.provider)
274            .map_err(ProviderProbeError::Storage)?;
275        if self
276            .verify_by(&administrator.device_signing_pubkey)
277            .is_err()
278        {
279            return invalid("provider access grant signature is invalid");
280        }
281        Ok(())
282    }
283}
284
285#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
286#[serde(deny_unknown_fields)]
287pub struct StoreMemberProviderAccessGrantRef {
288    pub grant_id: ProviderAccessGrantId,
289    pub grant_hash: ObjectHash,
290    pub object: ExactObjectRef,
291}
292
293impl StoreMemberProviderAccessGrantRef {
294    pub fn from_grant(grant: &StoreMemberProviderAccessGrant, object: ExactObjectRef) -> Self {
295        Self {
296            grant_id: grant.grant_id.clone(),
297            grant_hash: grant.grant_hash(),
298            object,
299        }
300    }
301
302    pub fn verify(&self, grant: &StoreMemberProviderAccessGrant) -> Result<(), ProviderProbeError> {
303        if self.grant_id != grant.grant_id || self.grant_hash != grant.grant_hash() {
304            return invalid("provider access grant reference differs from its signed grant");
305        }
306        Ok(())
307    }
308}
309
310#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
311#[serde(deny_unknown_fields)]
312pub struct ActivatedStoreMemberProviderAccessGrant {
313    pub grant: StoreMemberProviderAccessGrant,
314    pub grant_ref: StoreMemberProviderAccessGrantRef,
315    pub activation: StoreBatchCommitRef,
316}
317
318#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
319#[serde(rename_all = "snake_case", deny_unknown_fields)]
320pub enum ProviderAccessWithdrawal {
321    Direct {
322        locator: ProviderAccessLocator,
323        verified_absent: bool,
324    },
325    S3CredentialRotation {
326        retired_generation: u64,
327        active_generation: u64,
328        retired_credential_verified_rejected: bool,
329    },
330}
331
332impl ProviderAccessWithdrawal {
333    pub(super) fn validate(&self) -> Result<(), ProviderProbeError> {
334        let valid = match self {
335            Self::Direct {
336                verified_absent, ..
337            } => *verified_absent,
338            Self::S3CredentialRotation {
339                retired_generation,
340                active_generation,
341                retired_credential_verified_rejected,
342            } => {
343                *retired_generation > 0
344                    && retired_generation.checked_add(1) == Some(*active_generation)
345                    && *retired_credential_verified_rejected
346            }
347        };
348        if valid {
349            Ok(())
350        } else {
351            invalid("provider access withdrawal does not prove the stored authority is unusable")
352        }
353    }
354
355    pub fn verify_for_locator(
356        &self,
357        locator: &ProviderAccessLocator,
358    ) -> Result<(), ProviderProbeError> {
359        self.validate()?;
360        let matches = match (self, locator) {
361            (
362                Self::Direct {
363                    locator: withdrawn, ..
364                },
365                expected,
366            ) => withdrawn == expected,
367            (
368                Self::S3CredentialRotation {
369                    retired_generation, ..
370                },
371                ProviderAccessLocator::S3SharedCredentialGeneration { generation, .. },
372            ) => retired_generation == generation,
373            _ => false,
374        };
375        if matches {
376            Ok(())
377        } else {
378            invalid("provider access withdrawal differs from the stored authority locator")
379        }
380    }
381}