mas_storage/compat/
session.rs

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