mas_storage/user/mod.rs
1// Copyright 2024, 2025 New Vector Ltd.
2// Copyright 2021-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
7//! Repositories to interact with entities related to user accounts
8
9use async_trait::async_trait;
10use mas_data_model::User;
11use rand_core::RngCore;
12use ulid::Ulid;
13
14use crate::{Clock, Page, Pagination, repository_impl};
15
16mod email;
17mod password;
18mod recovery;
19mod registration;
20mod registration_token;
21mod session;
22mod terms;
23
24pub use self::{
25 email::{UserEmailFilter, UserEmailRepository},
26 password::UserPasswordRepository,
27 recovery::UserRecoveryRepository,
28 registration::UserRegistrationRepository,
29 registration_token::{UserRegistrationTokenFilter, UserRegistrationTokenRepository},
30 session::{BrowserSessionFilter, BrowserSessionRepository},
31 terms::UserTermsRepository,
32};
33
34/// The state of a user account
35#[derive(Clone, Copy, Debug, PartialEq, Eq)]
36pub enum UserState {
37 /// The account is deactivated, it has the `deactivated_at` timestamp set
38 Deactivated,
39
40 /// The account is locked, it has the `locked_at` timestamp set
41 Locked,
42
43 /// The account is active
44 Active,
45}
46
47impl UserState {
48 /// Returns `true` if the user state is [`Locked`].
49 ///
50 /// [`Locked`]: UserState::Locked
51 #[must_use]
52 pub fn is_locked(&self) -> bool {
53 matches!(self, Self::Locked)
54 }
55
56 /// Returns `true` if the user state is [`Deactivated`].
57 ///
58 /// [`Deactivated`]: UserState::Deactivated
59 #[must_use]
60 pub fn is_deactivated(&self) -> bool {
61 matches!(self, Self::Deactivated)
62 }
63
64 /// Returns `true` if the user state is [`Active`].
65 ///
66 /// [`Active`]: UserState::Active
67 #[must_use]
68 pub fn is_active(&self) -> bool {
69 matches!(self, Self::Active)
70 }
71}
72
73/// Filter parameters for listing users
74#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
75pub struct UserFilter<'a> {
76 state: Option<UserState>,
77 can_request_admin: Option<bool>,
78 _phantom: std::marker::PhantomData<&'a ()>,
79}
80
81impl UserFilter<'_> {
82 /// Create a new [`UserFilter`] with default values
83 #[must_use]
84 pub fn new() -> Self {
85 Self::default()
86 }
87
88 /// Filter for active users
89 #[must_use]
90 pub fn active_only(mut self) -> Self {
91 self.state = Some(UserState::Active);
92 self
93 }
94
95 /// Filter for locked users
96 #[must_use]
97 pub fn locked_only(mut self) -> Self {
98 self.state = Some(UserState::Locked);
99 self
100 }
101
102 /// Filter for deactivated users
103 #[must_use]
104 pub fn deactivated_only(mut self) -> Self {
105 self.state = Some(UserState::Deactivated);
106 self
107 }
108
109 /// Filter for users that can request admin privileges
110 #[must_use]
111 pub fn can_request_admin_only(mut self) -> Self {
112 self.can_request_admin = Some(true);
113 self
114 }
115
116 /// Filter for users that can't request admin privileges
117 #[must_use]
118 pub fn cannot_request_admin_only(mut self) -> Self {
119 self.can_request_admin = Some(false);
120 self
121 }
122
123 /// Get the state filter
124 ///
125 /// Returns [`None`] if no state filter was set
126 #[must_use]
127 pub fn state(&self) -> Option<UserState> {
128 self.state
129 }
130
131 /// Get the can request admin filter
132 ///
133 /// Returns [`None`] if no can request admin filter was set
134 #[must_use]
135 pub fn can_request_admin(&self) -> Option<bool> {
136 self.can_request_admin
137 }
138}
139
140/// A [`UserRepository`] helps interacting with [`User`] saved in the storage
141/// backend
142#[async_trait]
143pub trait UserRepository: Send + Sync {
144 /// The error type returned by the repository
145 type Error;
146
147 /// Lookup a [`User`] by its ID
148 ///
149 /// Returns `None` if no [`User`] was found
150 ///
151 /// # Parameters
152 ///
153 /// * `id`: The ID of the [`User`] to lookup
154 ///
155 /// # Errors
156 ///
157 /// Returns [`Self::Error`] if the underlying repository fails
158 async fn lookup(&mut self, id: Ulid) -> Result<Option<User>, Self::Error>;
159
160 /// Find a [`User`] by its username, in a case-insensitive manner
161 ///
162 /// Returns `None` if no [`User`] was found
163 ///
164 /// # Parameters
165 ///
166 /// * `username`: The username of the [`User`] to lookup
167 ///
168 /// # Errors
169 ///
170 /// Returns [`Self::Error`] if the underlying repository fails
171 async fn find_by_username(&mut self, username: &str) -> Result<Option<User>, Self::Error>;
172
173 /// Create a new [`User`]
174 ///
175 /// Returns the newly created [`User`]
176 ///
177 /// # Parameters
178 ///
179 /// * `rng`: A random number generator to generate the [`User`] ID
180 /// * `clock`: The clock used to generate timestamps
181 /// * `username`: The username of the [`User`]
182 ///
183 /// # Errors
184 ///
185 /// Returns [`Self::Error`] if the underlying repository fails
186 async fn add(
187 &mut self,
188 rng: &mut (dyn RngCore + Send),
189 clock: &dyn Clock,
190 username: String,
191 ) -> Result<User, Self::Error>;
192
193 /// Check if a [`User`] exists
194 ///
195 /// Returns `true` if the [`User`] exists, `false` otherwise
196 ///
197 /// # Parameters
198 ///
199 /// * `username`: The username of the [`User`] to lookup
200 ///
201 /// # Errors
202 ///
203 /// Returns [`Self::Error`] if the underlying repository fails
204 async fn exists(&mut self, username: &str) -> Result<bool, Self::Error>;
205
206 /// Lock a [`User`]
207 ///
208 /// Returns the locked [`User`]
209 ///
210 /// # Parameters
211 ///
212 /// * `clock`: The clock used to generate timestamps
213 /// * `user`: The [`User`] to lock
214 ///
215 /// # Errors
216 ///
217 /// Returns [`Self::Error`] if the underlying repository fails
218 async fn lock(&mut self, clock: &dyn Clock, user: User) -> Result<User, Self::Error>;
219
220 /// Unlock a [`User`]
221 ///
222 /// Returns the unlocked [`User`]
223 ///
224 /// # Parameters
225 ///
226 /// * `user`: The [`User`] to unlock
227 ///
228 /// # Errors
229 ///
230 /// Returns [`Self::Error`] if the underlying repository fails
231 async fn unlock(&mut self, user: User) -> Result<User, Self::Error>;
232
233 /// Deactivate a [`User`]
234 ///
235 /// Returns the deactivated [`User`]
236 ///
237 /// # Parameters
238 ///
239 /// * `clock`: The clock used to generate timestamps
240 /// * `user`: The [`User`] to deactivate
241 ///
242 /// # Errors
243 ///
244 /// Returns [`Self::Error`] if the underlying repository fails
245 async fn deactivate(&mut self, clock: &dyn Clock, user: User) -> Result<User, Self::Error>;
246
247 /// Set whether a [`User`] can request admin
248 ///
249 /// Returns the [`User`] with the new `can_request_admin` value
250 ///
251 /// # Parameters
252 ///
253 /// * `user`: The [`User`] to update
254 ///
255 /// # Errors
256 ///
257 /// Returns [`Self::Error`] if the underlying repository fails
258 async fn set_can_request_admin(
259 &mut self,
260 user: User,
261 can_request_admin: bool,
262 ) -> Result<User, Self::Error>;
263
264 /// List [`User`] with the given filter and pagination
265 ///
266 /// # Parameters
267 ///
268 /// * `filter`: The filter parameters
269 /// * `pagination`: The pagination parameters
270 ///
271 /// # Errors
272 ///
273 /// Returns [`Self::Error`] if the underlying repository fails
274 async fn list(
275 &mut self,
276 filter: UserFilter<'_>,
277 pagination: Pagination,
278 ) -> Result<Page<User>, Self::Error>;
279
280 /// Count the [`User`] with the given filter
281 ///
282 /// # Parameters
283 ///
284 /// * `filter`: The filter parameters
285 ///
286 /// # Errors
287 ///
288 /// Returns [`Self::Error`] if the underlying repository fails
289 async fn count(&mut self, filter: UserFilter<'_>) -> Result<usize, Self::Error>;
290
291 /// Acquire a lock on the user to make sure device operations are done in a
292 /// sequential way. The lock is released when the repository is saved or
293 /// rolled back.
294 ///
295 /// # Parameters
296 ///
297 /// * `user`: The user to lock
298 ///
299 /// # Errors
300 ///
301 /// Returns [`Self::Error`] if the underlying repository fails
302 async fn acquire_lock_for_sync(&mut self, user: &User) -> Result<(), Self::Error>;
303}
304
305repository_impl!(UserRepository:
306 async fn lookup(&mut self, id: Ulid) -> Result<Option<User>, Self::Error>;
307 async fn find_by_username(&mut self, username: &str) -> Result<Option<User>, Self::Error>;
308 async fn add(
309 &mut self,
310 rng: &mut (dyn RngCore + Send),
311 clock: &dyn Clock,
312 username: String,
313 ) -> Result<User, Self::Error>;
314 async fn exists(&mut self, username: &str) -> Result<bool, Self::Error>;
315 async fn lock(&mut self, clock: &dyn Clock, user: User) -> Result<User, Self::Error>;
316 async fn unlock(&mut self, user: User) -> Result<User, Self::Error>;
317 async fn deactivate(&mut self, clock: &dyn Clock, user: User) -> Result<User, Self::Error>;
318 async fn set_can_request_admin(
319 &mut self,
320 user: User,
321 can_request_admin: bool,
322 ) -> Result<User, Self::Error>;
323 async fn list(
324 &mut self,
325 filter: UserFilter<'_>,
326 pagination: Pagination,
327 ) -> Result<Page<User>, Self::Error>;
328 async fn count(&mut self, filter: UserFilter<'_>) -> Result<usize, Self::Error>;
329 async fn acquire_lock_for_sync(&mut self, user: &User) -> Result<(), Self::Error>;
330);