Skip to main content

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}