Skip to main content

bitwarden_pam/access_rules/
client.rs

1use std::sync::Arc;
2
3use bitwarden_core::{ApiError, FromClient, OrganizationId, client::ApiConfigurations};
4#[cfg(feature = "wasm")]
5use wasm_bindgen::prelude::wasm_bindgen;
6
7use super::{
8    error::AccessRuleError,
9    models::{AccessRuleAddEditRequest, AccessRuleView},
10    validate::validate_request,
11};
12use crate::AccessRuleId;
13
14/// Client for PAM access rule CRUD operations.
15#[cfg_attr(feature = "wasm", wasm_bindgen)]
16#[derive(FromClient)]
17pub struct AccessRulesClient {
18    pub(crate) api_configurations: Arc<ApiConfigurations>,
19}
20
21#[cfg_attr(feature = "wasm", wasm_bindgen)]
22impl AccessRulesClient {
23    /// Lists all access rules for an organization.
24    pub async fn list(
25        &self,
26        organization_id: OrganizationId,
27    ) -> Result<Vec<AccessRuleView>, AccessRuleError> {
28        let response = self
29            .api_configurations
30            .api_client
31            .access_rules_api()
32            .get_all(organization_id.into())
33            .await
34            .map_err(ApiError::from)?;
35
36        response
37            .data
38            .unwrap_or_default()
39            .into_iter()
40            .map(AccessRuleView::try_from)
41            .collect()
42    }
43
44    /// Retrieves a single access rule by ID.
45    pub async fn get(
46        &self,
47        organization_id: OrganizationId,
48        id: AccessRuleId,
49    ) -> Result<AccessRuleView, AccessRuleError> {
50        let response = self
51            .api_configurations
52            .api_client
53            .access_rules_api()
54            .get(organization_id.into(), id.into())
55            .await
56            .map_err(ApiError::from)?;
57
58        AccessRuleView::try_from(response)
59    }
60
61    /// Validates and creates a new access rule.
62    pub async fn create(
63        &self,
64        organization_id: OrganizationId,
65        request: AccessRuleAddEditRequest,
66    ) -> Result<AccessRuleView, AccessRuleError> {
67        validate_request(&request)?;
68
69        let response = self
70            .api_configurations
71            .api_client
72            .access_rules_api()
73            .post(organization_id.into(), request.try_into()?)
74            .await
75            .map_err(ApiError::from)?;
76
77        AccessRuleView::try_from(response)
78    }
79
80    /// Validates and updates an existing access rule.
81    pub async fn update(
82        &self,
83        organization_id: OrganizationId,
84        id: AccessRuleId,
85        request: AccessRuleAddEditRequest,
86    ) -> Result<AccessRuleView, AccessRuleError> {
87        validate_request(&request)?;
88
89        let response = self
90            .api_configurations
91            .api_client
92            .access_rules_api()
93            .put(organization_id.into(), id.into(), request.try_into()?)
94            .await
95            .map_err(ApiError::from)?;
96
97        AccessRuleView::try_from(response)
98    }
99
100    /// Deletes an access rule.
101    pub async fn delete(
102        &self,
103        organization_id: OrganizationId,
104        id: AccessRuleId,
105    ) -> Result<(), AccessRuleError> {
106        self.api_configurations
107            .api_client
108            .access_rules_api()
109            .delete(organization_id.into(), id.into())
110            .await
111            .map_err(ApiError::from)?;
112
113        Ok(())
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use bitwarden_api_api::{apis::ApiClient, models::AccessRuleResponseModel};
120    use uuid::uuid;
121
122    use super::*;
123    use crate::AccessCondition;
124
125    fn org_id() -> OrganizationId {
126        OrganizationId::new(uuid!("11111111-1111-1111-1111-111111111111"))
127    }
128
129    fn rule_id() -> AccessRuleId {
130        AccessRuleId::new(uuid!("22222222-2222-2222-2222-222222222222"))
131    }
132
133    fn client(api_client: ApiClient) -> AccessRulesClient {
134        AccessRulesClient {
135            api_configurations: Arc::new(ApiConfigurations::from_api_client(api_client)),
136        }
137    }
138
139    fn sample_response(id: uuid::Uuid, organization_id: uuid::Uuid) -> AccessRuleResponseModel {
140        let mut response = AccessRuleResponseModel::new();
141        response.id = Some(id);
142        response.organization_id = Some(organization_id);
143        response.name = Some("My rule".to_string());
144        response.conditions = Some(serde_json::json!([]));
145        response.creation_date = Some("2025-01-01T00:00:00Z".to_string());
146        response.revision_date = Some("2025-01-01T00:00:00Z".to_string());
147        response
148    }
149
150    fn sample_request() -> AccessRuleAddEditRequest {
151        AccessRuleAddEditRequest {
152            name: "My rule".to_string(),
153            description: None,
154            enabled: true,
155            conditions: vec![AccessCondition::HumanApproval],
156            single_active_lease: false,
157            default_lease_duration_seconds: None,
158            max_lease_duration_seconds: None,
159            allows_extensions: false,
160            max_extension_duration_seconds: None,
161            collections: Vec::new(),
162        }
163    }
164
165    #[tokio::test]
166    async fn list_returns_views() {
167        let organization_id = org_id();
168        let rule = rule_id();
169        let response = sample_response(rule.into(), organization_id.into());
170
171        let api_client = ApiClient::new_mocked(move |mock| {
172            mock.access_rules_api
173                .expect_get_all()
174                .returning(move |_org_id| {
175                    let mut list_response =
176                        bitwarden_api_api::models::AccessRuleResponseModelListResponseModel::new();
177                    list_response.data = Some(vec![response.clone()]);
178                    Ok(list_response)
179                })
180                .once();
181        });
182
183        let result = client(api_client).list(organization_id).await.unwrap();
184
185        assert_eq!(result.len(), 1);
186        assert_eq!(result[0].id, rule);
187    }
188
189    #[tokio::test]
190    async fn get_returns_view() {
191        let organization_id = org_id();
192        let rule = rule_id();
193        let response = sample_response(rule.into(), organization_id.into());
194
195        let api_client = ApiClient::new_mocked(move |mock| {
196            mock.access_rules_api
197                .expect_get()
198                .returning(move |_org_id, _id| Ok(response.clone()))
199                .once();
200        });
201
202        let result = client(api_client).get(organization_id, rule).await.unwrap();
203
204        assert_eq!(result.id, rule);
205    }
206
207    #[tokio::test]
208    async fn get_surfaces_api_error() {
209        let organization_id = org_id();
210        let rule = rule_id();
211
212        let api_client = ApiClient::new_mocked(move |mock| {
213            mock.access_rules_api
214                .expect_get()
215                .returning(move |_org_id, _id| {
216                    Err(bitwarden_api_api::apis::Error::Response(
217                        bitwarden_api_api::apis::ResponseContent {
218                            status: reqwest::StatusCode::NOT_FOUND,
219                            message: String::new(),
220                        },
221                    ))
222                })
223                .once();
224        });
225
226        let result = client(api_client).get(organization_id, rule).await;
227
228        assert!(matches!(result, Err(AccessRuleError::Api(_))));
229    }
230
231    #[tokio::test]
232    async fn create_rejects_invalid_request_without_calling_the_api() {
233        let organization_id = org_id();
234        let mut request = sample_request();
235        request.name = String::new();
236
237        let api_client = ApiClient::new_mocked(|mock| {
238            mock.access_rules_api.expect_post().never();
239        });
240
241        let result = client(api_client).create(organization_id, request).await;
242
243        assert!(matches!(result, Err(AccessRuleError::Validation(_))));
244    }
245
246    #[tokio::test]
247    async fn create_returns_created_view() {
248        let organization_id = org_id();
249        let rule = rule_id();
250        let response = sample_response(rule.into(), organization_id.into());
251
252        let api_client = ApiClient::new_mocked(move |mock| {
253            mock.access_rules_api
254                .expect_post()
255                .returning(move |_org_id, _request| Ok(response.clone()))
256                .once();
257        });
258
259        let result = client(api_client)
260            .create(organization_id, sample_request())
261            .await
262            .unwrap();
263
264        assert_eq!(result.id, rule);
265    }
266
267    #[tokio::test]
268    async fn create_surfaces_api_error() {
269        let organization_id = org_id();
270
271        let api_client = ApiClient::new_mocked(move |mock| {
272            mock.access_rules_api
273                .expect_post()
274                .returning(move |_org_id, _request| {
275                    Err(bitwarden_api_api::apis::Error::Response(
276                        bitwarden_api_api::apis::ResponseContent {
277                            status: reqwest::StatusCode::BAD_REQUEST,
278                            message: "Invalid rule".to_string(),
279                        },
280                    ))
281                })
282                .once();
283        });
284
285        let result = client(api_client)
286            .create(organization_id, sample_request())
287            .await;
288
289        assert!(matches!(result, Err(AccessRuleError::Api(_))));
290    }
291
292    #[tokio::test]
293    async fn update_returns_updated_view() {
294        let organization_id = org_id();
295        let rule = rule_id();
296        let response = sample_response(rule.into(), organization_id.into());
297
298        let api_client = ApiClient::new_mocked(move |mock| {
299            mock.access_rules_api
300                .expect_put()
301                .returning(move |_org_id, _id, _request| Ok(response.clone()))
302                .once();
303        });
304
305        let result = client(api_client)
306            .update(organization_id, rule, sample_request())
307            .await
308            .unwrap();
309
310        assert_eq!(result.id, rule);
311    }
312
313    #[tokio::test]
314    async fn delete_succeeds() {
315        let organization_id = org_id();
316        let rule = rule_id();
317
318        let api_client = ApiClient::new_mocked(move |mock| {
319            mock.access_rules_api
320                .expect_delete()
321                .returning(move |_org_id, _id| Ok(()))
322                .once();
323        });
324
325        let result = client(api_client).delete(organization_id, rule).await;
326
327        assert!(result.is_ok());
328    }
329}