bitwarden_pam/access_requests/
validate.rs1use chrono::{DateTime, Duration, Utc};
2use thiserror::Error;
3#[cfg(feature = "wasm")]
4use wasm_bindgen::prelude::wasm_bindgen;
5
6use super::models::AccessRequestCreateRequest;
7
8pub const MAX_REQUEST_ACCESS_WINDOW_SECONDS: u32 = 365 * 24 * 60 * 60;
15
16#[cfg_attr(feature = "wasm", wasm_bindgen)]
18pub fn max_request_access_window_seconds() -> u32 {
19 MAX_REQUEST_ACCESS_WINDOW_SECONDS
20}
21
22pub const DEFAULT_REQUEST_ACCESS_DURATION_SECONDS: u32 = 3_600;
25
26#[cfg_attr(feature = "wasm", wasm_bindgen)]
28pub fn default_request_access_duration_seconds() -> u32 {
29 DEFAULT_REQUEST_ACCESS_DURATION_SECONDS
30}
31
32#[derive(Debug, Error, PartialEq, Eq)]
34pub enum AccessRequestWindowError {
35 #[error("end must be strictly after start")]
37 EndBeforeStart,
38 #[error("The requested window has already ended.")]
40 EndInPast,
41 #[error(
44 "The requested window exceeds the maximum of {MAX_REQUEST_ACCESS_WINDOW_SECONDS} seconds"
45 )]
46 ExceedsMaxWindow,
47}
48
49impl AccessRequestCreateRequest {
50 pub(crate) fn validate(&self) -> Result<(), AccessRequestWindowError> {
52 self.validate_at(Utc::now())
53 }
54
55 pub(crate) fn validate_at(&self, now: DateTime<Utc>) -> Result<(), AccessRequestWindowError> {
58 if let (Some(start), Some(end)) = (self.start, self.end) {
59 if end <= start {
60 return Err(AccessRequestWindowError::EndBeforeStart);
61 }
62 if end <= now {
65 return Err(AccessRequestWindowError::EndInPast);
66 }
67 if end - start > Duration::seconds(MAX_REQUEST_ACCESS_WINDOW_SECONDS as i64) {
68 return Err(AccessRequestWindowError::ExceedsMaxWindow);
69 }
70 }
71
72 if let Some(duration_seconds) = self.duration_seconds
73 && duration_seconds.get() > MAX_REQUEST_ACCESS_WINDOW_SECONDS
74 {
75 return Err(AccessRequestWindowError::ExceedsMaxWindow);
76 }
77
78 Ok(())
79 }
80}
81
82#[cfg(test)]
83mod tests {
84 use std::num::NonZeroU32;
85
86 use super::*;
87
88 fn now() -> DateTime<Utc> {
90 "2024-12-31T23:00:00Z".parse().unwrap()
91 }
92
93 fn start() -> DateTime<Utc> {
95 "2025-01-01T00:00:00Z".parse().unwrap()
96 }
97
98 fn base_request() -> AccessRequestCreateRequest {
99 AccessRequestCreateRequest {
100 duration_seconds: None,
101 start: None,
102 end: None,
103 reason: None,
104 }
105 }
106
107 #[test]
108 fn neither_duration_nor_window_is_not_rejected_locally() {
109 assert_eq!(base_request().validate_at(now()), Ok(()));
111 }
112
113 #[test]
114 fn end_equal_to_start_is_invalid() {
115 let request = AccessRequestCreateRequest {
116 start: Some(start()),
117 end: Some(start()),
118 ..base_request()
119 };
120
121 assert_eq!(
122 request.validate_at(now()),
123 Err(AccessRequestWindowError::EndBeforeStart)
124 );
125 }
126
127 #[test]
128 fn end_before_start_is_invalid() {
129 let request = AccessRequestCreateRequest {
130 start: Some("2025-01-01T01:00:00Z".parse().unwrap()),
131 end: Some("2025-01-01T00:00:00Z".parse().unwrap()),
132 ..base_request()
133 };
134
135 assert_eq!(
136 request.validate_at(now()),
137 Err(AccessRequestWindowError::EndBeforeStart)
138 );
139 }
140
141 #[test]
142 fn window_span_is_valid_up_to_the_ceiling_and_invalid_past_it() {
143 let ceiling = Duration::seconds(MAX_REQUEST_ACCESS_WINDOW_SECONDS as i64);
144 for (span, expected) in [
145 (Duration::days(7), Ok(())),
147 (ceiling, Ok(())),
148 (
149 ceiling + Duration::seconds(1),
150 Err(AccessRequestWindowError::ExceedsMaxWindow),
151 ),
152 ] {
153 let request = AccessRequestCreateRequest {
154 start: Some(start()),
155 end: Some(start() + span),
156 ..base_request()
157 };
158
159 assert_eq!(request.validate_at(now()), expected, "span {span:?}");
160 }
161 }
162
163 #[test]
164 fn window_that_has_already_ended_is_invalid() {
165 let request = AccessRequestCreateRequest {
167 start: Some("2024-12-23T07:00:00Z".parse().unwrap()),
168 end: Some("2024-12-23T08:00:00Z".parse().unwrap()),
169 ..base_request()
170 };
171
172 assert_eq!(
173 request.validate_at(now()),
174 Err(AccessRequestWindowError::EndInPast)
175 );
176 }
177
178 #[test]
179 fn window_ending_exactly_now_is_invalid() {
180 let request = AccessRequestCreateRequest {
182 start: Some("2024-12-31T22:00:00Z".parse().unwrap()),
183 end: Some(now()),
184 ..base_request()
185 };
186
187 assert_eq!(
188 request.validate_at(now()),
189 Err(AccessRequestWindowError::EndInPast)
190 );
191 }
192
193 #[test]
194 fn window_already_under_way_is_valid() {
195 let request = AccessRequestCreateRequest {
197 start: Some("2024-12-31T22:00:00Z".parse().unwrap()),
198 end: Some("2025-01-01T00:00:00Z".parse().unwrap()),
199 ..base_request()
200 };
201
202 assert_eq!(request.validate_at(now()), Ok(()));
203 }
204
205 #[test]
206 fn reversed_window_reports_end_before_start_rather_than_end_in_past() {
207 let request = AccessRequestCreateRequest {
209 start: Some("2024-12-23T08:00:00Z".parse().unwrap()),
210 end: Some("2024-12-23T07:00:00Z".parse().unwrap()),
211 ..base_request()
212 };
213
214 assert_eq!(
215 request.validate_at(now()),
216 Err(AccessRequestWindowError::EndBeforeStart)
217 );
218 }
219
220 #[test]
221 fn validate_measures_against_the_real_clock() {
222 let request = AccessRequestCreateRequest {
223 start: Some("2020-01-01T00:00:00Z".parse().unwrap()),
224 end: Some("2020-01-01T01:00:00Z".parse().unwrap()),
225 ..base_request()
226 };
227
228 assert_eq!(request.validate(), Err(AccessRequestWindowError::EndInPast));
229 }
230
231 #[test]
232 fn duration_seconds_is_valid_up_to_the_ceiling_and_invalid_past_it() {
233 for (duration_seconds, expected) in [
234 (7 * 86_400, Ok(())),
236 (MAX_REQUEST_ACCESS_WINDOW_SECONDS, Ok(())),
237 (
238 MAX_REQUEST_ACCESS_WINDOW_SECONDS + 1,
239 Err(AccessRequestWindowError::ExceedsMaxWindow),
240 ),
241 ] {
242 let request = AccessRequestCreateRequest {
243 duration_seconds: NonZeroU32::new(duration_seconds),
244 ..base_request()
245 };
246
247 assert_eq!(request.validate_at(now()), expected, "{duration_seconds}s");
248 }
249 }
250
251 #[test]
252 fn small_duration_seconds_is_valid() {
253 let request = AccessRequestCreateRequest {
254 duration_seconds: NonZeroU32::new(3600),
255 ..base_request()
256 };
257
258 assert_eq!(request.validate_at(now()), Ok(()));
259 }
260
261 #[test]
262 fn only_start_without_end_is_not_window_checked_locally() {
263 let request = AccessRequestCreateRequest {
265 start: Some("2025-01-01T00:00:00Z".parse().unwrap()),
266 ..base_request()
267 };
268
269 assert_eq!(request.validate_at(now()), Ok(()));
270 }
271}