Skip to main content

coven_storage/
provider_access.rs

1//! Registered and granted provider access, separate from provider locations.
2
3use crate::{CloudProvider, StorageError, StorageFailure};
4use serde::{Deserialize, Serialize};
5
6/// Dropbox fixes this access level on the app registration (ยง20.2).
7#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
8pub enum DropboxAccess {
9    /// Paths relative to the app's folder; sharing is unavailable.
10    AppFolder,
11    /// Full-account access, permitting a shared folder namespace.
12    FullDropbox,
13}
14
15/// The access requested by one sign-in and retained with its credentials.
16#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
17pub enum OAuthAccess {
18    /// The required choice for Dropbox creation; no default when both keys exist.
19    Dropbox(DropboxAccess),
20    /// OneDrive's configured file and account scopes.
21    OneDrive,
22}
23
24impl OAuthAccess {
25    /// The provider receiving this authorization request.
26    pub fn provider(self) -> CloudProvider {
27        match self {
28            Self::Dropbox(_) => CloudProvider::Dropbox,
29            Self::OneDrive => CloudProvider::OneDrive,
30        }
31    }
32
33    pub(crate) fn scope(self) -> &'static str {
34        match self {
35            Self::Dropbox(DropboxAccess::AppFolder) => "files.content.read files.content.write files.metadata.read account_info.read",
36            Self::Dropbox(DropboxAccess::FullDropbox) => "files.content.read files.content.write files.metadata.read sharing.read sharing.write account_info.read",
37            Self::OneDrive => "Files.ReadWrite.All offline_access User.Read",
38        }
39    }
40
41    /// Check a token reply against this access. `refreshable` says whether
42    /// the resulting tokens hold a refresh token, issued by this reply or kept
43    /// from the one it refreshed.
44    pub(crate) fn granted(
45        self,
46        scope: Option<&str>,
47        refreshable: bool,
48    ) -> Result<Self, StorageError> {
49        let refused = || Err(StorageFailure::AccessNotGranted { required: self }.into());
50        // Microsoft's reply lists only the Graph permissions the access token
51        // carries, never `offline_access`; that grant is the refresh token,
52        // "only provided if offline_access scope was requested"
53        // (learn.microsoft.com/entra/identity-platform/v2-oauth2-auth-code-flow).
54        if self == Self::OneDrive && !refreshable {
55            return refused();
56        }
57        // OAuth 2.0 defines omission as the requested scope, not an empty grant.
58        let Some(scope) = scope else { return Ok(self) };
59        let scopes: Vec<_> = scope
60            .split_ascii_whitespace()
61            .map(|granted| match self {
62                // An unqualified permission names Microsoft Graph (scopes-oidc).
63                Self::OneDrive => {
64                    granted.strip_prefix("https://graph.microsoft.com/").unwrap_or(granted)
65                }
66                Self::Dropbox(_) => granted,
67            })
68            .collect();
69        if self
70            .scope()
71            .split_ascii_whitespace()
72            .filter(|required| !(self == Self::OneDrive && *required == "offline_access"))
73            .all(|required| scopes.contains(&required))
74        {
75            Ok(self)
76        } else {
77            refused()
78        }
79    }
80}
81
82/// Dropbox's permanent creation choice, retained in settings and restore codes.
83#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
84#[serde(deny_unknown_fields)]
85pub enum DropboxLocation {
86    /// Not-shared: the app folder itself is the path root.
87    AppFolder,
88    /// Shared-capable: a namespace independent of the member's mount path.
89    SharedFolder {
90        /// The provider's shared folder namespace id.
91        namespace_id: String,
92    },
93}
94
95impl DropboxLocation {
96    /// The app registration required to reach this location.
97    pub fn access(&self) -> DropboxAccess {
98        match self {
99            Self::AppFolder => DropboxAccess::AppFolder,
100            Self::SharedFolder { .. } => DropboxAccess::FullDropbox,
101        }
102    }
103}
104
105#[cfg(test)]
106#[path = "provider_access_tests.rs"]
107mod tests;