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