bitwarden_pam/access_rules/
models.rs1use bitwarden_api_api::models::{AccessRuleRequestModel, AccessRuleResponseModel};
2use bitwarden_collections::collection::CollectionId;
3use bitwarden_core::{OrganizationId, require};
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6
7use super::{conditions::AccessCondition, error::AccessRuleError};
8use crate::AccessRuleId;
9
10#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
12#[bitwarden_ffi::wasm_record]
13#[serde(rename_all = "camelCase")]
14pub struct AccessRuleView {
15 pub id: AccessRuleId,
17 pub organization_id: OrganizationId,
19 pub name: String,
21 pub description: Option<String>,
23 pub enabled: bool,
25 pub conditions: Vec<AccessCondition>,
27 pub single_active_lease: bool,
29 pub default_lease_duration_seconds: Option<i32>,
32 pub max_lease_duration_seconds: Option<i32>,
35 pub allows_extensions: bool,
37 pub max_extension_duration_seconds: Option<i32>,
39 pub collections: Vec<CollectionId>,
41 pub creation_date: DateTime<Utc>,
43 pub revision_date: DateTime<Utc>,
45}
46
47#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
49#[bitwarden_ffi::wasm_record]
50#[serde(rename_all = "camelCase")]
51pub struct AccessRuleAddEditRequest {
52 pub name: String,
54 pub description: Option<String>,
56 pub enabled: bool,
58 pub conditions: Vec<AccessCondition>,
60 pub single_active_lease: bool,
62 pub default_lease_duration_seconds: Option<i32>,
65 pub max_lease_duration_seconds: Option<i32>,
68 pub allows_extensions: bool,
71 pub max_extension_duration_seconds: Option<i32>,
74 pub collections: Vec<CollectionId>,
77}
78
79impl TryFrom<AccessRuleResponseModel> for AccessRuleView {
80 type Error = AccessRuleError;
81
82 fn try_from(response: AccessRuleResponseModel) -> Result<Self, Self::Error> {
83 let conditions = match response.conditions {
84 None => Vec::new(),
85 Some(value @ serde_json::Value::Array(_)) => serde_json::from_value(value)
86 .map_err(|e| AccessRuleError::InvalidConditions(e.to_string()))?,
87 Some(other) => {
88 return Err(AccessRuleError::InvalidConditions(format!(
89 "expected `conditions` to be a JSON array, got: {other}"
90 )));
91 }
92 };
93
94 Ok(Self {
95 id: AccessRuleId::new(require!(response.id)),
96 organization_id: OrganizationId::new(require!(response.organization_id)),
97 name: require!(response.name),
98 description: response.description,
99 enabled: response.enabled.unwrap_or(true),
101 conditions,
102 single_active_lease: response.single_active_lease.unwrap_or(false),
103 default_lease_duration_seconds: response.default_lease_duration_seconds,
104 max_lease_duration_seconds: response.max_lease_duration_seconds,
105 allows_extensions: response.allows_extensions.unwrap_or(false),
106 max_extension_duration_seconds: response.max_extension_duration_seconds,
107 collections: response
108 .collections
109 .unwrap_or_default()
110 .into_iter()
111 .map(CollectionId::new)
112 .collect(),
113 creation_date: require!(response.creation_date).parse()?,
114 revision_date: require!(response.revision_date).parse()?,
115 })
116 }
117}
118
119impl From<AccessRuleView> for AccessRuleAddEditRequest {
127 fn from(view: AccessRuleView) -> Self {
128 Self {
129 name: view.name,
130 description: view.description,
131 enabled: view.enabled,
132 conditions: view.conditions,
133 single_active_lease: view.single_active_lease,
134 default_lease_duration_seconds: view.default_lease_duration_seconds,
135 max_lease_duration_seconds: view.max_lease_duration_seconds,
136 allows_extensions: view.allows_extensions,
137 max_extension_duration_seconds: view.max_extension_duration_seconds,
138 collections: view.collections,
139 }
140 }
141}
142
143impl TryFrom<AccessRuleAddEditRequest> for AccessRuleRequestModel {
144 type Error = AccessRuleError;
145
146 fn try_from(request: AccessRuleAddEditRequest) -> Result<Self, Self::Error> {
147 let conditions = serde_json::to_value(&request.conditions)
150 .map_err(|e| AccessRuleError::InvalidConditions(e.to_string()))?;
151
152 Ok(Self {
153 name: request.name.trim().to_string(),
154 description: request.description,
155 enabled: Some(request.enabled),
156 conditions: Some(conditions),
157 single_active_lease: Some(request.single_active_lease),
158 default_lease_duration_seconds: request.default_lease_duration_seconds,
159 max_lease_duration_seconds: request.max_lease_duration_seconds,
160 allows_extensions: Some(request.allows_extensions),
161 max_extension_duration_seconds: request.max_extension_duration_seconds,
162 collections: request
163 .collections
164 .into_iter()
165 .map(uuid::Uuid::from)
166 .collect(),
167 })
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use bitwarden_collections::collection::CollectionId;
174 use uuid::Uuid;
175
176 use super::*;
177
178 fn full_response() -> AccessRuleResponseModel {
179 AccessRuleResponseModel {
180 id: Some(Uuid::new_v4()),
181 organization_id: Some(Uuid::new_v4()),
182 name: Some("My rule".to_string()),
183 description: Some("A description".to_string()),
184 enabled: Some(false),
185 conditions: Some(serde_json::json!([{ "kind": "human_approval" }])),
186 single_active_lease: Some(true),
187 default_lease_duration_seconds: Some(60),
188 max_lease_duration_seconds: Some(120),
189 allows_extensions: Some(true),
190 max_extension_duration_seconds: Some(30),
191 collections: Some(vec![Uuid::new_v4()]),
192 creation_date: Some("2025-01-01T00:00:00Z".to_string()),
193 revision_date: Some("2025-01-02T00:00:00Z".to_string()),
194 ..Default::default()
195 }
196 }
197
198 #[test]
199 fn full_response_converts_to_view() {
200 let response = full_response();
201 let expected_id = response.id.unwrap();
202 let expected_org_id = response.organization_id.unwrap();
203 let expected_collection_id = response.collections.as_ref().unwrap()[0];
204
205 let view = AccessRuleView::try_from(response).unwrap();
206
207 assert_eq!(
208 view,
209 AccessRuleView {
210 id: AccessRuleId::new(expected_id),
211 organization_id: OrganizationId::new(expected_org_id),
212 name: "My rule".to_string(),
213 description: Some("A description".to_string()),
214 enabled: false,
215 conditions: vec![AccessCondition::HumanApproval],
216 single_active_lease: true,
217 default_lease_duration_seconds: Some(60),
218 max_lease_duration_seconds: Some(120),
219 allows_extensions: true,
220 max_extension_duration_seconds: Some(30),
221 collections: vec![CollectionId::new(expected_collection_id)],
222 creation_date: "2025-01-01T00:00:00Z".parse().unwrap(),
223 revision_date: "2025-01-02T00:00:00Z".parse().unwrap(),
224 }
225 );
226 }
227
228 #[test]
229 fn missing_id_is_a_missing_field_error() {
230 let mut response = full_response();
231 response.id = None;
232
233 let result = AccessRuleView::try_from(response);
234
235 assert!(matches!(result, Err(AccessRuleError::MissingField(_))));
236 }
237
238 #[test]
239 fn null_conditions_become_empty_vec() {
240 let mut response = full_response();
241 response.conditions = None;
242
243 let view = AccessRuleView::try_from(response).unwrap();
244
245 assert_eq!(view.conditions, Vec::new());
246 }
247
248 #[test]
249 fn missing_enabled_defaults_to_true() {
250 let mut response = full_response();
251 response.enabled = None;
252
253 let view = AccessRuleView::try_from(response).unwrap();
254
255 assert!(view.enabled);
256 }
257
258 #[test]
259 fn non_array_conditions_is_an_error() {
260 let mut response = full_response();
261 response.conditions = Some(serde_json::json!({ "not": "an array" }));
262
263 let result = AccessRuleView::try_from(response);
264
265 assert!(matches!(result, Err(AccessRuleError::InvalidConditions(_))));
266 }
267
268 #[test]
269 fn request_always_sends_conditions_as_an_array() {
270 let request = AccessRuleAddEditRequest {
271 name: "My rule".to_string(),
272 description: None,
273 enabled: true,
274 conditions: Vec::new(),
275 single_active_lease: false,
276 default_lease_duration_seconds: None,
277 max_lease_duration_seconds: None,
278 allows_extensions: false,
279 max_extension_duration_seconds: None,
280 collections: Vec::new(),
281 };
282
283 let model = AccessRuleRequestModel::try_from(request).unwrap();
284
285 assert_eq!(model.conditions, Some(serde_json::json!([])));
286 }
287
288 #[test]
289 fn request_trims_name_before_sending() {
290 let request = AccessRuleAddEditRequest {
291 name: " My rule ".to_string(),
292 description: None,
293 enabled: true,
294 conditions: Vec::new(),
295 single_active_lease: false,
296 default_lease_duration_seconds: None,
297 max_lease_duration_seconds: None,
298 allows_extensions: false,
299 max_extension_duration_seconds: None,
300 collections: Vec::new(),
301 };
302
303 let model = AccessRuleRequestModel::try_from(request).unwrap();
304
305 assert_eq!(model.name, "My rule");
306 }
307}