Skip to main content

coven_storage/cloud/
setup.rs

1//! Cloud provider setup and management.
2//!
3//! Contains the OAuth sign-in flows for Google Drive, Dropbox, and OneDrive,
4//! as well as managed service signup/login, disconnect, and account display logic.
5
6use coven_foundation::config::{CloudProvider, Config};
7
8/// Why building the sync storage from config failed. Each arm preserves the
9/// typed error the layer below produced — notably
10/// [`CloudHomeError`](super::CloudHomeError), so the
11/// [`is_retryable`](super::CloudHomeError::is_retryable) verdict survives up to
12/// the caller rather than being flattened into a string here.
13#[derive(Debug, thiserror::Error)]
14pub enum StorageSetupError {
15    #[error("failed to build cloud home: {0}")]
16    CloudHome(#[from] super::CloudHomeError),
17    #[error("key error: {0}")]
18    Key(#[from] coven_keys::keys::KeyError),
19    #[error("no encryption key found for an encrypted cloud home")]
20    NoEncryptionKey,
21    #[error("{provider:?} cannot provide exact protocol and blob slots with this configuration")]
22    ExactSlotsUnavailable { provider: CloudProvider },
23}
24
25pub fn require_exact_slot_capabilities_config(config: &Config) -> Result<(), StorageSetupError> {
26    let provider = config.cloud_home.provider.clone().ok_or_else(|| {
27        super::CloudHomeError::Configuration(
28            "sync requires a cloud provider with exact-slot storage".to_string(),
29        )
30    })?;
31    if exact_slot_capabilities_supported(&provider, config.cloud_home.exact_upload_verification) {
32        Ok(())
33    } else {
34        Err(StorageSetupError::ExactSlotsUnavailable { provider })
35    }
36}
37
38pub fn require_exact_slot_capabilities_join_info(
39    join_info: &crate::cloud::CloudHomeJoinInfo,
40    verification: coven_foundation::config::ExactUploadVerification,
41) -> Result<(), CloudProvider> {
42    let provider = join_info.cloud_provider();
43    if exact_slot_capabilities_supported(&provider, verification) {
44        Ok(())
45    } else {
46        Err(provider)
47    }
48}
49
50fn exact_slot_capabilities_supported(
51    provider: &CloudProvider,
52    verification: coven_foundation::config::ExactUploadVerification,
53) -> bool {
54    !matches!(
55        verification,
56        coven_foundation::config::ExactUploadVerification::UploadChecksum
57    ) || matches!(provider, CloudProvider::S3)
58}
59
60/// Cloud provider setup error.
61#[derive(Debug, thiserror::Error)]
62pub enum SetupError {
63    #[error("cloud setup configuration is invalid: {0}")]
64    Configuration(String),
65    #[cfg(feature = "oauth-providers")]
66    #[error("{provider} authorization failed: {source}")]
67    Authorization {
68        provider: &'static str,
69        #[source]
70        source: crate::oauth::OAuthError,
71    },
72    #[cfg(feature = "oauth-providers")]
73    #[error("{operation}: {source}")]
74    Http {
75        operation: &'static str,
76        #[source]
77        source: reqwest::Error,
78    },
79    #[cfg(feature = "oauth-providers")]
80    #[error("{operation} failed with HTTP {status}: {body}")]
81    ProviderResponse {
82        operation: &'static str,
83        status: reqwest::StatusCode,
84        body: String,
85    },
86    #[error("cloud setup key access failed: {0}")]
87    Key(#[from] coven_keys::keys::KeyError),
88}
89
90#[cfg(feature = "oauth-providers")]
91impl SetupError {
92    pub(crate) fn authorization(provider: &'static str, source: crate::oauth::OAuthError) -> Self {
93        Self::Authorization { provider, source }
94    }
95
96    pub(crate) fn http(operation: &'static str, source: reqwest::Error) -> Self {
97        Self::Http { operation, source }
98    }
99
100    pub(crate) fn provider_response(
101        operation: &'static str,
102        status: reqwest::StatusCode,
103        body: String,
104    ) -> Self {
105        Self::ProviderResponse {
106            operation,
107            status,
108            body,
109        }
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116    use crate::cloud::CloudHomeJoinInfo;
117
118    #[test]
119    fn exact_slot_admission_rejects_upload_checksums_where_the_provider_cannot_enforce_them() {
120        let mut config = Config::with_defaults(
121            "store-1".to_string(),
122            "device-1".to_string(),
123            "Provider matrix".to_string(),
124        );
125
126        config.cloud_home.provider = Some(CloudProvider::S3);
127        config.cloud_home.s3_endpoint = Some("https://objects.example".to_string());
128        config.cloud_home.exact_upload_verification =
129            coven_foundation::config::ExactUploadVerification::UploadChecksum;
130        assert!(require_exact_slot_capabilities_config(&config).is_ok());
131
132        for provider in [
133            CloudProvider::GoogleDrive,
134            CloudProvider::Dropbox,
135            CloudProvider::OneDrive,
136            CloudProvider::CloudKit,
137        ] {
138            config.cloud_home.provider = Some(provider.clone());
139            assert!(matches!(
140                require_exact_slot_capabilities_config(&config),
141                Err(StorageSetupError::ExactSlotsUnavailable { provider: rejected })
142                    if rejected == provider
143            ));
144            config.cloud_home.exact_upload_verification =
145                coven_foundation::config::ExactUploadVerification::MetadataHash;
146            assert!(require_exact_slot_capabilities_config(&config).is_ok());
147            config.cloud_home.exact_upload_verification =
148                coven_foundation::config::ExactUploadVerification::UploadChecksum;
149        }
150    }
151
152    #[test]
153    fn join_admission_uses_the_local_verification_policy() {
154        let join_info = CloudHomeJoinInfo::S3 {
155            bucket: "bucket".to_string(),
156            region: "region".to_string(),
157            endpoint: Some("https://objects.example".to_string()),
158            access_key: "access".to_string(),
159            secret_key: "secret".to_string(),
160            key_prefix: None,
161        };
162
163        assert!(require_exact_slot_capabilities_join_info(
164            &join_info,
165            coven_foundation::config::ExactUploadVerification::UploadChecksum,
166        )
167        .is_ok());
168    }
169}