mas_storage/compat/
session.rs

1// Copyright 2024, 2025 New Vector Ltd.
2// Copyright 2023, 2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5// Please see LICENSE files in the repository root for full details.
6
7use std::net::IpAddr;
8
9use async_trait::async_trait;
10use chrono::{DateTime, Utc};
11use mas_data_model::{BrowserSession, CompatSession, CompatSsoLogin, Device, User};
12use rand_core::RngCore;
13use ulid::Ulid;
14
15use crate::{Clock, Page, Pagination, repository_impl, user::BrowserSessionFilter};
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub enum CompatSessionState {
19    Active,
20    Finished,
21}
22
23impl CompatSessionState {
24    /// Returns [`true`] if we're looking for active sessions
25    #[must_use]
26    pub fn is_active(self) -> bool {
27        matches!(self, Self::Active)
28    }
29
30    /// Returns [`true`] if we're looking for finished sessions
31    #[must_use]
32    pub fn is_finished(self) -> bool {
33        matches!(self, Self::Finished)
34    }
35}
36
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38pub enum CompatSessionType {
39    SsoLogin,
40    Unknown,
41}
42
43impl CompatSessionType {
44    /// Returns [`true`] if we're looking for SSO logins
45    #[must_use]
46    pub fn is_sso_login(self) -> bool {
47        matches!(self, Self::SsoLogin)
48    }
49
50    /// Returns [`true`] if we're looking for unknown sessions
51    #[must_use]
52    pub fn is_unknown(self) -> bool {
53        matches!(self, Self::Unknown)
54    }
55}
56
57/// Filter parameters for listing compatibility sessions
58#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
59pub struct CompatSessionFilter<'a> {
60    user: Option<&'a User>,
61    browser_session: Option<&'a BrowserSession>,
62    browser_session_filter: Option<BrowserSessionFilter<'a>>,
63    state: Option<CompatSessionState>,
64    auth_type: Option<CompatSessionType>,
65    device: Option<&'a Device>,
66    last_active_before: Option<DateTime<Utc>>,
67    last_active_after: Option<DateTime<Utc>>,
68}
69
70impl<'a> CompatSessionFilter<'a> {
71    /// Create a new [`CompatSessionFilter`] with default values
72    #[must_use]
73    pub fn new() -> Self {
74        Self::default()
75    }
76
77    /// Set the user who owns the compatibility sessions
78    #[must_use]
79    pub fn for_user(mut self, user: &'a User) -> Self {
80        self.user = Some(user);
81        self
82    }
83
84    /// Get the user filter
85    #[must_use]
86    pub fn user(&self) -> Option<&'a User> {
87        self.user
88    }
89
90    /// Set the device filter
91    #[must_use]
92    pub fn for_device(mut self, device: &'a Device) -> Self {
93        self.device = Some(device);
94        self
95    }
96
97    /// Get the device filter
98    #[must_use]
99    pub fn device(&self) -> Option<&'a Device> {
100        self.device
101    }
102
103    /// Set the browser session filter
104    #[must_use]
105    pub fn for_browser_session(mut self, browser_session: &'a BrowserSession) -> Self {
106        self.browser_session = Some(browser_session);
107        self
108    }
109
110    /// Set the browser sessions filter
111    #[must_use]
112    pub fn for_browser_sessions(
113        mut self,
114        browser_session_filter: BrowserSessionFilter<'a>,
115    ) -> Self {
116        self.browser_session_filter = Some(browser_session_filter);
117        self
118    }
119
120    /// Get the browser session filter
121    #[must_use]
122    pub fn browser_session(&self) -> Option<&'a BrowserSession> {
123        self.browser_session
124    }
125
126    /// Get the browser sessions filter
127    #[must_use]
128    pub fn browser_session_filter(&self) -> Option<BrowserSessionFilter<'a>> {
129        self.browser_session_filter
130    }
131
132    /// Only return sessions with a last active time before the given time
133    #[must_use]
134    pub fn with_last_active_before(mut self, last_active_before: DateTime<Utc>) -> Self {
135        self.last_active_before = Some(last_active_before);
136        self
137    }
138
139    /// Only return sessions with a last active time after the given time
140    #[must_use]
141    pub fn with_last_active_after(mut self, last_active_after: DateTime<Utc>) -> Self {
142        self.last_active_after = Some(last_active_after);
143        self
144    }
145
146    /// Get the last active before filter
147    ///
148    /// Returns [`None`] if no client filter was set
149    #[must_use]
150    pub fn last_active_before(&self) -> Option<DateTime<Utc>> {
151        self.last_active_before
152    }
153
154    /// Get the last active after filter
155    ///
156    /// Returns [`None`] if no client filter was set
157    #[must_use]
158    pub fn last_active_after(&self) -> Option<DateTime<Utc>> {
159        self.last_active_after
160    }
161
162    /// Only return active compatibility sessions
163    #[must_use]
164    pub fn active_only(mut self) -> Self {
165        self.state = Some(CompatSessionState::Active);
166        self
167    }
168
169    /// Only return finished compatibility sessions
170    #[must_use]
171    pub fn finished_only(mut self) -> Self {
172        self.state = Some(CompatSessionState::Finished);
173        self
174    }
175
176    /// Get the state filter
177    #[must_use]
178    pub fn state(&self) -> Option<CompatSessionState> {
179        self.state
180    }
181
182    /// Only return SSO login compatibility sessions
183    #[must_use]
184    pub fn sso_login_only(mut self) -> Self {
185        self.auth_type = Some(CompatSessionType::SsoLogin);
186        self
187    }
188
189    /// Only return unknown compatibility sessions
190    #[must_use]
191    pub fn unknown_only(mut self) -> Self {
192        self.auth_type = Some(CompatSessionType::Unknown);
193        self
194    }
195
196    /// Get the auth type filter
197    #[must_use]
198    pub fn auth_type(&self) -> Option<CompatSessionType> {
199        self.auth_type
200    }
201}
202
203/// A [`CompatSessionRepository`] helps interacting with
204/// [`CompatSession`] saved in the storage backend
205#[async_trait]
206pub trait CompatSessionRepository: Send + Sync {
207    /// The error type returned by the repository
208    type Error;
209
210    /// Lookup a compat session by its ID
211    ///
212    /// Returns the compat session if it exists, `None` otherwise
213    ///
214    /// # Parameters
215    ///
216    /// * `id`: The ID of the compat session to lookup
217    ///
218    /// # Errors
219    ///
220    /// Returns [`Self::Error`] if the underlying repository fails
221    async fn lookup(&mut self, id: Ulid) -> Result<Option<CompatSession>, Self::Error>;
222
223    /// Start a new compat session
224    ///
225    /// Returns the newly created compat session
226    ///
227    /// # Parameters
228    ///
229    /// * `rng`: The random number generator to use
230    /// * `clock`: The clock used to generate timestamps
231    /// * `user`: The user to create the compat session for
232    /// * `device`: The device ID of this session
233    /// * `browser_session`: The browser session which created this session
234    /// * `is_synapse_admin`: Whether the session is a synapse admin session
235    /// * `human_name`: The human-readable name of the session provided by the
236    ///   client or the user
237    ///
238    /// # Errors
239    ///
240    /// Returns [`Self::Error`] if the underlying repository fails
241    #[expect(clippy::too_many_arguments)]
242    async fn add(
243        &mut self,
244        rng: &mut (dyn RngCore + Send),
245        clock: &dyn Clock,
246        user: &User,
247        device: Device,
248        browser_session: Option<&BrowserSession>,
249        is_synapse_admin: bool,
250        human_name: Option<String>,
251    ) -> Result<CompatSession, Self::Error>;
252
253    /// End a compat session
254    ///
255    /// Returns the ended compat session
256    ///
257    /// # Parameters
258    ///
259    /// * `clock`: The clock used to generate timestamps
260    /// * `compat_session`: The compat session to end
261    ///
262    /// # Errors
263    ///
264    /// Returns [`Self::Error`] if the underlying repository fails
265    async fn finish(
266        &mut self,
267        clock: &dyn Clock,
268        compat_session: CompatSession,
269    ) -> Result<CompatSession, Self::Error>;
270
271    /// Mark all the [`CompatSession`] matching the given filter as finished
272    ///
273    /// Returns the number of sessions affected
274    ///
275    /// # Parameters
276    ///
277    /// * `clock`: The clock used to generate timestamps
278    /// * `filter`: The filter to apply
279    ///
280    /// # Errors
281    ///
282    /// Returns [`Self::Error`] if the underlying repository fails
283    async fn finish_bulk(
284        &mut self,
285        clock: &dyn Clock,
286        filter: CompatSessionFilter<'_>,
287    ) -> Result<usize, Self::Error>;
288
289    /// List [`CompatSession`] with the given filter and pagination
290    ///
291    /// Returns a page of compat sessions, with the associated SSO logins if any
292    ///
293    /// # Parameters
294    ///
295    /// * `filter`: The filter to apply
296    /// * `pagination`: The pagination parameters
297    ///
298    /// # Errors
299    ///
300    /// Returns [`Self::Error`] if the underlying repository fails
301    async fn list(
302        &mut self,
303        filter: CompatSessionFilter<'_>,
304        pagination: Pagination,
305    ) -> Result<Page<(CompatSession, Option<CompatSsoLogin>)>, Self::Error>;
306
307    /// Count the number of [`CompatSession`] with the given filter
308    ///
309    /// # Parameters
310    ///
311    /// * `filter`: The filter to apply
312    ///
313    /// # Errors
314    ///
315    /// Returns [`Self::Error`] if the underlying repository fails
316    async fn count(&mut self, filter: CompatSessionFilter<'_>) -> Result<usize, Self::Error>;
317
318    /// Record a batch of [`CompatSession`] activity
319    ///
320    /// # Parameters
321    ///
322    /// * `activity`: A list of tuples containing the session ID, the last
323    ///   activity timestamp and the IP address of the client
324    ///
325    /// # Errors
326    ///
327    /// Returns [`Self::Error`] if the underlying repository fails
328    async fn record_batch_activity(
329        &mut self,
330        activity: Vec<(Ulid, DateTime<Utc>, Option<IpAddr>)>,
331    ) -> Result<(), Self::Error>;
332
333    /// Record the user agent of a compat session
334    ///
335    /// # Parameters
336    ///
337    /// * `compat_session`: The compat session to record the user agent for
338    /// * `user_agent`: The user agent to record
339    ///
340    /// # Errors
341    ///
342    /// Returns [`Self::Error`] if the underlying repository fails
343    async fn record_user_agent(
344        &mut self,
345        compat_session: CompatSession,
346        user_agent: String,
347    ) -> Result<CompatSession, Self::Error>;
348
349    /// Set the human name of a compat session
350    ///
351    /// # Parameters
352    ///
353    /// * `compat_session`: The compat session to set the human name for
354    /// * `human_name`: The human name to set
355    ///
356    /// # Errors
357    ///
358    /// Returns [`Self::Error`] if the underlying repository fails
359    async fn set_human_name(
360        &mut self,
361        compat_session: CompatSession,
362        human_name: Option<String>,
363    ) -> Result<CompatSession, Self::Error>;
364}
365
366repository_impl!(CompatSessionRepository:
367    async fn lookup(&mut self, id: Ulid) -> Result<Option<CompatSession>, Self::Error>;
368
369    async fn add(
370        &mut self,
371        rng: &mut (dyn RngCore + Send),
372        clock: &dyn Clock,
373        user: &User,
374        device: Device,
375        browser_session: Option<&BrowserSession>,
376        is_synapse_admin: bool,
377        human_name: Option<String>,
378    ) -> Result<CompatSession, Self::Error>;
379
380    async fn finish(
381        &mut self,
382        clock: &dyn Clock,
383        compat_session: CompatSession,
384    ) -> Result<CompatSession, Self::Error>;
385
386    async fn finish_bulk(
387        &mut self,
388        clock: &dyn Clock,
389        filter: CompatSessionFilter<'_>,
390    ) -> Result<usize, Self::Error>;
391
392    async fn list(
393        &mut self,
394        filter: CompatSessionFilter<'_>,
395        pagination: Pagination,
396    ) -> Result<Page<(CompatSession, Option<CompatSsoLogin>)>, Self::Error>;
397
398    async fn count(&mut self, filter: CompatSessionFilter<'_>) -> Result<usize, Self::Error>;
399
400    async fn record_batch_activity(
401        &mut self,
402        activity: Vec<(Ulid, DateTime<Utc>, Option<IpAddr>)>,
403    ) -> Result<(), Self::Error>;
404
405    async fn record_user_agent(
406        &mut self,
407        compat_session: CompatSession,
408        user_agent: String,
409    ) -> Result<CompatSession, Self::Error>;
410
411    async fn set_human_name(
412        &mut self,
413        compat_session: CompatSession,
414        human_name: Option<String>,
415    ) -> Result<CompatSession, Self::Error>;
416);