Skip to main content

bitwarden_pam/access_rules/
client.rs

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