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