Skip to main content

bitwarden_pam/leases/
client.rs

1use 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/// Client for reading and managing a requester's PAM access leases.
11///
12/// A lease is minted by [`AccessRequestsClient::activate`](crate::AccessRequestsClient::activate);
13/// this client covers the rest of a lease's life: listing the caller's leases, extending an active
14/// one, and ending one early.
15#[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    /// Lists the caller's currently active leases.
24    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    /// Lists all of the caller's leases, active or not.
41    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    /// Extends an active lease, returning the updated originating request.
58    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    /// Ends a lease before it expires, re-locking the cipher.
74    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}