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