Skip to main content

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);