bitwarden_pam/access_requests/
client.rs1use std::sync::Arc;
2
3use bitwarden_core::{FromClient, client::ApiConfigurations};
4use bitwarden_vault::CipherId;
5#[cfg(feature = "wasm")]
6use wasm_bindgen::prelude::wasm_bindgen;
7
8use super::models::{
9 AccessPreCheckView, AccessRequestCreateRequest, AccessRequestResultView, AccessRequestView,
10 CipherAccessStateView,
11};
12use crate::{AccessRequestId, error::LeasingError, leases::AccessLeaseView};
13
14#[cfg_attr(feature = "wasm", wasm_bindgen)]
23#[derive(FromClient)]
24pub struct AccessRequestsClient {
25 pub(crate) api_configurations: Arc<ApiConfigurations>,
26}
27
28#[cfg_attr(feature = "wasm", wasm_bindgen)]
29impl AccessRequestsClient {
30 pub async fn pre_check(&self, cipher_id: CipherId) -> Result<AccessPreCheckView, LeasingError> {
34 let response = self
35 .api_configurations
36 .api_client
37 .cipher_lease_api()
38 .pre_check(cipher_id.into())
39 .await?;
40
41 AccessPreCheckView::try_from(response)
42 }
43
44 pub async fn cipher_access_state(
47 &self,
48 cipher_id: CipherId,
49 ) -> Result<CipherAccessStateView, LeasingError> {
50 let response = self
51 .api_configurations
52 .api_client
53 .cipher_lease_api()
54 .state(cipher_id.into())
55 .await?;
56
57 CipherAccessStateView::try_from(response)
58 }
59
60 pub async fn request(
63 &self,
64 cipher_id: CipherId,
65 request: AccessRequestCreateRequest,
66 ) -> Result<AccessRequestResultView, LeasingError> {
67 let response = self
68 .api_configurations
69 .api_client
70 .cipher_lease_api()
71 .post(cipher_id.into(), request.into())
72 .await?;
73
74 AccessRequestResultView::try_from(response)
75 }
76
77 pub async fn list_mine(&self) -> Result<Vec<AccessRequestView>, LeasingError> {
79 let response = self
80 .api_configurations
81 .api_client
82 .access_requests_api()
83 .get_mine()
84 .await?;
85
86 response
87 .data
88 .unwrap_or_default()
89 .into_iter()
90 .map(AccessRequestView::try_from)
91 .collect()
92 }
93
94 pub async fn get(&self, id: AccessRequestId) -> Result<AccessRequestView, LeasingError> {
96 let response = self
97 .api_configurations
98 .api_client
99 .access_requests_api()
100 .get_details(id.into())
101 .await?;
102
103 AccessRequestView::try_from(response)
104 }
105
106 pub async fn activate(&self, id: AccessRequestId) -> Result<AccessLeaseView, LeasingError> {
112 let response = self
113 .api_configurations
114 .api_client
115 .access_requests_api()
116 .activate(id.into())
117 .await?;
118
119 AccessLeaseView::try_from(response)
120 }
121
122 pub async fn cancel(&self, id: AccessRequestId) -> Result<(), LeasingError> {
124 self.api_configurations
125 .api_client
126 .access_requests_api()
127 .revoke(id.into())
128 .await?;
129
130 Ok(())
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use std::num::NonZeroU32;
137
138 use bitwarden_api_api::{
139 apis::ApiClient,
140 models::{
141 AccessApprovalMode as ApiAccessApprovalMode, AccessLeaseResponseModel,
142 AccessLeaseStatus as ApiAccessLeaseStatus, AccessPreCheckResponseModel,
143 AccessRequestDetailsResponseModel, AccessRequestDetailsResponseModelListResponseModel,
144 AccessRequestResultResponseModel, AccessRequestStatus as ApiAccessRequestStatus,
145 CipherAccessStateResponseModel,
146 },
147 };
148 use uuid::uuid;
149
150 use super::*;
151 use crate::{AccessApprovalMode, AccessLeaseStatus, AccessRequestStatus};
152
153 fn request_id() -> AccessRequestId {
154 AccessRequestId::new(uuid!("44444444-4444-4444-4444-444444444444"))
155 }
156
157 fn cipher_id() -> CipherId {
158 CipherId::new(uuid!("55555555-5555-5555-5555-555555555555"))
159 }
160
161 fn client(api_client: ApiClient) -> AccessRequestsClient {
162 AccessRequestsClient {
163 api_configurations: Arc::new(ApiConfigurations::from_api_client(api_client)),
164 }
165 }
166
167 fn sample_request() -> AccessRequestDetailsResponseModel {
168 AccessRequestDetailsResponseModel {
169 id: Some(request_id().into()),
170 cipher_id: Some(uuid!("55555555-5555-5555-5555-555555555555")),
171 collection_id: Some(uuid!("66666666-6666-6666-6666-666666666666")),
172 requester_id: Some(uuid!("88888888-8888-8888-8888-888888888888")),
173 status: Some(ApiAccessRequestStatus::Approved),
174 lease_not_before: Some("2025-01-01T00:00:00Z".to_string()),
175 lease_not_after: Some("2025-01-01T01:00:00Z".to_string()),
176 submitted_at: Some("2025-01-01T00:00:00Z".to_string()),
177 ..Default::default()
178 }
179 }
180
181 fn sample_lease() -> AccessLeaseResponseModel {
182 AccessLeaseResponseModel {
183 id: Some(uuid!("33333333-3333-3333-3333-333333333333")),
184 request_id: Some(request_id().into()),
185 cipher_id: Some(uuid!("55555555-5555-5555-5555-555555555555")),
186 collection_id: Some(uuid!("66666666-6666-6666-6666-666666666666")),
187 requester_id: Some(uuid!("88888888-8888-8888-8888-888888888888")),
188 status: Some(ApiAccessLeaseStatus::Active),
189 not_before: Some("2025-01-01T00:00:00Z".to_string()),
190 not_after: Some("2025-01-01T01:00:00Z".to_string()),
191 ..Default::default()
192 }
193 }
194
195 #[tokio::test]
196 async fn list_mine_returns_views() {
197 let api_client = ApiClient::new_mocked(move |mock| {
198 mock.access_requests_api
199 .expect_get_mine()
200 .returning(move || {
201 let mut list = AccessRequestDetailsResponseModelListResponseModel::new();
202 list.data = Some(vec![sample_request()]);
203 Ok(list)
204 })
205 .once();
206 });
207
208 let result = client(api_client).list_mine().await.unwrap();
209
210 assert_eq!(result.len(), 1);
211 assert_eq!(result[0].id, request_id());
212 assert_eq!(result[0].status, AccessRequestStatus::Approved);
213 }
214
215 #[tokio::test]
216 async fn get_returns_view() {
217 let api_client = ApiClient::new_mocked(move |mock| {
218 mock.access_requests_api
219 .expect_get_details()
220 .returning(move |_id| Ok(sample_request()))
221 .once();
222 });
223
224 let result = client(api_client).get(request_id()).await.unwrap();
225
226 assert_eq!(result.id, request_id());
227 }
228
229 #[tokio::test]
230 async fn activate_mints_a_lease() {
231 let api_client = ApiClient::new_mocked(move |mock| {
232 mock.access_requests_api
233 .expect_activate()
234 .returning(move |_id| Ok(sample_lease()))
235 .once();
236 });
237
238 let lease = client(api_client).activate(request_id()).await.unwrap();
239
240 assert_eq!(lease.request_id, request_id());
241 assert_eq!(lease.status, AccessLeaseStatus::Active);
242 }
243
244 #[tokio::test]
245 async fn activate_surfaces_api_error() {
246 let api_client = ApiClient::new_mocked(move |mock| {
247 mock.access_requests_api
248 .expect_activate()
249 .returning(move |_id| {
250 Err(bitwarden_api_api::apis::Error::Response(
251 bitwarden_api_api::apis::ResponseContent {
252 status: reqwest::StatusCode::CONFLICT,
253 message: "Not approved".to_string(),
254 },
255 ))
256 })
257 .once();
258 });
259
260 let result = client(api_client).activate(request_id()).await;
261
262 assert!(matches!(result, Err(LeasingError::Api(_))));
263 }
264
265 #[tokio::test]
266 async fn cancel_succeeds() {
267 let api_client = ApiClient::new_mocked(move |mock| {
268 mock.access_requests_api
269 .expect_revoke()
270 .returning(move |_id| Ok(()))
271 .once();
272 });
273
274 let result = client(api_client).cancel(request_id()).await;
275
276 assert!(result.is_ok());
277 }
278
279 #[tokio::test]
280 async fn pre_check_returns_view() {
281 let api_client = ApiClient::new_mocked(move |mock| {
282 mock.cipher_lease_api
283 .expect_pre_check()
284 .returning(move |_id| {
285 Ok(AccessPreCheckResponseModel {
286 cipher_id: Some(cipher_id().into()),
287 approval_mode: Some(ApiAccessApprovalMode::Automatic),
288 has_active_lease: Some(false),
289 ..Default::default()
290 })
291 })
292 .once();
293 });
294
295 let result = client(api_client).pre_check(cipher_id()).await.unwrap();
296
297 assert_eq!(result.approval_mode, AccessApprovalMode::Automatic);
298 assert!(!result.has_active_lease);
299 }
300
301 #[tokio::test]
302 async fn pre_check_surfaces_api_error() {
303 let api_client = ApiClient::new_mocked(move |mock| {
304 mock.cipher_lease_api
305 .expect_pre_check()
306 .returning(move |_id| {
307 Err(bitwarden_api_api::apis::Error::Response(
308 bitwarden_api_api::apis::ResponseContent {
309 status: reqwest::StatusCode::NOT_FOUND,
310 message: "Not found".to_string(),
311 },
312 ))
313 })
314 .once();
315 });
316
317 let result = client(api_client).pre_check(cipher_id()).await;
318
319 assert!(matches!(result, Err(LeasingError::Api(_))));
320 }
321
322 #[tokio::test]
323 async fn cipher_access_state_returns_view() {
324 let api_client = ApiClient::new_mocked(move |mock| {
325 mock.cipher_lease_api
326 .expect_state()
327 .returning(move |_id| {
328 Ok(CipherAccessStateResponseModel {
329 cipher_id: Some(cipher_id().into()),
330 active_lease: None,
331 pending_request: None,
332 approved_request: None,
333 extensions_allowed: Some(true),
334 max_extension_duration_seconds: Some(1800),
335 ..Default::default()
336 })
337 })
338 .once();
339 });
340
341 let result = client(api_client)
342 .cipher_access_state(cipher_id())
343 .await
344 .unwrap();
345
346 assert_eq!(result.active_lease, None);
347 assert!(result.extensions_allowed);
348 assert_eq!(result.max_extension_duration_seconds, Some(1800));
349 }
350
351 #[tokio::test]
352 async fn request_submits_and_returns_result() {
353 let api_client = ApiClient::new_mocked(move |mock| {
354 mock.cipher_lease_api
355 .expect_post()
356 .returning(move |_id, _request| {
357 Ok(AccessRequestResultResponseModel {
358 approval_mode: Some(ApiAccessApprovalMode::Automatic),
359 request: Some(Box::new(AccessRequestDetailsResponseModel {
360 id: Some(request_id().into()),
361 cipher_id: Some(cipher_id().into()),
362 collection_id: Some(uuid!("66666666-6666-6666-6666-666666666666")),
363 status: Some(ApiAccessRequestStatus::Approved),
364 lease_not_before: Some("2025-01-01T00:00:00Z".to_string()),
365 lease_not_after: Some("2025-01-01T01:00:00Z".to_string()),
366 submitted_at: Some("2025-01-01T00:00:00Z".to_string()),
367 ..Default::default()
368 })),
369 ..Default::default()
370 })
371 })
372 .once();
373 });
374
375 let request = AccessRequestCreateRequest {
376 duration_seconds: NonZeroU32::new(3600),
377 ..Default::default()
378 };
379
380 let result = client(api_client)
381 .request(cipher_id(), request)
382 .await
383 .unwrap();
384
385 assert_eq!(result.approval_mode, AccessApprovalMode::Automatic);
386 assert_eq!(result.request.id, request_id());
387 }
388
389 #[tokio::test]
390 async fn request_surfaces_api_error() {
391 let api_client = ApiClient::new_mocked(move |mock| {
392 mock.cipher_lease_api
393 .expect_post()
394 .returning(move |_id, _request| {
395 Err(bitwarden_api_api::apis::Error::Response(
396 bitwarden_api_api::apis::ResponseContent {
397 status: reqwest::StatusCode::BAD_REQUEST,
398 message: "Invalid window".to_string(),
399 },
400 ))
401 })
402 .once();
403 });
404
405 let result = client(api_client)
406 .request(cipher_id(), AccessRequestCreateRequest::default())
407 .await;
408
409 assert!(matches!(result, Err(LeasingError::Api(_))));
410 }
411}