bitwarden_pam/access_rules/
client.rs1use 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#[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
35 response
36 .data
37 .unwrap_or_default()
38 .into_iter()
39 .map(AccessRuleView::try_from)
40 .collect()
41 }
42
43 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 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 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 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}