bitwarden_pam/access_rules/
validate.rs1use std::net::IpAddr;
2
3use thiserror::Error;
4#[cfg(feature = "wasm")]
5use wasm_bindgen::prelude::wasm_bindgen;
6
7use super::{conditions::AccessCondition, models::AccessRuleAddEditRequest};
8
9const MAX_NAME_LENGTH: usize = 256;
11const MAX_CONDITIONS: usize = 10;
13
14#[derive(Debug, Error, PartialEq, Eq)]
17pub enum AccessRuleValidationError {
18 #[error("Name must be between 1 and {MAX_NAME_LENGTH} characters")]
20 InvalidName,
21 #[error("A positive max extension duration is required when extensions are allowed")]
24 MissingMaxExtensionDuration,
25 #[error("Lease durations must be positive")]
28 InvalidLeaseDuration,
29 #[error("A rule may have at most {MAX_CONDITIONS} conditions")]
31 TooManyConditions,
32 #[error("Invalid CIDR range: {0}")]
34 InvalidCidr(String),
35 #[error("An IP allowlist condition must contain at least one CIDR range")]
37 EmptyCidrList,
38}
39
40pub fn validate_request(
44 request: &AccessRuleAddEditRequest,
45) -> Result<(), AccessRuleValidationError> {
46 let trimmed_name = request.name.trim();
47 if trimmed_name.is_empty() || trimmed_name.encode_utf16().count() > MAX_NAME_LENGTH {
48 return Err(AccessRuleValidationError::InvalidName);
49 }
50
51 if request.allows_extensions
52 && request
53 .max_extension_duration_seconds
54 .is_none_or(|seconds| seconds <= 0)
55 {
56 return Err(AccessRuleValidationError::MissingMaxExtensionDuration);
57 }
58
59 if request
60 .default_lease_duration_seconds
61 .is_some_and(|d| d <= 0)
62 || request.max_lease_duration_seconds.is_some_and(|m| m <= 0)
63 {
64 return Err(AccessRuleValidationError::InvalidLeaseDuration);
65 }
66
67 if request.conditions.len() > MAX_CONDITIONS {
68 return Err(AccessRuleValidationError::TooManyConditions);
69 }
70
71 for condition in &request.conditions {
72 if let AccessCondition::IpAllowlist { cidrs } = condition {
73 if cidrs.is_empty() {
74 return Err(AccessRuleValidationError::EmptyCidrList);
75 }
76 for cidr in cidrs {
77 if !is_valid_cidr(cidr) {
78 return Err(AccessRuleValidationError::InvalidCidr(cidr.clone()));
79 }
80 }
81 }
82 }
83
84 Ok(())
85}
86
87#[cfg_attr(feature = "wasm", wasm_bindgen)]
101pub fn is_valid_cidr(value: &str) -> bool {
102 let Some((addr, prefix)) = value.split_once('/') else {
103 return false;
104 };
105 if !prefix.bytes().all(|b| b.is_ascii_digit()) {
107 return false;
108 }
109 let Ok(prefix) = prefix.parse::<u8>() else {
110 return false;
111 };
112 match addr.parse::<IpAddr>() {
113 Ok(IpAddr::V4(ip)) => prefix <= 32 && no_host_bits(u32::from(ip).into(), prefix, 32),
114 Ok(IpAddr::V6(ip)) => {
115 ip.to_ipv4_mapped().is_none() && prefix <= 128 && no_host_bits(ip.into(), prefix, 128)
119 }
120 Err(_) => false,
121 }
122}
123
124fn no_host_bits(addr: u128, prefix: u8, width: u8) -> bool {
133 debug_assert!(prefix <= width);
134 let host_bits = width - prefix;
135 host_bits == 0 || addr & (u128::MAX >> (128 - u32::from(host_bits))) == 0
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 fn base_request() -> AccessRuleAddEditRequest {
143 AccessRuleAddEditRequest {
144 name: "My rule".to_string(),
145 description: None,
146 enabled: true,
147 conditions: Vec::new(),
148 single_active_lease: false,
149 default_lease_duration_seconds: None,
150 max_lease_duration_seconds: None,
151 allows_extensions: false,
152 max_extension_duration_seconds: None,
153 collections: Vec::new(),
154 }
155 }
156
157 #[test]
158 fn blank_name_is_invalid() {
159 let mut request = base_request();
160 request.name = " ".to_string();
161 assert_eq!(
162 validate_request(&request),
163 Err(AccessRuleValidationError::InvalidName)
164 );
165 }
166
167 #[test]
168 fn name_over_256_chars_is_invalid() {
169 let mut request = base_request();
170 request.name = "a".repeat(257);
171 assert_eq!(
172 validate_request(&request),
173 Err(AccessRuleValidationError::InvalidName)
174 );
175 }
176
177 #[test]
178 fn name_at_256_chars_is_valid() {
179 let mut request = base_request();
180 request.name = "a".repeat(256);
181 assert_eq!(validate_request(&request), Ok(()));
182 }
183
184 #[test]
185 fn name_at_256_chars_with_surrounding_whitespace_is_valid() {
186 let mut request = base_request();
187 request.name = format!(" {} ", "a".repeat(256));
188 assert_eq!(validate_request(&request), Ok(()));
189 }
190
191 #[test]
192 fn allows_extensions_without_max_duration_is_invalid() {
193 let mut request = base_request();
194 request.allows_extensions = true;
195 request.max_extension_duration_seconds = None;
196 assert_eq!(
197 validate_request(&request),
198 Err(AccessRuleValidationError::MissingMaxExtensionDuration)
199 );
200 }
201
202 #[test]
203 fn allows_extensions_with_zero_max_duration_is_invalid() {
204 let mut request = base_request();
205 request.allows_extensions = true;
206 request.max_extension_duration_seconds = Some(0);
207 assert_eq!(
208 validate_request(&request),
209 Err(AccessRuleValidationError::MissingMaxExtensionDuration)
210 );
211 }
212
213 #[test]
214 fn allows_extensions_with_positive_max_duration_is_valid() {
215 let mut request = base_request();
216 request.allows_extensions = true;
217 request.max_extension_duration_seconds = Some(60);
218 assert_eq!(validate_request(&request), Ok(()));
219 }
220
221 #[test]
222 fn more_than_ten_conditions_is_invalid() {
223 let mut request = base_request();
224 request.conditions = (0..11).map(|_| AccessCondition::HumanApproval).collect();
225 assert_eq!(
226 validate_request(&request),
227 Err(AccessRuleValidationError::TooManyConditions)
228 );
229 }
230
231 #[test]
232 fn exactly_ten_conditions_is_valid() {
233 let mut request = base_request();
234 request.conditions = (0..10).map(|_| AccessCondition::HumanApproval).collect();
235 assert_eq!(validate_request(&request), Ok(()));
236 }
237
238 #[test]
239 fn empty_cidr_list_is_invalid() {
240 let mut request = base_request();
241 request.conditions = vec![AccessCondition::IpAllowlist { cidrs: Vec::new() }];
242 assert_eq!(
243 validate_request(&request),
244 Err(AccessRuleValidationError::EmptyCidrList)
245 );
246 }
247
248 #[test]
249 fn valid_ipv4_cidr() {
250 assert!(is_valid_cidr("10.0.0.0/8"));
251 }
252
253 #[test]
254 fn valid_ipv6_cidr() {
255 assert!(is_valid_cidr("2001:db8::/32"));
256 }
257
258 #[test]
259 fn cidr_with_host_bits_set_is_invalid() {
260 assert!(!is_valid_cidr("10.0.0.1/8"));
261 }
262
263 #[test]
264 fn cidr_without_prefix_is_invalid() {
265 assert!(!is_valid_cidr("10.0.0.0"));
266 }
267
268 #[test]
269 fn garbage_cidr_is_invalid() {
270 assert!(!is_valid_cidr("not-a-cidr"));
271 }
272
273 #[test]
274 fn ip_allowlist_with_invalid_cidr_is_rejected() {
275 let mut request = base_request();
276 request.conditions = vec![AccessCondition::IpAllowlist {
277 cidrs: vec!["10.0.0.1/8".to_string()],
278 }];
279 assert_eq!(
280 validate_request(&request),
281 Err(AccessRuleValidationError::InvalidCidr(
282 "10.0.0.1/8".to_string()
283 ))
284 );
285 }
286
287 #[test]
288 fn unknown_condition_kind_is_skipped() {
289 let mut request = base_request();
290 request.conditions = vec![AccessCondition::Unknown(serde_json::json!({
291 "kind": "time_of_day",
292 }))];
293 assert_eq!(validate_request(&request), Ok(()));
294 }
295
296 #[test]
299 fn zero_zero_zero_zero_slash_zero_is_valid() {
300 assert!(is_valid_cidr("0.0.0.0/0"));
301 }
302
303 #[test]
304 fn ipv6_slash_zero_is_valid() {
305 assert!(is_valid_cidr("::/0"));
306 }
307
308 #[test]
309 fn ipv4_nonzero_host_with_slash_zero_is_invalid() {
310 assert!(!is_valid_cidr("10.0.0.0/0"));
312 }
313
314 #[test]
315 fn ipv4_full_prefix_is_valid() {
316 assert!(is_valid_cidr("10.0.0.1/32"));
317 }
318
319 #[test]
320 fn ipv6_full_prefix_is_valid() {
321 assert!(is_valid_cidr("2001:db8::1/128"));
322 }
323
324 #[test]
325 fn ipv4_prefix_out_of_range_is_invalid() {
326 assert!(!is_valid_cidr("10.0.0.0/33"));
327 }
328
329 #[test]
330 fn ipv6_prefix_out_of_range_is_invalid() {
331 assert!(!is_valid_cidr("2001:db8::/129"));
332 }
333
334 #[test]
335 fn empty_prefix_is_invalid() {
336 assert!(!is_valid_cidr("10.0.0.0/"));
337 }
338
339 #[test]
340 fn empty_address_is_invalid() {
341 assert!(!is_valid_cidr("/8"));
342 }
343
344 #[test]
345 fn double_slash_prefix_is_invalid() {
346 assert!(!is_valid_cidr("10.0.0.0/8/8"));
347 }
348
349 #[test]
350 fn leading_whitespace_is_invalid() {
351 assert!(!is_valid_cidr(" 10.0.0.0/8"));
352 }
353
354 #[test]
355 fn prefix_with_leading_whitespace_is_invalid() {
356 assert!(!is_valid_cidr("10.0.0.0/ 8"));
357 }
358
359 #[test]
360 fn ipv6_prefix_300_is_invalid() {
361 assert!(!is_valid_cidr("2001:db8::/300"));
362 }
363
364 #[test]
367 fn signed_positive_prefix_is_invalid() {
368 assert!(!is_valid_cidr("10.0.0.0/+8"));
369 }
370
371 #[test]
372 fn signed_negative_prefix_is_invalid() {
373 assert!(!is_valid_cidr("10.0.0.0/-8"));
374 }
375
376 #[test]
379 fn prefix_with_leading_zero_is_valid() {
380 assert!(is_valid_cidr("10.0.0.0/08"));
381 }
382
383 #[test]
386 fn leading_zero_octet_is_invalid() {
387 assert!(!is_valid_cidr("010.0.0.0/8"));
390 }
391
392 #[test]
393 fn hex_octet_is_invalid() {
394 assert!(!is_valid_cidr("0x0A.0.0.0/8"));
395 }
396
397 #[test]
398 fn partial_ipv4_address_is_invalid() {
399 assert!(!is_valid_cidr("1.2.3/24"));
400 }
401
402 #[test]
403 fn ipv6_zone_id_is_invalid() {
404 assert!(!is_valid_cidr("fe80::1%1/64"));
405 }
406
407 #[test]
410 fn ipv4_mapped_ipv6_is_invalid() {
411 assert!(!is_valid_cidr("::ffff:10.0.0.0/104"));
414 }
415
416 #[test]
417 fn ipv6_loopback_is_not_treated_as_mapped() {
418 assert!(is_valid_cidr("::1/128"));
421 }
422
423 #[test]
426 fn name_with_supplementary_chars_measured_in_utf16() {
427 let base_char = '𝔸';
430 let mut request = base_request();
431
432 request.name = base_char.to_string().repeat(128);
433 assert_eq!(validate_request(&request), Ok(()));
434
435 request.name = base_char.to_string().repeat(129);
436 assert_eq!(
437 validate_request(&request),
438 Err(AccessRuleValidationError::InvalidName)
439 );
440 }
441
442 #[test]
445 fn negative_default_lease_duration_is_invalid() {
446 let mut request = base_request();
447 request.default_lease_duration_seconds = Some(-1);
448 assert_eq!(
449 validate_request(&request),
450 Err(AccessRuleValidationError::InvalidLeaseDuration)
451 );
452 }
453
454 #[test]
455 fn zero_default_lease_duration_is_invalid() {
456 let mut request = base_request();
457 request.default_lease_duration_seconds = Some(0);
458 assert_eq!(
459 validate_request(&request),
460 Err(AccessRuleValidationError::InvalidLeaseDuration)
461 );
462 }
463
464 #[test]
465 fn negative_max_lease_duration_is_invalid() {
466 let mut request = base_request();
467 request.max_lease_duration_seconds = Some(-1);
468 assert_eq!(
469 validate_request(&request),
470 Err(AccessRuleValidationError::InvalidLeaseDuration)
471 );
472 }
473
474 #[test]
475 fn positive_lease_durations_are_valid() {
476 let mut request = base_request();
477 request.default_lease_duration_seconds = Some(300);
478 request.max_lease_duration_seconds = Some(3600);
479 assert_eq!(validate_request(&request), Ok(()));
480 }
481
482 #[test]
483 fn none_lease_durations_are_valid() {
484 let mut request = base_request();
485 request.default_lease_duration_seconds = None;
486 request.max_lease_duration_seconds = None;
487 assert_eq!(validate_request(&request), Ok(()));
488 }
489}