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