bitwarden_pam/access_rules/
client.rs1use 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#[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 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 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 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 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 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}