omni_dev/drive/
permissions_api.rs1use anyhow::Result;
9use serde::Deserialize;
10use url::Url;
11
12use crate::drive::client::DriveClient;
13use crate::drive::types::DrivePermission;
14
15const MAX_PERMISSIONS: usize = 10_000;
26
27const LIST_FIELDS: &str = "nextPageToken,permissions(id,type,role,emailAddress,domain)";
31
32#[derive(Debug)]
34pub struct PermissionsApi<'a> {
35 client: &'a DriveClient,
36}
37
38impl<'a> PermissionsApi<'a> {
39 #[must_use]
41 pub fn new(client: &'a DriveClient) -> Self {
42 Self { client }
43 }
44
45 pub async fn list_all(&self, file_or_folder_id: &str) -> Result<Vec<DrivePermission>> {
50 let mut acc: Vec<DrivePermission> = Vec::new();
51 let mut page_token: Option<String> = None;
52 loop {
53 let page = self
54 .list_page(file_or_folder_id, page_token.as_deref())
55 .await?;
56 acc.extend(page.permissions);
57 if acc.len() >= MAX_PERMISSIONS || page.next_page_token.is_none() {
58 break;
59 }
60 page_token = page.next_page_token;
61 }
62 acc.truncate(MAX_PERMISSIONS);
63 Ok(acc)
64 }
65
66 async fn list_page(
67 &self,
68 file_or_folder_id: &str,
69 page_token: Option<&str>,
70 ) -> Result<PermissionListResponse> {
71 let url =
72 build_permissions_list_url(self.client.base_url(), file_or_folder_id, page_token)?;
73 self.client
74 .get_parsed(url.as_str(), "Failed to parse permissions.list response")
75 .await
76 }
77}
78
79#[derive(Debug, Clone, Default, Deserialize)]
81struct PermissionListResponse {
82 #[serde(default)]
83 permissions: Vec<DrivePermission>,
84 #[serde(default, rename = "nextPageToken")]
85 next_page_token: Option<String>,
86}
87
88fn build_permissions_list_url(
93 base_url: &str,
94 file_or_folder_id: &str,
95 page_token: Option<&str>,
96) -> Result<Url> {
97 let mut url = DriveClient::api_url(
98 base_url,
99 &format!("/drive/v3/files/{file_or_folder_id}/permissions"),
100 )?;
101 {
102 let mut pairs = url.query_pairs_mut();
103 pairs.append_pair("fields", LIST_FIELDS);
104 pairs.append_pair("supportsAllDrives", "true");
105 if let Some(token) = page_token {
106 pairs.append_pair("pageToken", token);
107 }
108 }
109 Ok(url)
110}
111
112#[cfg(test)]
113#[allow(clippy::unwrap_used, clippy::expect_used)]
114mod tests {
115 use super::*;
116 use crate::drive::auth::{DriveCredentials, DriveScope};
117 use crate::utils::secret::Secret;
118
119 fn test_credentials() -> DriveCredentials {
120 DriveCredentials {
121 client_id: "client-1".to_string(),
122 client_secret: Secret::new("secret-1"),
123 refresh_token: Secret::new("refresh-1"),
124 scope: DriveScope::Metadata,
125 }
126 }
127
128 async fn client_with_bootstrapped_token(server: &wiremock::MockServer) -> DriveClient {
129 wiremock::Mock::given(wiremock::matchers::method("POST"))
130 .and(wiremock::matchers::path("/token"))
131 .respond_with(
132 wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
133 "access_token": "test-token",
134 "expires_in": 3600,
135 })),
136 )
137 .mount(server)
138 .await;
139
140 let mut client = DriveClient::new(&server.uri(), &test_credentials()).unwrap();
141 crate::drive::client::test_support::replace_session(
142 &mut client,
143 &test_credentials(),
144 &format!("{}/token", server.uri()),
145 );
146 client
147 }
148
149 #[tokio::test]
150 async fn list_all_returns_a_single_page_verbatim() {
151 let server = wiremock::MockServer::start().await;
152 let client = client_with_bootstrapped_token(&server).await;
153 wiremock::Mock::given(wiremock::matchers::method("GET"))
154 .and(wiremock::matchers::path("/drive/v3/files/f1/permissions"))
155 .and(wiremock::matchers::query_param("fields", LIST_FIELDS))
156 .respond_with(
157 wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
158 "permissions": [
159 {"id": "p1", "type": "user", "role": "reader", "emailAddress": "alice@example.com"},
160 ],
161 })),
162 )
163 .expect(1)
164 .mount(&server)
165 .await;
166
167 let perms = PermissionsApi::new(&client).list_all("f1").await.unwrap();
168 assert_eq!(perms.len(), 1);
169 assert_eq!(perms[0].email_address.as_deref(), Some("alice@example.com"));
170 }
171
172 #[tokio::test]
173 async fn list_all_follows_next_page_token_to_exhaustion() {
174 let server = wiremock::MockServer::start().await;
175 let client = client_with_bootstrapped_token(&server).await;
176
177 wiremock::Mock::given(wiremock::matchers::method("GET"))
178 .and(wiremock::matchers::path("/drive/v3/files/f1/permissions"))
179 .and(wiremock::matchers::query_param_is_missing("pageToken"))
180 .respond_with(
181 wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
182 "permissions": [
183 {"id": "p1", "type": "user", "role": "reader", "emailAddress": "alice@example.com"},
184 ],
185 "nextPageToken": "page-2",
186 })),
187 )
188 .expect(1)
189 .mount(&server)
190 .await;
191 wiremock::Mock::given(wiremock::matchers::method("GET"))
192 .and(wiremock::matchers::path("/drive/v3/files/f1/permissions"))
193 .and(wiremock::matchers::query_param("pageToken", "page-2"))
194 .respond_with(
195 wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
196 "permissions": [
197 {"id": "p2", "type": "user", "role": "reader", "emailAddress": "bob@example.com"},
198 ],
199 })),
200 )
201 .expect(1)
202 .mount(&server)
203 .await;
204
205 let perms = PermissionsApi::new(&client).list_all("f1").await.unwrap();
206 assert_eq!(perms.len(), 2);
207 assert_eq!(perms[0].email_address.as_deref(), Some("alice@example.com"));
208 assert_eq!(perms[1].email_address.as_deref(), Some("bob@example.com"));
209 }
210
211 #[tokio::test]
212 async fn list_all_returns_empty_for_a_permission_less_response() {
213 let server = wiremock::MockServer::start().await;
214 let client = client_with_bootstrapped_token(&server).await;
215 wiremock::Mock::given(wiremock::matchers::method("GET"))
216 .and(wiremock::matchers::path("/drive/v3/files/f1/permissions"))
217 .respond_with(wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({})))
218 .mount(&server)
219 .await;
220
221 let perms = PermissionsApi::new(&client).list_all("f1").await.unwrap();
222 assert!(perms.is_empty());
223 }
224
225 #[tokio::test]
226 async fn list_all_propagates_api_errors() {
227 let server = wiremock::MockServer::start().await;
228 let client = client_with_bootstrapped_token(&server).await;
229 wiremock::Mock::given(wiremock::matchers::method("GET"))
230 .and(wiremock::matchers::path(
231 "/drive/v3/files/missing/permissions",
232 ))
233 .respond_with(wiremock::ResponseTemplate::new(404).set_body_string("not found"))
234 .mount(&server)
235 .await;
236
237 let err = PermissionsApi::new(&client)
238 .list_all("missing")
239 .await
240 .unwrap_err();
241 assert!(err.to_string().contains("404"));
242 }
243
244 #[tokio::test]
245 async fn list_all_works_for_a_folder_id_the_same_as_a_file_id() {
246 let server = wiremock::MockServer::start().await;
247 let client = client_with_bootstrapped_token(&server).await;
248 wiremock::Mock::given(wiremock::matchers::method("GET"))
249 .and(wiremock::matchers::path(
250 "/drive/v3/files/folder1/permissions",
251 ))
252 .respond_with(
253 wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
254 "permissions": [
255 {"id": "p1", "type": "domain", "role": "reader", "domain": "example.com"},
256 ],
257 })),
258 )
259 .mount(&server)
260 .await;
261
262 let perms = PermissionsApi::new(&client)
263 .list_all("folder1")
264 .await
265 .unwrap();
266 assert_eq!(perms[0].domain.as_deref(), Some("example.com"));
267 }
268}