Skip to main content

mas_storage/compat/
session.rs

1// Copyright 2025, 2026 Element Creations Ltd.
2// Copyright 2024, 2025 New Vector Ltd.
3// Copyright 2023, 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::{BrowserSession, Clock, CompatSession, CompatSsoLogin, Device, User};
13use rand_core::RngCore;
14use ulid::Ulid;
15
16use crate::{Page, Pagination, repository_impl, user::BrowserSessionFilter};
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub enum CompatSessionState {
20    Active,
21    Finished,
22}
23
24impl CompatSessionState {
25    /// Returns [`true`] if we're looking for active sessions
26    #[must_use]
27    pub fn is_active(self) -> bool {
28        matches!(self, Self::Active)
29    }
30
31    /// Returns [`true`] if we're looking for finished sessions
32    #[must_use]
33    pub fn is_finished(self) -> bool {
34        matches!(self, Self::Finished)
35    }
36}
37
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
39pub enum CompatSessionType {
40    SsoLogin,
41    Unknown,
42}
43
44impl CompatSessionType {
45    /// Returns [`true`] if we're looking for SSO logins
46    #[must_use]
47    pub fn is_sso_login(self) -> bool {
48        matches!(self, Self::SsoLogin)
49    }
50
51    /// Returns [`true`] if we're looking for unknown sessions
52    #[must_use]
53    pub fn is_unknown(self) -> bool {
54        matches!(self, Self::Unknown)
55    }
56}
57
58/// Filter parameters for listing compatibility sessions
59#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
60pub struct CompatSessionFilter<'a> {
61    user: Option<&'a User>,
62    browser_session: Option<&'a BrowserSession>,
63    browser_session_filter: Option<BrowserSessionFilter<'a>>,
64    state: Option<CompatSessionState>,
65    auth_type: Option<CompatSessionType>,
66    device: Option<&'a Device>,
67    last_active_before: Option<DateTime<Utc>>,
68    last_active_after: Option<DateTime<Utc>>,
69    created_before: Option<DateTime<Utc>>,
70    created_after: Option<DateTime<Utc>>,
71}
72
73impl<'a> CompatSessionFilter<'a> {
74    /// Create a new [`CompatSessionFilter`] with default values
75    #[must_use]
76    pub fn new() -> Self {
77        Self::default()
78    }
79
80    /// Set the user who owns the compatibility sessions
81    #[must_use]
82    pub fn for_user(mut self, user: &'a User) -> Self {
83        self.user = Some(user);
84        self
85    }
86
87    /// Get the user filter
88    #[must_use]
89    pub fn user(&self) -> Option<&'a User> {
90        self.user
91    }
92
93    /// Set the device filter
94    #[must_use]
95    pub fn for_device(mut self, device: &'a Device) -> Self {
96        self.device = Some(device);
97        self
98    }
99
100    /// Get the device filter
101    #[must_use]
102    pub fn device(&self) -> Option<&'a Device> {
103        self.device
104    }
105
106    /// Set the browser session filter
107    #[must_use]
108    pub fn for_browser_session(mut self, browser_session: &'a BrowserSession) -> Self {
109        self.browser_session = Some(browser_session);
110        self
111    }
112
113    /// Set the browser sessions filter
114    #[must_use]
115    pub fn for_browser_sessions(
116        mut self,
117        browser_session_filter: BrowserSessionFilter<'a>,
118    ) -> Self {
119        self.browser_session_filter = Some(browser_session_filter);
120        self
121    }
122
123    /// Get the browser session filter
124    #[must_use]
125    pub fn browser_session(&self) -> Option<&'a BrowserSession> {
126        self.browser_session
127    }
128
129    /// Get the browser sessions filter
130    #[must_use]
131    pub fn browser_session_filter(&self) -> Option<BrowserSessionFilter<'a>> {
132        self.browser_session_filter
133    }
134
135    /// Only return sessions with a last active time before the given time
136    #[must_use]
137    pub fn with_last_active_before(mut self, last_active_before: DateTime<Utc>) -> Self {
138        self.last_active_before = Some(last_active_before);
139        self
140    }
141
142    /// Only return sessions with a last active time after the given time
143    #[must_use]
144    pub fn with_last_active_after(mut self, last_active_after: DateTime<Utc>) -> Self {
145        self.last_active_after = Some(last_active_after);
146        self
147    }
148
149    /// Get the last active before filter
150    ///
151    /// Returns [`None`] if no client filter was set
152    #[must_use]
153    pub fn last_active_before(&self) -> Option<DateTime<Utc>> {
154        self.last_active_before
155    }
156
157    /// Get the last active after filter
158    ///
159    /// Returns [`None`] if no client filter was set
160    #[must_use]
161    pub fn last_active_after(&self) -> Option<DateTime<Utc>> {
162        self.last_active_after
163    }
164
165    /// Only return sessions created before the given time
166    #[must_use]
167    pub fn with_created_before(mut self, created_before: DateTime<Utc>) -> Self {
168        self.created_before = Some(created_before);
169        self
170    }
171
172    /// Only return sessions created after the given time
173    #[must_use]
174    pub fn with_created_after(mut self, created_after: DateTime<Utc>) -> Self {
175        self.created_after = Some(created_after);
176        self
177    }
178
179    /// Get the created-before filter
180    ///
181    /// Returns [`None`] if no filter was set
182    #[must_use]
183    pub fn created_before(&self) -> Option<DateTime<Utc>> {
184        self.created_before
185    }
186
187    /// Get the created-after filter
188    ///
189    /// Returns [`None`] if no filter was set
190    #[must_use]
191    pub fn created_after(&self) -> Option<DateTime<Utc>> {
192        self.created_after
193    }
194
195    /// Only return active compatibility sessions
196    #[must_use]
197    pub fn active_only(mut self) -> Self {
198        self.state = Some(CompatSessionState::Active);
199        self
200    }
201
202    /// Only return finished compatibility sessions
203    #[must_use]
204    pub fn finished_only(mut self) -> Self {
205        self.state = Some(CompatSessionState::Finished);
206        self
207    }
208
209    /// Get the state filter
210    #[must_use]
211    pub fn state(&self) -> Option<CompatSessionState> {
212        self.state
213    }
214
215    /// Only return SSO login compatibility sessions
216    #[must_use]
217    pub fn sso_login_only(mut self) -> Self {
218        self.auth_type = Some(CompatSessionType::SsoLogin);
219        self
220    }
221
222    /// Only return unknown compatibility sessions
223    #[must_use]
224    pub fn unknown_only(mut self) -> Self {
225        self.auth_type = Some(CompatSessionType::Unknown);
226        self
227    }
228
229    /// Get the auth type filter
230    #[must_use]
231    pub fn auth_type(&self) -> Option<CompatSessionType> {
232        self.auth_type
233    }
234}
235
236/// A [`CompatSessionRepository`] helps interacting with
237/// [`CompatSession`] saved in the storage backend
238#[async_trait]
239pub trait CompatSessionRepository: Send + Sync {
240    /// The error type returned by the repository
241    type Error;
242
243    /// Lookup a compat session by its ID
244    ///
245    /// Returns the compat session if it exists, `None` otherwise
246    ///
247    /// # Parameters
248    ///
249    /// * `id`: The ID of the compat session to lookup
250    ///
251    /// # Errors
252    ///
253    /// Returns [`Self::Error`] if the underlying repository fails
254    async fn lookup(&mut self, id: Ulid) -> Result<Option<CompatSession>, Self::Error>;
255
256    /// Start a new compat session
257    ///
258    /// Returns the newly created compat session
259    ///
260    /// # Parameters
261    ///
262    /// * `rng`: The random number generator to use
263    /// * `clock`: The clock used to generate timestamps
264    /// * `user`: The user to create the compat session for
265    /// * `device`: The device ID of this session
266    /// * `browser_session`: The browser session which created this session
267    /// * `is_synapse_admin`: Whether the session is a synapse admin session
268    /// * `human_name`: The human-readable name of the session provided by the
269    ///   client or the user
270    ///
271    /// # Errors
272    ///
273    /// Returns [`Self::Error`] if the underlying repository fails
274    #[expect(clippy::too_many_arguments)]
275    async fn add(
276        &mut self,
277        rng: &mut (dyn RngCore + Send),
278        clock: &dyn Clock,
279        user: &User,
280        device: Device,
281        browser_session: Option<&BrowserSession>,
282        is_synapse_admin: bool,
283        human_name: Option<String>,
284    ) -> Result<CompatSession, Self::Error>;
285
286    /// End a compat session
287    ///
288    /// Returns the ended compat session
289    ///
290    /// # Parameters
291    ///
292    /// * `clock`: The clock used to generate timestamps
293    /// * `compat_session`: The compat session to end
294    ///
295    /// # Errors
296    ///
297    /// Returns [`Self::Error`] if the underlying repository fails
298    async fn finish(
299        &mut self,
300        clock: &dyn Clock,
301        compat_session: CompatSession,
302    ) -> Result<CompatSession, Self::Error>;
303
304    /// Mark all the [`CompatSession`] matching the given filter as finished
305    ///
306    /// Returns the number of sessions affected
307    ///
308    /// # Parameters
309    ///
310    /// * `clock`: The clock used to generate timestamps
311    /// * `filter`: The filter to apply
312    ///
313    /// # Errors
314    ///
315    /// Returns [`Self::Error`] if the underlying repository fails
316    async fn finish_bulk(
317        &mut self,
318        clock: &dyn Clock,
319        filter: CompatSessionFilter<'_>,
320    ) -> Result<usize, Self::Error>;
321
322    /// List [`CompatSession`] with the given filter and pagination
323    ///
324    /// Returns a page of compat sessions, with the associated SSO logins if any
325    ///
326    /// # Parameters
327    ///
328    /// * `filter`: The filter to apply
329    /// * `pagination`: The pagination parameters
330    ///
331    /// # Errors
332    ///
333    /// Returns [`Self::Error`] if the underlying repository fails
334    async fn list(
335        &mut self,
336        filter: CompatSessionFilter<'_>,
337        pagination: Pagination,
338    ) -> Result<Page<(CompatSession, Option<CompatSsoLogin>)>, Self::Error>;
339
340    /// Count the number of [`CompatSession`] with the given filter
341    ///
342    /// # Parameters
343    ///
344    /// * `filter`: The filter to apply
345    ///
346    /// # Errors
347    ///
348    /// Returns [`Self::Error`] if the underlying repository fails
349    async fn count(&mut self, filter: CompatSessionFilter<'_>) -> Result<usize, Self::Error>;
350
351    /// Record a batch of [`CompatSession`] activity
352    ///
353    /// # Parameters
354    ///
355    /// * `activity`: A list of tuples containing the session ID, the last
356    ///   activity timestamp and the IP address of the client
357    ///
358    /// # Errors
359    ///
360    /// Returns [`Self::Error`] if the underlying repository fails
361    async fn record_batch_activity(
362        &mut self,
363        activity: Vec<(Ulid, DateTime<Utc>, Option<IpAddr>)>,
364    ) -> Result<(), Self::Error>;
365
366    /// Record the user agent of a compat session
367    ///
368    /// # Parameters
369    ///
370    /// * `compat_session`: The compat session to record the user agent for
371    /// * `user_agent`: The user agent to record
372    ///
373    /// # Errors
374    ///
375    /// Returns [`Self::Error`] if the underlying repository fails
376    async fn record_user_agent(
377        &mut self,
378        compat_session: CompatSession,
379        user_agent: String,
380    ) -> Result<CompatSession, Self::Error>;
381
382    /// Set the human name of a compat session
383    ///
384    /// # Parameters
385    ///
386    /// * `compat_session`: The compat session to set the human name for
387    /// * `human_name`: The human name to set
388    ///
389    /// # Errors
390    ///
391    /// Returns [`Self::Error`] if the underlying repository fails
392    async fn set_human_name(
393        &mut self,
394        compat_session: CompatSession,
395        human_name: Option<String>,
396    ) -> Result<CompatSession, Self::Error>;
397
398    /// Cleanup finished [`CompatSession`]s and their associated tokens.
399    ///
400    /// This deletes compat sessions that have been finished, along with their
401    /// associated access tokens, refresh tokens, and SSO logins.
402    ///
403    /// Returns the number of sessions deleted and the timestamp of the last
404    /// deleted session's `finished_at`, which can be used for pagination.
405    ///
406    /// # Parameters
407    ///
408    /// * `since`: Only delete sessions finished at or after this timestamp
409    /// * `until`: Only delete sessions finished before this timestamp
410    /// * `limit`: Maximum number of sessions to delete
411    ///
412    /// # Errors
413    ///
414    /// Returns [`Self::Error`] if the underlying repository fails
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    /// Clear IP addresses from sessions inactive since the threshold
423    ///
424    /// Sets `last_active_ip` to `NULL` for sessions where `last_active_at` is
425    /// before the threshold. Returns the number of sessions affected and the
426    /// last `last_active_at` timestamp processed for pagination.
427    ///
428    /// # Parameters
429    ///
430    /// * `since`: Only process sessions with `last_active_at` at or after this
431    ///   timestamp (exclusive). If `None`, starts from the beginning.
432    /// * `threshold`: Clear IPs for sessions with `last_active_at` before this
433    ///   time
434    /// * `limit`: Maximum number of sessions to update in this batch
435    ///
436    /// # Errors
437    ///
438    /// Returns [`Self::Error`] if the underlying repository fails
439    async fn cleanup_inactive_ips(
440        &mut self,
441        since: Option<DateTime<Utc>>,
442        threshold: DateTime<Utc>,
443        limit: usize,
444    ) -> Result<(usize, Option<DateTime<Utc>>), Self::Error>;
445}
446
447repository_impl!(CompatSessionRepository:
448    async fn lookup(&mut self, id: Ulid) -> Result<Option<CompatSession>, Self::Error>;
449
450    async fn add(
451        &mut self,
452        rng: &mut (dyn RngCore + Send),
453        clock: &dyn Clock,
454        user: &User,
455        device: Device,
456        browser_session: Option<&BrowserSession>,
457        is_synapse_admin: bool,
458        human_name: Option<String>,
459    ) -> Result<CompatSession, Self::Error>;
460
461    async fn finish(
462        &mut self,
463        clock: &dyn Clock,
464        compat_session: CompatSession,
465    ) -> Result<CompatSession, Self::Error>;
466
467    async fn finish_bulk(
468        &mut self,
469        clock: &dyn Clock,
470        filter: CompatSessionFilter<'_>,
471    ) -> Result<usize, Self::Error>;
472
473    async fn list(
474        &mut self,
475        filter: CompatSessionFilter<'_>,
476        pagination: Pagination,
477    ) -> Result<Page<(CompatSession, Option<CompatSsoLogin>)>, Self::Error>;
478
479    async fn count(&mut self, filter: CompatSessionFilter<'_>) -> Result<usize, Self::Error>;
480
481    async fn record_batch_activity(
482        &mut self,
483        activity: Vec<(Ulid, DateTime<Utc>, Option<IpAddr>)>,
484    ) -> Result<(), Self::Error>;
485
486    async fn record_user_agent(
487        &mut self,
488        compat_session: CompatSession,
489        user_agent: String,
490    ) -> Result<CompatSession, Self::Error>;
491
492    async fn set_human_name(
493        &mut self,
494        compat_session: CompatSession,
495        human_name: Option<String>,
496    ) -> Result<CompatSession, Self::Error>;
497
498    async fn cleanup_finished(
499        &mut self,
500        since: Option<DateTime<Utc>>,
501        until: DateTime<Utc>,
502        limit: usize,
503    ) -> Result<(usize, Option<DateTime<Utc>>), Self::Error>;
504
505    async fn cleanup_inactive_ips(
506        &mut self,
507        since: Option<DateTime<Utc>>,
508        threshold: DateTime<Utc>,
509        limit: usize,
510    ) -> Result<(usize, Option<DateTime<Utc>>), Self::Error>;
511);