coven_storage/
invitation.rs1use crate::{CloudProvider, StorageConfig, StorageError, StorageFailure};
2use coven_crypto::{SecretBytes, SecretText};
3use coven_format::code_fields::{FieldReader, FieldWriter};
4use serde::{Deserialize, Serialize};
5
6#[derive(Clone, Serialize, Deserialize)]
9#[serde(try_from = "RecordedInvitation")]
10pub struct StorageInvitation {
11 location: StorageConfig,
12 pub(crate) acceptance: InvitationAcceptance,
13}
14#[derive(Clone, Serialize, Deserialize)]
15#[serde(deny_unknown_fields)]
16pub(crate) enum InvitationAcceptance {
17 Granted,
18 DropboxMount,
19 OneDriveShare { token: SecretText },
20 CloudKitShare { url: SecretText },
21}
22#[derive(Deserialize)]
23#[serde(deny_unknown_fields)]
24struct RecordedInvitation {
25 location: StorageConfig,
26 acceptance: InvitationAcceptance,
27}
28impl TryFrom<RecordedInvitation> for StorageInvitation {
29 type Error = StorageError;
30 fn try_from(value: RecordedInvitation) -> Result<Self, Self::Error> {
31 Self::new(value.location, value.acceptance)
32 }
33}
34impl std::fmt::Debug for StorageInvitation {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 f.debug_struct("StorageInvitation")
37 .field("provider", &self.location.provider())
38 .finish_non_exhaustive()
39 }
40}
41impl StorageInvitation {
42 pub fn for_account(location: StorageConfig) -> Result<Self, StorageError> {
46 let acceptance = match location.provider() {
47 CloudProvider::Dropbox => InvitationAcceptance::DropboxMount,
48 CloudProvider::CloudKit => {
49 return Err(StorageFailure::InvalidConfiguration
50 .with_source("CloudKit requires a share URL"))
51 }
52 _ => InvitationAcceptance::Granted,
53 };
54 Self::new(location, acceptance)
55 }
56 pub(crate) fn new(
57 location: StorageConfig,
58 acceptance: InvitationAcceptance,
59 ) -> Result<Self, StorageError> {
60 location.validate()?;
61 if matches!(
62 location,
63 StorageConfig::Dropbox { location: crate::DropboxLocation::AppFolder, .. }
64 ) {
65 return Err(StorageFailure::DropboxAppFolderNotShareable.into());
66 }
67 match (location.provider(), &acceptance) {
68 (CloudProvider::S3 | CloudProvider::OneDrive, InvitationAcceptance::Granted)
69 | (CloudProvider::Dropbox, InvitationAcceptance::DropboxMount) => {}
70 (CloudProvider::OneDrive, InvitationAcceptance::OneDriveShare { token })
71 if !token.as_str().is_empty() => {}
72 (CloudProvider::CloudKit, InvitationAcceptance::CloudKitShare { url }) => {
73 let url = url::Url::parse(url.as_str())
74 .map_err(|error| StorageFailure::InvalidConfiguration.with_source(error))?;
75 if !crate::web_url::is_web_url(&url) || url.scheme() != "https" {
76 return Err(StorageFailure::InvalidConfiguration
77 .with_source("invalid CloudKit share URL"));
78 }
79 }
80 _ => {
81 return Err(StorageFailure::InvalidConfiguration
82 .with_source("invitation does not match its provider"))
83 }
84 }
85 Ok(Self { location, acceptance })
86 }
87 pub fn location(&self) -> &StorageConfig {
89 &self.location
90 }
91 pub fn encode(&self) -> Result<SecretBytes, StorageError> {
94 crate::secret_json::encode(self)
95 }
96 pub fn decode(bytes: &[u8]) -> Result<Self, StorageError> {
98 crate::secret_json::decode(bytes)
99 }
100 pub(crate) fn pack(&self, out: &mut FieldWriter) -> Result<(), coven_format::Error> {
105 self.location.pack(out)?;
106 match &self.acceptance {
107 InvitationAcceptance::Granted
108 if self.location.provider() == CloudProvider::OneDrive =>
109 {
110 out.option(None::<&str>, FieldWriter::text)
111 }
112 InvitationAcceptance::Granted | InvitationAcceptance::DropboxMount => Ok(()),
113 InvitationAcceptance::OneDriveShare { token } => {
114 out.option(Some(token.as_str()), FieldWriter::text)
115 }
116 InvitationAcceptance::CloudKitShare { url } => out.text(url.as_str()),
117 }
118 }
119 pub(crate) fn unpack(input: &mut FieldReader<'_>) -> Result<Self, StorageError> {
121 let location = StorageConfig::unpack(input)?;
122 let acceptance = match location.provider() {
123 CloudProvider::S3 => InvitationAcceptance::Granted,
124 CloudProvider::Dropbox => InvitationAcceptance::DropboxMount,
125 CloudProvider::OneDrive => {
126 match input.option(|input| input.secret_text("share token"))? {
127 Some(token) => InvitationAcceptance::OneDriveShare { token },
128 None => InvitationAcceptance::Granted,
129 }
130 }
131 CloudProvider::CloudKit => {
132 InvitationAcceptance::CloudKitShare { url: input.secret_text("share URL")? }
133 }
134 };
135 Self::new(location, acceptance)
136 }
137 pub(crate) fn check(&self, location: &StorageConfig) -> Result<(), StorageError> {
138 if !self.location.same_location(location) {
139 return Err(StorageFailure::InvitationMismatch.into());
140 }
141 Ok(())
142 }
143}
144
145#[cfg(test)]
146#[path = "invitation_tests.rs"]
147mod tests;