mas_storage/personal/
session.rs

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