mas_storage/personal/session.rs
1// Copyright 2025, 2026 Element Creations Ltd.
2// Copyright 2025 New Vector Ltd.
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
7use std::net::IpAddr;
8
9use async_trait::async_trait;
10use chrono::{DateTime, Utc};
11use mas_data_model::{
12 Client, Clock, Device, User,
13 personal::{
14 PersonalAccessToken,
15 session::{PersonalSession, PersonalSessionOwner},
16 },
17};
18use oauth2_types::scope::Scope;
19use rand_core::Rng;
20use ulid::Ulid;
21
22use crate::{Page, Pagination, repository_impl};
23
24/// A [`PersonalSessionRepository`] helps interacting with
25/// [`PersonalSession`] saved in the storage backend
26#[async_trait]
27pub trait PersonalSessionRepository: Send + Sync {
28 /// The error type returned by the repository
29 type Error;
30
31 /// Lookup a Personal session by its ID
32 ///
33 /// Returns the Personal session if it exists, `None` otherwise
34 ///
35 /// # Parameters
36 ///
37 /// * `id`: The ID of the Personal session to lookup
38 ///
39 /// # Errors
40 ///
41 /// Returns [`Self::Error`] if the underlying repository fails
42 async fn lookup(&mut self, id: Ulid) -> Result<Option<PersonalSession>, Self::Error>;
43
44 /// Start a new Personal session
45 ///
46 /// Returns the newly created Personal session
47 ///
48 /// # Parameters
49 ///
50 /// * `rng`: The random number generator to use
51 /// * `clock`: The clock used to generate timestamps
52 /// * `owner_user`: The user that will own the personal session
53 /// * `actor_user`: The user that will be represented by the personal
54 /// session
55 /// * `device`: The device ID of this session
56 /// * `human_name`: The human-readable name of the session provided by the
57 /// client or the user
58 /// * `scope`: The [`Scope`] of the [`PersonalSession`]
59 ///
60 /// # Errors
61 ///
62 /// Returns [`Self::Error`] if the underlying repository fails
63 async fn add(
64 &mut self,
65 rng: &mut (dyn Rng + Send),
66 clock: &dyn Clock,
67 owner: PersonalSessionOwner,
68 actor_user: &User,
69 human_name: String,
70 scope: Scope,
71 ) -> Result<PersonalSession, Self::Error>;
72
73 /// End a Personal session
74 ///
75 /// Returns the ended Personal session
76 ///
77 /// # Parameters
78 ///
79 /// * `clock`: The clock used to generate timestamps
80 /// * `Personal_session`: The Personal session to end
81 ///
82 /// # Errors
83 ///
84 /// Returns [`Self::Error`] if the underlying repository fails
85 async fn revoke(
86 &mut self,
87 clock: &dyn Clock,
88 personal_session: PersonalSession,
89 ) -> Result<PersonalSession, Self::Error>;
90
91 /// Revoke all the [`PersonalSession`]s matching the given filter.
92 ///
93 /// Returns the number of sessions affected
94 ///
95 /// # Parameters
96 ///
97 /// * `clock`: The clock used to generate timestamps
98 /// * `filter`: The filter to apply
99 ///
100 /// # Errors
101 ///
102 /// Returns [`Self::Error`] if the underlying repository fails
103 async fn revoke_bulk(
104 &mut self,
105 clock: &dyn Clock,
106 filter: PersonalSessionFilter<'_>,
107 ) -> Result<usize, Self::Error>;
108
109 /// List [`PersonalSession`]s matching the given filter and pagination
110 /// parameters
111 ///
112 /// # Parameters
113 ///
114 /// * `filter`: The filter parameters
115 /// * `pagination`: The pagination parameters
116 ///
117 /// # Errors
118 ///
119 /// Returns [`Self::Error`] if the underlying repository fails
120 async fn list(
121 &mut self,
122 filter: PersonalSessionFilter<'_>,
123 pagination: Pagination,
124 ) -> Result<Page<(PersonalSession, Option<PersonalAccessToken>)>, Self::Error>;
125
126 /// Count [`PersonalSession`]s matching the given filter
127 ///
128 /// # Parameters
129 ///
130 /// * `filter`: The filter parameters
131 ///
132 /// # Errors
133 ///
134 /// Returns [`Self::Error`] if the underlying repository fails
135 async fn count(&mut self, filter: PersonalSessionFilter<'_>) -> Result<usize, Self::Error>;
136
137 /// Record a batch of [`PersonalSession`] activity
138 ///
139 /// # Parameters
140 ///
141 /// * `activity`: A list of tuples containing the session ID, the last
142 /// activity timestamp and the IP address of the client
143 ///
144 /// # Errors
145 ///
146 /// Returns [`Self::Error`] if the underlying repository fails
147 async fn record_batch_activity(
148 &mut self,
149 activity: Vec<(Ulid, DateTime<Utc>, Option<IpAddr>)>,
150 ) -> Result<(), Self::Error>;
151}
152
153repository_impl!(PersonalSessionRepository:
154 async fn lookup(&mut self, id: Ulid) -> Result<Option<PersonalSession>, Self::Error>;
155
156 async fn add(
157 &mut self,
158 rng: &mut (dyn Rng + Send),
159 clock: &dyn Clock,
160 owner: PersonalSessionOwner,
161 actor_user: &User,
162 human_name: String,
163 scope: Scope,
164 ) -> Result<PersonalSession, Self::Error>;
165
166 async fn revoke(
167 &mut self,
168 clock: &dyn Clock,
169 personal_session: PersonalSession,
170 ) -> Result<PersonalSession, Self::Error>;
171
172 async fn revoke_bulk(
173 &mut self,
174 clock: &dyn Clock,
175 filter: PersonalSessionFilter<'_>,
176 ) -> Result<usize, Self::Error>;
177
178 async fn list(
179 &mut self,
180 filter: PersonalSessionFilter<'_>,
181 pagination: Pagination,
182 ) -> Result<Page<(PersonalSession, Option<PersonalAccessToken>)>, Self::Error>;
183
184 async fn count(&mut self, filter: PersonalSessionFilter<'_>) -> Result<usize, Self::Error>;
185
186 async fn record_batch_activity(
187 &mut self,
188 activity: Vec<(Ulid, DateTime<Utc>, Option<IpAddr>)>,
189 ) -> Result<(), Self::Error>;
190);
191
192/// Filter parameters for listing personal sessions alongside personal access
193/// tokens
194#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
195pub struct PersonalSessionFilter<'a> {
196 owner_user: Option<&'a User>,
197 owner_oauth2_client: Option<&'a Client>,
198 actor_user: Option<&'a User>,
199 device: Option<&'a Device>,
200 state: Option<PersonalSessionState>,
201 scope: Option<&'a Scope>,
202 last_active_before: Option<DateTime<Utc>>,
203 last_active_after: Option<DateTime<Utc>>,
204 expires_before: Option<DateTime<Utc>>,
205 expires_after: Option<DateTime<Utc>>,
206 expires: Option<bool>,
207}
208
209/// Filter for what state a personal session is in.
210#[derive(Clone, Copy, Debug, PartialEq, Eq)]
211pub enum PersonalSessionState {
212 /// The personal session is active, which means it either
213 /// has active access tokens or can have new access tokens generated.
214 Active,
215 /// The personal session is revoked, which means no more access tokens
216 /// can be generated and none are active.
217 Revoked,
218}
219
220impl<'a> PersonalSessionFilter<'a> {
221 /// Create a new [`PersonalSessionFilter`] with default values
222 #[must_use]
223 pub fn new() -> Self {
224 Self::default()
225 }
226
227 /// List sessions owned by a specific user
228 #[must_use]
229 pub fn for_owner_user(mut self, user: &'a User) -> Self {
230 self.owner_user = Some(user);
231 self
232 }
233
234 /// Get the owner user filter
235 ///
236 /// Returns [`None`] if no user filter was set
237 #[must_use]
238 pub fn owner_oauth2_client(&self) -> Option<&'a Client> {
239 self.owner_oauth2_client
240 }
241
242 /// List sessions owned by a specific user
243 #[must_use]
244 pub fn for_owner_oauth2_client(mut self, client: &'a Client) -> Self {
245 self.owner_oauth2_client = Some(client);
246 self
247 }
248
249 /// Get the owner user filter
250 ///
251 /// Returns [`None`] if no user filter was set
252 #[must_use]
253 pub fn owner_user(&self) -> Option<&'a User> {
254 self.owner_user
255 }
256
257 /// List sessions acting as a specific user
258 #[must_use]
259 pub fn for_actor_user(mut self, user: &'a User) -> Self {
260 self.actor_user = Some(user);
261 self
262 }
263
264 /// Get the actor user filter
265 ///
266 /// Returns [`None`] if no user filter was set
267 #[must_use]
268 pub fn actor_user(&self) -> Option<&'a User> {
269 self.actor_user
270 }
271
272 /// Only return sessions with a last active time before the given time
273 #[must_use]
274 pub fn with_last_active_before(mut self, last_active_before: DateTime<Utc>) -> Self {
275 self.last_active_before = Some(last_active_before);
276 self
277 }
278
279 /// Only return sessions with a last active time after the given time
280 #[must_use]
281 pub fn with_last_active_after(mut self, last_active_after: DateTime<Utc>) -> Self {
282 self.last_active_after = Some(last_active_after);
283 self
284 }
285
286 /// Get the last active before filter
287 ///
288 /// Returns [`None`] if no client filter was set
289 #[must_use]
290 pub fn last_active_before(&self) -> Option<DateTime<Utc>> {
291 self.last_active_before
292 }
293
294 /// Get the last active after filter
295 ///
296 /// Returns [`None`] if no client filter was set
297 #[must_use]
298 pub fn last_active_after(&self) -> Option<DateTime<Utc>> {
299 self.last_active_after
300 }
301
302 /// Only return active sessions
303 #[must_use]
304 pub fn active_only(mut self) -> Self {
305 self.state = Some(PersonalSessionState::Active);
306 self
307 }
308
309 /// Only return finished sessions
310 #[must_use]
311 pub fn finished_only(mut self) -> Self {
312 self.state = Some(PersonalSessionState::Revoked);
313 self
314 }
315
316 /// Get the state filter
317 ///
318 /// Returns [`None`] if no state filter was set
319 #[must_use]
320 pub fn state(&self) -> Option<PersonalSessionState> {
321 self.state
322 }
323
324 /// Only return sessions with the given scope
325 #[must_use]
326 pub fn with_scope(mut self, scope: &'a Scope) -> Self {
327 self.scope = Some(scope);
328 self
329 }
330
331 /// Get the scope filter
332 ///
333 /// Returns [`None`] if no scope filter was set
334 #[must_use]
335 pub fn scope(&self) -> Option<&'a Scope> {
336 self.scope
337 }
338
339 /// Only return sessions that have the given device in their scope
340 #[must_use]
341 pub fn for_device(mut self, device: &'a Device) -> Self {
342 self.device = Some(device);
343 self
344 }
345
346 /// Get the device filter
347 ///
348 /// Returns [`None`] if no device filter was set
349 #[must_use]
350 pub fn device(&self) -> Option<&'a Device> {
351 self.device
352 }
353
354 /// Only return sessions whose access tokens expire before the given time
355 #[must_use]
356 pub fn with_expires_before(mut self, expires_before: DateTime<Utc>) -> Self {
357 self.expires_before = Some(expires_before);
358 self
359 }
360
361 /// Get the expires before filter
362 ///
363 /// Returns [`None`] if no expires before filter was set
364 #[must_use]
365 pub fn expires_before(&self) -> Option<DateTime<Utc>> {
366 self.expires_before
367 }
368
369 /// Only return sessions whose access tokens expire after the given time
370 #[must_use]
371 pub fn with_expires_after(mut self, expires_after: DateTime<Utc>) -> Self {
372 self.expires_after = Some(expires_after);
373 self
374 }
375
376 /// Get the expires after filter
377 ///
378 /// Returns [`None`] if no expires after filter was set
379 #[must_use]
380 pub fn expires_after(&self) -> Option<DateTime<Utc>> {
381 self.expires_after
382 }
383
384 /// Only return sessions whose access tokens have, or don't have,
385 /// an expiry time set
386 #[must_use]
387 pub fn with_expires(mut self, expires: bool) -> Self {
388 self.expires = Some(expires);
389 self
390 }
391
392 /// Get the expires filter
393 ///
394 /// Returns [`None`] if no expires filter was set
395 #[must_use]
396 pub fn expires(&self) -> Option<bool> {
397 self.expires
398 }
399}