mas_storage/user/session.rs
1// Copyright 2025, 2026 Element Creations Ltd.
2// Copyright 2024, 2025 New Vector Ltd.
3// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
4//
5// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
6// Please see LICENSE files in the repository root for full details.
7
8use std::net::IpAddr;
9
10use async_trait::async_trait;
11use chrono::{DateTime, Utc};
12use mas_data_model::{
13 Authentication, BrowserSession, Clock, Password, UpstreamOAuthAuthorizationSession, User,
14};
15use rand_core::RngCore;
16use ulid::Ulid;
17
18use crate::{
19 Pagination, pagination::Page, repository_impl, upstream_oauth2::UpstreamOAuthSessionFilter,
20};
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23pub enum BrowserSessionState {
24 Active,
25 Finished,
26}
27
28impl BrowserSessionState {
29 pub fn is_active(self) -> bool {
30 matches!(self, Self::Active)
31 }
32
33 pub fn is_finished(self) -> bool {
34 matches!(self, Self::Finished)
35 }
36}
37
38/// Filter parameters for listing browser sessions
39#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
40pub struct BrowserSessionFilter<'a> {
41 user: Option<&'a User>,
42 state: Option<BrowserSessionState>,
43 last_active_before: Option<DateTime<Utc>>,
44 last_active_after: Option<DateTime<Utc>>,
45 created_before: Option<DateTime<Utc>>,
46 created_after: Option<DateTime<Utc>>,
47 linked_to_upstream_sessions: Option<UpstreamOAuthSessionFilter<'a>>,
48}
49
50impl<'a> BrowserSessionFilter<'a> {
51 /// Create a new [`BrowserSessionFilter`] with default values
52 #[must_use]
53 pub fn new() -> Self {
54 Self::default()
55 }
56
57 /// Set the user who owns the browser sessions
58 #[must_use]
59 pub fn for_user(mut self, user: &'a User) -> Self {
60 self.user = Some(user);
61 self
62 }
63
64 /// Get the user filter
65 #[must_use]
66 pub fn user(&self) -> Option<&User> {
67 self.user
68 }
69
70 /// Only return sessions with a last active time before the given time
71 #[must_use]
72 pub fn with_last_active_before(mut self, last_active_before: DateTime<Utc>) -> Self {
73 self.last_active_before = Some(last_active_before);
74 self
75 }
76
77 /// Only return sessions with a last active time after the given time
78 #[must_use]
79 pub fn with_last_active_after(mut self, last_active_after: DateTime<Utc>) -> Self {
80 self.last_active_after = Some(last_active_after);
81 self
82 }
83
84 /// Get the last active before filter
85 ///
86 /// Returns [`None`] if no client filter was set
87 #[must_use]
88 pub fn last_active_before(&self) -> Option<DateTime<Utc>> {
89 self.last_active_before
90 }
91
92 /// Get the last active after filter
93 ///
94 /// Returns [`None`] if no client filter was set
95 #[must_use]
96 pub fn last_active_after(&self) -> Option<DateTime<Utc>> {
97 self.last_active_after
98 }
99
100 /// Only return sessions created before the given time
101 #[must_use]
102 pub fn with_created_before(mut self, created_before: DateTime<Utc>) -> Self {
103 self.created_before = Some(created_before);
104 self
105 }
106
107 /// Only return sessions created after the given time
108 #[must_use]
109 pub fn with_created_after(mut self, created_after: DateTime<Utc>) -> Self {
110 self.created_after = Some(created_after);
111 self
112 }
113
114 /// Get the created-before filter
115 ///
116 /// Returns [`None`] if no filter was set
117 #[must_use]
118 pub fn created_before(&self) -> Option<DateTime<Utc>> {
119 self.created_before
120 }
121
122 /// Get the created-after filter
123 ///
124 /// Returns [`None`] if no filter was set
125 #[must_use]
126 pub fn created_after(&self) -> Option<DateTime<Utc>> {
127 self.created_after
128 }
129
130 /// Only return active browser sessions
131 #[must_use]
132 pub fn active_only(mut self) -> Self {
133 self.state = Some(BrowserSessionState::Active);
134 self
135 }
136
137 /// Only return finished browser sessions
138 #[must_use]
139 pub fn finished_only(mut self) -> Self {
140 self.state = Some(BrowserSessionState::Finished);
141 self
142 }
143
144 /// Get the state filter
145 #[must_use]
146 pub fn state(&self) -> Option<BrowserSessionState> {
147 self.state
148 }
149
150 /// Only return browser sessions linked to the given upstream OAuth sessions
151 #[must_use]
152 pub fn linked_to_upstream_sessions_only(
153 mut self,
154 filter: UpstreamOAuthSessionFilter<'a>,
155 ) -> Self {
156 self.linked_to_upstream_sessions = Some(filter);
157 self
158 }
159
160 /// Get the upstream OAuth session filter
161 #[must_use]
162 pub fn linked_to_upstream_sessions(&self) -> Option<UpstreamOAuthSessionFilter<'a>> {
163 self.linked_to_upstream_sessions
164 }
165}
166
167/// A [`BrowserSessionRepository`] helps interacting with [`BrowserSession`]
168/// saved in the storage backend
169#[async_trait]
170pub trait BrowserSessionRepository: Send + Sync {
171 /// The error type returned by the repository
172 type Error;
173
174 /// Lookup a [`BrowserSession`] by its ID
175 ///
176 /// Returns `None` if the session is not found
177 ///
178 /// # Parameters
179 ///
180 /// * `id`: The ID of the session to lookup
181 ///
182 /// # Errors
183 ///
184 /// Returns [`Self::Error`] if the underlying repository fails
185 async fn lookup(&mut self, id: Ulid) -> Result<Option<BrowserSession>, Self::Error>;
186
187 /// Create a new [`BrowserSession`] for a [`User`]
188 ///
189 /// Returns the newly created [`BrowserSession`]
190 ///
191 /// # Parameters
192 ///
193 /// * `rng`: The random number generator to use
194 /// * `clock`: The clock used to generate timestamps
195 /// * `user`: The user to create the session for
196 /// * `user_agent`: If available, the user agent of the browser
197 ///
198 /// # Errors
199 ///
200 /// Returns [`Self::Error`] if the underlying repository fails
201 async fn add(
202 &mut self,
203 rng: &mut (dyn RngCore + Send),
204 clock: &dyn Clock,
205 user: &User,
206 user_agent: Option<String>,
207 ) -> Result<BrowserSession, Self::Error>;
208
209 /// Finish a [`BrowserSession`]
210 ///
211 /// Returns the finished session
212 ///
213 /// # Parameters
214 ///
215 /// * `clock`: The clock used to generate timestamps
216 /// * `user_session`: The session to finish
217 ///
218 /// # Errors
219 ///
220 /// Returns [`Self::Error`] if the underlying repository fails
221 async fn finish(
222 &mut self,
223 clock: &dyn Clock,
224 user_session: BrowserSession,
225 ) -> Result<BrowserSession, Self::Error>;
226
227 /// Mark all the [`BrowserSession`] matching the given filter as finished
228 ///
229 /// Returns the number of sessions affected
230 ///
231 /// # Parameters
232 ///
233 /// * `clock`: The clock used to generate timestamps
234 /// * `filter`: The filter parameters
235 ///
236 /// # Errors
237 ///
238 /// Returns [`Self::Error`] if the underlying repository fails
239 async fn finish_bulk(
240 &mut self,
241 clock: &dyn Clock,
242 filter: BrowserSessionFilter<'_>,
243 ) -> Result<usize, Self::Error>;
244
245 /// List [`BrowserSession`] with the given filter and pagination
246 ///
247 /// # Parameters
248 ///
249 /// * `filter`: The filter to apply
250 /// * `pagination`: The pagination parameters
251 ///
252 /// # Errors
253 ///
254 /// Returns [`Self::Error`] if the underlying repository fails
255 async fn list(
256 &mut self,
257 filter: BrowserSessionFilter<'_>,
258 pagination: Pagination,
259 ) -> Result<Page<BrowserSession>, Self::Error>;
260
261 /// Count the number of [`BrowserSession`] with the given filter
262 ///
263 /// # Parameters
264 ///
265 /// * `filter`: The filter to apply
266 ///
267 /// # Errors
268 ///
269 /// Returns [`Self::Error`] if the underlying repository fails
270 async fn count(&mut self, filter: BrowserSessionFilter<'_>) -> Result<usize, Self::Error>;
271
272 /// Authenticate a [`BrowserSession`] with the given [`Password`]
273 ///
274 /// # Parameters
275 ///
276 /// * `rng`: The random number generator to use
277 /// * `clock`: The clock used to generate timestamps
278 /// * `user_session`: The session to authenticate
279 /// * `user_password`: The password which was used to authenticate
280 ///
281 /// # Errors
282 ///
283 /// Returns [`Self::Error`] if the underlying repository fails
284 async fn authenticate_with_password(
285 &mut self,
286 rng: &mut (dyn RngCore + Send),
287 clock: &dyn Clock,
288 user_session: &BrowserSession,
289 user_password: &Password,
290 ) -> Result<Authentication, Self::Error>;
291
292 /// Authenticate a [`BrowserSession`] with the given
293 /// [`UpstreamOAuthAuthorizationSession`]
294 ///
295 /// # Parameters
296 ///
297 /// * `rng`: The random number generator to use
298 /// * `clock`: The clock used to generate timestamps
299 /// * `user_session`: The session to authenticate
300 /// * `upstream_oauth_session`: The upstream OAuth session which was used to
301 /// authenticate
302 ///
303 /// # Errors
304 ///
305 /// Returns [`Self::Error`] if the underlying repository fails
306 async fn authenticate_with_upstream(
307 &mut self,
308 rng: &mut (dyn RngCore + Send),
309 clock: &dyn Clock,
310 user_session: &BrowserSession,
311 upstream_oauth_session: &UpstreamOAuthAuthorizationSession,
312 ) -> Result<Authentication, Self::Error>;
313
314 /// Get the last successful authentication for a [`BrowserSession`]
315 ///
316 /// # Params
317 ///
318 /// * `user_session`: The session for which to get the last authentication
319 ///
320 /// # Errors
321 ///
322 /// Returns [`Self::Error`] if the underlying repository fails
323 async fn get_last_authentication(
324 &mut self,
325 user_session: &BrowserSession,
326 ) -> Result<Option<Authentication>, Self::Error>;
327
328 /// Record a batch of [`BrowserSession`] activity
329 ///
330 /// # Parameters
331 ///
332 /// * `activity`: A list of tuples containing the session ID, the last
333 /// activity timestamp and the IP address of the client
334 ///
335 /// # Errors
336 ///
337 /// Returns [`Self::Error`] if the underlying repository fails
338 async fn record_batch_activity(
339 &mut self,
340 activity: Vec<(Ulid, DateTime<Utc>, Option<IpAddr>)>,
341 ) -> Result<(), Self::Error>;
342
343 /// Cleanup finished [`BrowserSession`]s
344 ///
345 /// Deletes sessions finished between `since` and `until`, but only if they
346 /// have no child sessions (`compat_sessions` or `oauth2_sessions`). Returns
347 /// the number of deleted sessions and the timestamp of the last deleted
348 /// session for pagination.
349 ///
350 /// # Parameters
351 ///
352 /// * `since`: The earliest finish time to delete (exclusive). If `None`,
353 /// starts from the beginning.
354 /// * `until`: The latest finish time to delete (exclusive)
355 /// * `limit`: Maximum number of sessions to delete in this batch
356 ///
357 /// # Errors
358 ///
359 /// Returns [`Self::Error`] if the underlying repository fails
360 async fn cleanup_finished(
361 &mut self,
362 since: Option<DateTime<Utc>>,
363 until: DateTime<Utc>,
364 limit: usize,
365 ) -> Result<(usize, Option<DateTime<Utc>>), Self::Error>;
366
367 /// Clear IP addresses from sessions inactive since the threshold
368 ///
369 /// Sets `last_active_ip` to `NULL` for sessions where `last_active_at` is
370 /// before the threshold. Returns the number of sessions affected and the
371 /// last `last_active_at` timestamp processed for pagination.
372 ///
373 /// # Parameters
374 ///
375 /// * `since`: Only process sessions with `last_active_at` at or after this
376 /// timestamp (exclusive). If `None`, starts from the beginning.
377 /// * `threshold`: Clear IPs for sessions with `last_active_at` before this
378 /// time
379 /// * `limit`: Maximum number of sessions to update in this batch
380 ///
381 /// # Errors
382 ///
383 /// Returns [`Self::Error`] if the underlying repository fails
384 async fn cleanup_inactive_ips(
385 &mut self,
386 since: Option<DateTime<Utc>>,
387 threshold: DateTime<Utc>,
388 limit: usize,
389 ) -> Result<(usize, Option<DateTime<Utc>>), Self::Error>;
390}
391
392repository_impl!(BrowserSessionRepository:
393 async fn lookup(&mut self, id: Ulid) -> Result<Option<BrowserSession>, Self::Error>;
394 async fn add(
395 &mut self,
396 rng: &mut (dyn RngCore + Send),
397 clock: &dyn Clock,
398 user: &User,
399 user_agent: Option<String>,
400 ) -> Result<BrowserSession, Self::Error>;
401 async fn finish(
402 &mut self,
403 clock: &dyn Clock,
404 user_session: BrowserSession,
405 ) -> Result<BrowserSession, Self::Error>;
406
407 async fn finish_bulk(
408 &mut self,
409 clock: &dyn Clock,
410 filter: BrowserSessionFilter<'_>,
411 ) -> Result<usize, Self::Error>;
412
413 async fn list(
414 &mut self,
415 filter: BrowserSessionFilter<'_>,
416 pagination: Pagination,
417 ) -> Result<Page<BrowserSession>, Self::Error>;
418
419 async fn count(&mut self, filter: BrowserSessionFilter<'_>) -> Result<usize, Self::Error>;
420
421 async fn authenticate_with_password(
422 &mut self,
423 rng: &mut (dyn RngCore + Send),
424 clock: &dyn Clock,
425 user_session: &BrowserSession,
426 user_password: &Password,
427 ) -> Result<Authentication, Self::Error>;
428
429 async fn authenticate_with_upstream(
430 &mut self,
431 rng: &mut (dyn RngCore + Send),
432 clock: &dyn Clock,
433 user_session: &BrowserSession,
434 upstream_oauth_session: &UpstreamOAuthAuthorizationSession,
435 ) -> Result<Authentication, Self::Error>;
436
437 async fn get_last_authentication(
438 &mut self,
439 user_session: &BrowserSession,
440 ) -> Result<Option<Authentication>, Self::Error>;
441
442 async fn record_batch_activity(
443 &mut self,
444 activity: Vec<(Ulid, DateTime<Utc>, Option<IpAddr>)>,
445 ) -> Result<(), Self::Error>;
446
447 async fn cleanup_finished(
448 &mut self,
449 since: Option<DateTime<Utc>>,
450 until: DateTime<Utc>,
451 limit: usize,
452 ) -> Result<(usize, Option<DateTime<Utc>>), Self::Error>;
453
454 async fn cleanup_inactive_ips(
455 &mut self,
456 since: Option<DateTime<Utc>>,
457 threshold: DateTime<Utc>,
458 limit: usize,
459 ) -> Result<(usize, Option<DateTime<Utc>>), Self::Error>;
460);