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