mas_storage/user/mod.rs
1// Copyright 2025, 2026 Element Creations Ltd.
2// Copyright 2024, 2025 New Vector Ltd.
3// Copyright 2021-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
8//! Repositories to interact with entities related to user accounts
9
10use async_trait::async_trait;
11use mas_data_model::{Clock, User};
12use rand_core::RngCore;
13use ulid::Ulid;
14
15use crate::{Page, Pagination, repository_impl};
16
17mod email;
18mod password;
19mod recovery;
20mod registration;
21mod registration_token;
22mod session;
23mod terms;
24
25pub use self::{
26 email::{UserEmailFilter, UserEmailRepository},
27 password::UserPasswordRepository,
28 recovery::UserRecoveryRepository,
29 registration::UserRegistrationRepository,
30 registration_token::{UserRegistrationTokenFilter, UserRegistrationTokenRepository},
31 session::{BrowserSessionFilter, BrowserSessionRepository},
32 terms::UserTermsRepository,
33};
34
35/// The state of a user account
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub enum UserState {
38 /// The account is deactivated, it has the `deactivated_at` timestamp set
39 Deactivated,
40
41 /// The account is locked, it has the `locked_at` timestamp set
42 Locked,
43
44 /// The account is active
45 Active,
46}
47
48impl UserState {
49 /// Returns `true` if the user state is [`Locked`].
50 ///
51 /// [`Locked`]: UserState::Locked
52 #[must_use]
53 pub fn is_locked(&self) -> bool {
54 matches!(self, Self::Locked)
55 }
56
57 /// Returns `true` if the user state is [`Deactivated`].
58 ///
59 /// [`Deactivated`]: UserState::Deactivated
60 #[must_use]
61 pub fn is_deactivated(&self) -> bool {
62 matches!(self, Self::Deactivated)
63 }
64
65 /// Returns `true` if the user state is [`Active`].
66 ///
67 /// [`Active`]: UserState::Active
68 #[must_use]
69 pub fn is_active(&self) -> bool {
70 matches!(self, Self::Active)
71 }
72}
73
74/// Filter parameters for listing users
75#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
76pub struct UserFilter<'a> {
77 state: Option<UserState>,
78 can_request_admin: Option<bool>,
79 is_guest: Option<bool>,
80 search: Option<&'a str>,
81 active_oauth2_session_for_any_of_clients: Option<&'a [Ulid]>,
82 has_active_oauth2_session: Option<bool>,
83 has_active_compat_session: Option<bool>,
84}
85
86impl<'a> UserFilter<'a> {
87 /// Create a new [`UserFilter`] with default values
88 #[must_use]
89 pub fn new() -> Self {
90 Self::default()
91 }
92
93 /// Filter for active users
94 #[must_use]
95 pub fn active_only(mut self) -> Self {
96 self.state = Some(UserState::Active);
97 self
98 }
99
100 /// Filter for locked users
101 #[must_use]
102 pub fn locked_only(mut self) -> Self {
103 self.state = Some(UserState::Locked);
104 self
105 }
106
107 /// Filter for deactivated users
108 #[must_use]
109 pub fn deactivated_only(mut self) -> Self {
110 self.state = Some(UserState::Deactivated);
111 self
112 }
113
114 /// Filter for users that can request admin privileges
115 #[must_use]
116 pub fn can_request_admin_only(mut self) -> Self {
117 self.can_request_admin = Some(true);
118 self
119 }
120
121 /// Filter for users that can't request admin privileges
122 #[must_use]
123 pub fn cannot_request_admin_only(mut self) -> Self {
124 self.can_request_admin = Some(false);
125 self
126 }
127
128 /// Filter for guest users
129 #[must_use]
130 pub fn guest_only(mut self) -> Self {
131 self.is_guest = Some(true);
132 self
133 }
134
135 /// Filter for non-guest users
136 #[must_use]
137 pub fn non_guest_only(mut self) -> Self {
138 self.is_guest = Some(false);
139 self
140 }
141
142 /// Filter for users that match the given search string
143 #[must_use]
144 pub fn matching_search(mut self, search: &'a str) -> Self {
145 self.search = Some(search);
146 self
147 }
148
149 /// Filter for users which have at least one active (non-finished)
150 /// `OAuth2` session belonging to any of the given clients.
151 ///
152 /// The semantics are OR across the clients: a user matches if they have
153 /// an active session for *any* of the supplied client IDs.
154 #[must_use]
155 pub fn with_active_oauth2_session_for_any_of_clients(mut self, clients: &'a [Ulid]) -> Self {
156 self.active_oauth2_session_for_any_of_clients = Some(clients);
157 self
158 }
159
160 /// Filter for users which have (or don't have) at least one active
161 /// (non-finished) `OAuth2` session, regardless of the client.
162 #[must_use]
163 pub fn with_active_oauth2_session(mut self, has: bool) -> Self {
164 self.has_active_oauth2_session = Some(has);
165 self
166 }
167
168 /// Filter for users which have (or don't have) at least one active
169 /// (non-finished) compatibility session.
170 #[must_use]
171 pub fn with_active_compat_session(mut self, has: bool) -> Self {
172 self.has_active_compat_session = Some(has);
173 self
174 }
175
176 /// Get the state filter
177 ///
178 /// Returns [`None`] if no state filter was set
179 #[must_use]
180 pub fn state(&self) -> Option<UserState> {
181 self.state
182 }
183
184 /// Get the can request admin filter
185 ///
186 /// Returns [`None`] if no can request admin filter was set
187 #[must_use]
188 pub fn can_request_admin(&self) -> Option<bool> {
189 self.can_request_admin
190 }
191
192 /// Get the is guest filter
193 ///
194 /// Returns [`None`] if no is guest filter was set
195 #[must_use]
196 pub fn is_guest(&self) -> Option<bool> {
197 self.is_guest
198 }
199
200 /// Get the search filter
201 ///
202 /// Returns [`None`] if no search filter was set
203 #[must_use]
204 pub fn search(&self) -> Option<&'a str> {
205 self.search
206 }
207
208 /// Get the active-`OAuth2`-session-for-any-of-clients filter
209 ///
210 /// Returns [`None`] if no such filter was set
211 #[must_use]
212 pub fn active_oauth2_session_for_any_of_clients(&self) -> Option<&'a [Ulid]> {
213 self.active_oauth2_session_for_any_of_clients
214 }
215
216 /// Get the has-active-`OAuth2`-session filter
217 ///
218 /// Returns [`None`] if no such filter was set
219 #[must_use]
220 pub fn has_active_oauth2_session(&self) -> Option<bool> {
221 self.has_active_oauth2_session
222 }
223
224 /// Get the has-active-compat-session filter
225 ///
226 /// Returns [`None`] if no such filter was set
227 #[must_use]
228 pub fn has_active_compat_session(&self) -> Option<bool> {
229 self.has_active_compat_session
230 }
231}
232
233/// A [`UserRepository`] helps interacting with [`User`] saved in the storage
234/// backend
235#[async_trait]
236pub trait UserRepository: Send + Sync {
237 /// The error type returned by the repository
238 type Error;
239
240 /// Lookup a [`User`] by its ID
241 ///
242 /// Returns `None` if no [`User`] was found
243 ///
244 /// # Parameters
245 ///
246 /// * `id`: The ID of the [`User`] to lookup
247 ///
248 /// # Errors
249 ///
250 /// Returns [`Self::Error`] if the underlying repository fails
251 async fn lookup(&mut self, id: Ulid) -> Result<Option<User>, Self::Error>;
252
253 /// Find a [`User`] by its username, in a case-insensitive manner
254 ///
255 /// Returns `None` if no [`User`] was found
256 ///
257 /// # Parameters
258 ///
259 /// * `username`: The username of the [`User`] to lookup
260 ///
261 /// # Errors
262 ///
263 /// Returns [`Self::Error`] if the underlying repository fails
264 async fn find_by_username(&mut self, username: &str) -> Result<Option<User>, Self::Error>;
265
266 /// Create a new [`User`]
267 ///
268 /// Returns the newly created [`User`]
269 ///
270 /// # Parameters
271 ///
272 /// * `rng`: A random number generator to generate the [`User`] ID
273 /// * `clock`: The clock used to generate timestamps
274 /// * `username`: The username of the [`User`]
275 ///
276 /// # Errors
277 ///
278 /// Returns [`Self::Error`] if the underlying repository fails
279 async fn add(
280 &mut self,
281 rng: &mut (dyn RngCore + Send),
282 clock: &dyn Clock,
283 username: String,
284 ) -> Result<User, Self::Error>;
285
286 /// Check if a [`User`] exists
287 ///
288 /// Returns `true` if the [`User`] exists, `false` otherwise
289 ///
290 /// # Parameters
291 ///
292 /// * `username`: The username of the [`User`] to lookup
293 ///
294 /// # Errors
295 ///
296 /// Returns [`Self::Error`] if the underlying repository fails
297 async fn exists(&mut self, username: &str) -> Result<bool, Self::Error>;
298
299 /// Lock a [`User`]
300 ///
301 /// Returns the locked [`User`]
302 ///
303 /// # Parameters
304 ///
305 /// * `clock`: The clock used to generate timestamps
306 /// * `user`: The [`User`] to lock
307 ///
308 /// # Errors
309 ///
310 /// Returns [`Self::Error`] if the underlying repository fails
311 async fn lock(&mut self, clock: &dyn Clock, user: User) -> Result<User, Self::Error>;
312
313 /// Unlock a [`User`]
314 ///
315 /// Returns the unlocked [`User`]
316 ///
317 /// # Parameters
318 ///
319 /// * `user`: The [`User`] to unlock
320 ///
321 /// # Errors
322 ///
323 /// Returns [`Self::Error`] if the underlying repository fails
324 async fn unlock(&mut self, user: User) -> Result<User, Self::Error>;
325
326 /// Deactivate a [`User`]
327 ///
328 /// Returns the deactivated [`User`]
329 ///
330 /// # Parameters
331 ///
332 /// * `clock`: The clock used to generate timestamps
333 /// * `user`: The [`User`] to deactivate
334 ///
335 /// # Errors
336 ///
337 /// Returns [`Self::Error`] if the underlying repository fails
338 async fn deactivate(&mut self, clock: &dyn Clock, user: User) -> Result<User, Self::Error>;
339
340 /// Reactivate a [`User`]
341 ///
342 /// Returns the reactivated [`User`]
343 ///
344 /// # Parameters
345 ///
346 /// * `user`: The [`User`] to reactivate
347 ///
348 /// # Errors
349 ///
350 /// Returns [`Self::Error`] if the underlying repository fails
351 async fn reactivate(&mut self, user: User) -> Result<User, Self::Error>;
352
353 /// Delete all the unsupported third-party IDs of a [`User`].
354 ///
355 /// Those were imported by syn2mas and kept in case we wanted to support
356 /// them later. They still need to be cleaned up when a user deactivates
357 /// their account.
358 ///
359 /// Returns the number of deleted third-party IDs.
360 ///
361 /// # Parameters
362 ///
363 /// * `user`: The [`User`] whose unsupported third-party IDs should be
364 /// deleted
365 ///
366 /// # Errors
367 ///
368 /// Returns [`Self::Error`] if the underlying repository fails
369 async fn delete_unsupported_threepids(&mut self, user: &User) -> Result<usize, Self::Error>;
370
371 /// Set whether a [`User`] can request admin
372 ///
373 /// Returns the [`User`] with the new `can_request_admin` value
374 ///
375 /// # Parameters
376 ///
377 /// * `user`: The [`User`] to update
378 ///
379 /// # Errors
380 ///
381 /// Returns [`Self::Error`] if the underlying repository fails
382 async fn set_can_request_admin(
383 &mut self,
384 user: User,
385 can_request_admin: bool,
386 ) -> Result<User, Self::Error>;
387
388 /// List [`User`] with the given filter and pagination
389 ///
390 /// # Parameters
391 ///
392 /// * `filter`: The filter parameters
393 /// * `pagination`: The pagination parameters
394 ///
395 /// # Errors
396 ///
397 /// Returns [`Self::Error`] if the underlying repository fails
398 async fn list(
399 &mut self,
400 filter: UserFilter<'_>,
401 pagination: Pagination,
402 ) -> Result<Page<User>, Self::Error>;
403
404 /// Count the [`User`] with the given filter
405 ///
406 /// # Parameters
407 ///
408 /// * `filter`: The filter parameters
409 ///
410 /// # Errors
411 ///
412 /// Returns [`Self::Error`] if the underlying repository fails
413 async fn count(&mut self, filter: UserFilter<'_>) -> Result<usize, Self::Error>;
414
415 /// Acquire a lock on the user to make sure device operations are done in a
416 /// sequential way. The lock is released when the repository is saved or
417 /// rolled back.
418 ///
419 /// # Parameters
420 ///
421 /// * `user`: The user to lock
422 ///
423 /// # Errors
424 ///
425 /// Returns [`Self::Error`] if the underlying repository fails
426 async fn acquire_lock_for_sync(&mut self, user: &User) -> Result<(), Self::Error>;
427}
428
429repository_impl!(UserRepository:
430 async fn lookup(&mut self, id: Ulid) -> Result<Option<User>, Self::Error>;
431 async fn find_by_username(&mut self, username: &str) -> Result<Option<User>, Self::Error>;
432 async fn add(
433 &mut self,
434 rng: &mut (dyn RngCore + Send),
435 clock: &dyn Clock,
436 username: String,
437 ) -> Result<User, Self::Error>;
438 async fn exists(&mut self, username: &str) -> Result<bool, Self::Error>;
439 async fn lock(&mut self, clock: &dyn Clock, user: User) -> Result<User, Self::Error>;
440 async fn unlock(&mut self, user: User) -> Result<User, Self::Error>;
441 async fn deactivate(&mut self, clock: &dyn Clock, user: User) -> Result<User, Self::Error>;
442 async fn reactivate(&mut self, user: User) -> Result<User, Self::Error>;
443 async fn delete_unsupported_threepids(&mut self, user: &User) -> Result<usize, Self::Error>;
444 async fn set_can_request_admin(
445 &mut self,
446 user: User,
447 can_request_admin: bool,
448 ) -> Result<User, Self::Error>;
449 async fn list(
450 &mut self,
451 filter: UserFilter<'_>,
452 pagination: Pagination,
453 ) -> Result<Page<User>, Self::Error>;
454 async fn count(&mut self, filter: UserFilter<'_>) -> Result<usize, Self::Error>;
455 async fn acquire_lock_for_sync(&mut self, user: &User) -> Result<(), Self::Error>;
456);