bitwarden_pam/leases/
client.rs1use std::sync::Arc;
2
3use bitwarden_core::{FromClient, client::ApiConfigurations};
4#[cfg(feature = "wasm")]
5use wasm_bindgen::prelude::wasm_bindgen;
6
7use super::models::{AccessLeaseExtensionRequest, AccessLeaseRevokeRequest, AccessLeaseView};
8use crate::{AccessLeaseId, access_requests::AccessRequestView, error::LeasingError};
9
10#[cfg_attr(feature = "wasm", wasm_bindgen)]
16#[derive(FromClient)]
17pub struct LeasesClient {
18 pub(crate) api_configurations: Arc<ApiConfigurations>,
19}
20
21#[cfg_attr(feature = "wasm", wasm_bindgen)]
22impl LeasesClient {
23 pub async fn list_active(&self) -> Result<Vec<AccessLeaseView>, LeasingError> {
25 let response = self
26 .api_configurations
27 .api_client
28 .leases_api()
29 .get_active()
30 .await?;
31
32 response
33 .data
34 .unwrap_or_default()
35 .into_iter()
36 .map(AccessLeaseView::try_from)
37 .collect()
38 }
39
40 pub async fn list_mine(&self) -> Result<Vec<AccessLeaseView>, LeasingError> {
42 let response = self
43 .api_configurations
44 .api_client
45 .leases_api()
46 .get_mine()
47 .await?;
48
49 response
50 .data
51 .unwrap_or_default()
52 .into_iter()
53 .map(AccessLeaseView::try_from)
54 .collect()
55 }
56
57 pub async fn extend(
59 &self,
60 lease_id: AccessLeaseId,
61 request: AccessLeaseExtensionRequest,
62 ) -> Result<AccessRequestView, LeasingError> {
63 let response = self
64 .api_configurations
65 .api_client
66 .leases_api()
67 .extend(lease_id.into(), request.into())
68 .await?;
69
70 AccessRequestView::try_from(response)
71 }
72
73 pub async fn end(
75 &self,
76 lease_id: AccessLeaseId,
77 request: AccessLeaseRevokeRequest,
78 ) -> Result<(), LeasingError> {
79 self.api_configurations
80 .api_client
81 .leases_api()
82 .revoke(lease_id.into(), request.into())
83 .await?;
84
85 Ok(())
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use std::num::NonZeroU32;
92
93 use bitwarden_api_api::{
94 apis::ApiClient,
95 models::{
96 AccessLeaseResponseModel, AccessLeaseResponseModelListResponseModel,
97 AccessLeaseStatus as ApiAccessLeaseStatus, AccessRequestDetailsResponseModel,
98 },
99 };
100 use chrono::{DateTime, Utc};
101 use uuid::uuid;
102
103 use super::*;
104 use crate::leases::models::AccessLeaseStatus;
105
106 fn lease_id() -> AccessLeaseId {
107 AccessLeaseId::new(uuid!("33333333-3333-3333-3333-333333333333"))
108 }
109
110 fn client(api_client: ApiClient) -> LeasesClient {
111 LeasesClient {
112 api_configurations: Arc::new(ApiConfigurations::from_api_client(api_client)),
113 }
114 }
115
116 fn sample_lease() -> AccessLeaseResponseModel {
117 AccessLeaseResponseModel {
118 id: Some(lease_id().into()),
119 request_id: Some(uuid!("44444444-4444-4444-4444-444444444444")),
120 cipher_id: Some(uuid!("55555555-5555-5555-5555-555555555555")),
121 collection_id: Some(uuid!("66666666-6666-6666-6666-666666666666")),
122 organization_id: Some(uuid!("77777777-7777-7777-7777-777777777777")),
123 requester_id: Some(uuid!("88888888-8888-8888-8888-888888888888")),
124 status: Some(ApiAccessLeaseStatus::Active),
125 not_before: Some("2025-01-01T00:00:00Z".to_string()),
126 not_after: Some("2025-01-01T01:00:00Z".to_string()),
127 ..Default::default()
128 }
129 }
130
131 #[tokio::test]
132 async fn list_active_returns_views() {
133 let api_client = ApiClient::new_mocked(move |mock| {
134 mock.leases_api
135 .expect_get_active()
136 .returning(move || {
137 let mut list = AccessLeaseResponseModelListResponseModel::new();
138 list.data = Some(vec![sample_lease()]);
139 Ok(list)
140 })
141 .once();
142 });
143
144 let result = client(api_client).list_active().await.unwrap();
145
146 assert_eq!(result.len(), 1);
147 assert_eq!(result[0].id, lease_id());
148 assert_eq!(result[0].status, AccessLeaseStatus::Active);
149 }
150
151 #[tokio::test]
152 async fn list_mine_surfaces_api_error() {
153 let api_client = ApiClient::new_mocked(move |mock| {
154 mock.leases_api
155 .expect_get_mine()
156 .returning(move || {
157 Err(bitwarden_api_api::apis::Error::Response(
158 bitwarden_api_api::apis::ResponseContent {
159 status: reqwest::StatusCode::INTERNAL_SERVER_ERROR,
160 message: String::new(),
161 },
162 ))
163 })
164 .once();
165 });
166
167 let result = client(api_client).list_mine().await;
168
169 assert!(matches!(result, Err(LeasingError::Api(_))));
170 }
171
172 #[tokio::test]
173 async fn extend_returns_updated_request() {
174 let api_client = ApiClient::new_mocked(move |mock| {
175 mock.leases_api
176 .expect_extend()
177 .returning(move |_id, _request| {
178 Ok(AccessRequestDetailsResponseModel {
179 id: Some(uuid!("44444444-4444-4444-4444-444444444444")),
180 cipher_id: Some(uuid!("55555555-5555-5555-5555-555555555555")),
181 collection_id: Some(uuid!("66666666-6666-6666-6666-666666666666")),
182 requester_id: Some(uuid!("88888888-8888-8888-8888-888888888888")),
183 status: Some(bitwarden_api_api::models::AccessRequestStatus::Activated),
184 lease_not_before: Some("2025-01-01T00:00:00Z".to_string()),
185 lease_not_after: Some("2025-01-01T02:00:00Z".to_string()),
186 submitted_at: Some("2025-01-01T00:00:00Z".to_string()),
187 ..Default::default()
188 })
189 })
190 .once();
191 });
192
193 let request = AccessLeaseExtensionRequest {
194 duration_seconds: NonZeroU32::new(3600),
195 reason: "Need more time".to_string(),
196 };
197 let result = client(api_client)
198 .extend(lease_id(), request)
199 .await
200 .unwrap();
201
202 assert_eq!(
203 result.lease_not_after,
204 "2025-01-01T02:00:00Z".parse::<DateTime<Utc>>().unwrap()
205 );
206 }
207
208 #[tokio::test]
209 async fn end_succeeds() {
210 let api_client = ApiClient::new_mocked(move |mock| {
211 mock.leases_api
212 .expect_revoke()
213 .returning(move |_id, _request| Ok(()))
214 .once();
215 });
216
217 let result = client(api_client)
218 .end(lease_id(), AccessLeaseRevokeRequest::default())
219 .await;
220
221 assert!(result.is_ok());
222 }
223}