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