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    /// Revoke all the [`PersonalSession`]s matching the given filter.
91    ///
92    /// Returns the number of sessions affected
93    ///
94    /// # Parameters
95    ///
96    /// * `clock`: The clock used to generate timestamps
97    /// * `filter`: The filter to apply
98    ///
99    /// # Errors
100    ///
101    /// Returns [`Self::Error`] if the underlying repository fails
102    async fn revoke_bulk(
103        &mut self,
104        clock: &dyn Clock,
105        filter: PersonalSessionFilter<'_>,
106    ) -> Result<usize, Self::Error>;
107
108    /// List [`PersonalSession`]s matching the given filter and pagination
109    /// parameters
110    ///
111    /// # Parameters
112    ///
113    /// * `filter`: The filter parameters
114    /// * `pagination`: The pagination parameters
115    ///
116    /// # Errors
117    ///
118    /// Returns [`Self::Error`] if the underlying repository fails
119    async fn list(
120        &mut self,
121        filter: PersonalSessionFilter<'_>,
122        pagination: Pagination,
123    ) -> Result<Page<(PersonalSession, Option<PersonalAccessToken>)>, Self::Error>;
124
125    /// Count [`PersonalSession`]s matching the given filter
126    ///
127    /// # Parameters
128    ///
129    /// * `filter`: The filter parameters
130    ///
131    /// # Errors
132    ///
133    /// Returns [`Self::Error`] if the underlying repository fails
134    async fn count(&mut self, filter: PersonalSessionFilter<'_>) -> Result<usize, Self::Error>;
135
136    /// Record a batch of [`PersonalSession`] activity
137    ///
138    /// # Parameters
139    ///
140    /// * `activity`: A list of tuples containing the session ID, the last
141    ///   activity timestamp and the IP address of the client
142    ///
143    /// # Errors
144    ///
145    /// Returns [`Self::Error`] if the underlying repository fails
146    async fn record_batch_activity(
147        &mut self,
148        activity: Vec<(Ulid, DateTime<Utc>, Option<IpAddr>)>,
149    ) -> Result<(), Self::Error>;
150}
151
152repository_impl!(PersonalSessionRepository:
153    async fn lookup(&mut self, id: Ulid) -> Result<Option<PersonalSession>, Self::Error>;
154
155    async fn add(
156        &mut self,
157        rng: &mut (dyn RngCore + Send),
158        clock: &dyn Clock,
159        owner: PersonalSessionOwner,
160        actor_user: &User,
161        human_name: String,
162        scope: Scope,
163    ) -> Result<PersonalSession, Self::Error>;
164
165    async fn revoke(
166        &mut self,
167        clock: &dyn Clock,
168        personal_session: PersonalSession,
169    ) -> Result<PersonalSession, Self::Error>;
170
171    async fn revoke_bulk(
172        &mut self,
173        clock: &dyn Clock,
174        filter: PersonalSessionFilter<'_>,
175    ) -> Result<usize, Self::Error>;
176
177    async fn list(
178        &mut self,
179        filter: PersonalSessionFilter<'_>,
180        pagination: Pagination,
181    ) -> Result<Page<(PersonalSession, Option<PersonalAccessToken>)>, Self::Error>;
182
183    async fn count(&mut self, filter: PersonalSessionFilter<'_>) -> Result<usize, Self::Error>;
184
185    async fn record_batch_activity(
186        &mut self,
187        activity: Vec<(Ulid, DateTime<Utc>, Option<IpAddr>)>,
188    ) -> Result<(), Self::Error>;
189);
190
191/// Filter parameters for listing personal sessions alongside personal access
192/// tokens
193#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
194pub struct PersonalSessionFilter<'a> {
195    owner_user: Option<&'a User>,
196    owner_oauth2_client: Option<&'a Client>,
197    actor_user: Option<&'a User>,
198    device: Option<&'a Device>,
199    state: Option<PersonalSessionState>,
200    scope: Option<&'a Scope>,
201    last_active_before: Option<DateTime<Utc>>,
202    last_active_after: Option<DateTime<Utc>>,
203    expires_before: Option<DateTime<Utc>>,
204    expires_after: Option<DateTime<Utc>>,
205    expires: Option<bool>,
206}
207
208/// Filter for what state a personal session is in.
209#[derive(Clone, Copy, Debug, PartialEq, Eq)]
210pub enum PersonalSessionState {
211    /// The personal session is active, which means it either
212    /// has active access tokens or can have new access tokens generated.
213    Active,
214    /// The personal session is revoked, which means no more access tokens
215    /// can be generated and none are active.
216    Revoked,
217}
218
219impl<'a> PersonalSessionFilter<'a> {
220    /// Create a new [`PersonalSessionFilter`] with default values
221    #[must_use]
222    pub fn new() -> Self {
223        Self::default()
224    }
225
226    /// List sessions owned by a specific user
227    #[must_use]
228    pub fn for_owner_user(mut self, user: &'a User) -> Self {
229        self.owner_user = Some(user);
230        self
231    }
232
233    /// Get the owner user filter
234    ///
235    /// Returns [`None`] if no user filter was set
236    #[must_use]
237    pub fn owner_oauth2_client(&self) -> Option<&'a Client> {
238        self.owner_oauth2_client
239    }
240
241    /// List sessions owned by a specific user
242    #[must_use]
243    pub fn for_owner_oauth2_client(mut self, client: &'a Client) -> Self {
244        self.owner_oauth2_client = Some(client);
245        self
246    }
247
248    /// Get the owner user filter
249    ///
250    /// Returns [`None`] if no user filter was set
251    #[must_use]
252    pub fn owner_user(&self) -> Option<&'a User> {
253        self.owner_user
254    }
255
256    /// List sessions acting as a specific user
257    #[must_use]
258    pub fn for_actor_user(mut self, user: &'a User) -> Self {
259        self.actor_user = Some(user);
260        self
261    }
262
263    /// Get the actor user filter
264    ///
265    /// Returns [`None`] if no user filter was set
266    #[must_use]
267    pub fn actor_user(&self) -> Option<&'a User> {
268        self.actor_user
269    }
270
271    /// Only return sessions with a last active time before the given time
272    #[must_use]
273    pub fn with_last_active_before(mut self, last_active_before: DateTime<Utc>) -> Self {
274        self.last_active_before = Some(last_active_before);
275        self
276    }
277
278    /// Only return sessions with a last active time after the given time
279    #[must_use]
280    pub fn with_last_active_after(mut self, last_active_after: DateTime<Utc>) -> Self {
281        self.last_active_after = Some(last_active_after);
282        self
283    }
284
285    /// Get the last active before filter
286    ///
287    /// Returns [`None`] if no client filter was set
288    #[must_use]
289    pub fn last_active_before(&self) -> Option<DateTime<Utc>> {
290        self.last_active_before
291    }
292
293    /// Get the last active after filter
294    ///
295    /// Returns [`None`] if no client filter was set
296    #[must_use]
297    pub fn last_active_after(&self) -> Option<DateTime<Utc>> {
298        self.last_active_after
299    }
300
301    /// Only return active sessions
302    #[must_use]
303    pub fn active_only(mut self) -> Self {
304        self.state = Some(PersonalSessionState::Active);
305        self
306    }
307
308    /// Only return finished sessions
309    #[must_use]
310    pub fn finished_only(mut self) -> Self {
311        self.state = Some(PersonalSessionState::Revoked);
312        self
313    }
314
315    /// Get the state filter
316    ///
317    /// Returns [`None`] if no state filter was set
318    #[must_use]
319    pub fn state(&self) -> Option<PersonalSessionState> {
320        self.state
321    }
322
323    /// Only return sessions with the given scope
324    #[must_use]
325    pub fn with_scope(mut self, scope: &'a Scope) -> Self {
326        self.scope = Some(scope);
327        self
328    }
329
330    /// Get the scope filter
331    ///
332    /// Returns [`None`] if no scope filter was set
333    #[must_use]
334    pub fn scope(&self) -> Option<&'a Scope> {
335        self.scope
336    }
337
338    /// Only return sessions that have the given device in their scope
339    #[must_use]
340    pub fn for_device(mut self, device: &'a Device) -> Self {
341        self.device = Some(device);
342        self
343    }
344
345    /// Get the device filter
346    ///
347    /// Returns [`None`] if no device filter was set
348    #[must_use]
349    pub fn device(&self) -> Option<&'a Device> {
350        self.device
351    }
352
353    /// Only return sessions whose access tokens expire before the given time
354    #[must_use]
355    pub fn with_expires_before(mut self, expires_before: DateTime<Utc>) -> Self {
356        self.expires_before = Some(expires_before);
357        self
358    }
359
360    /// Get the expires before filter
361    ///
362    /// Returns [`None`] if no expires before filter was set
363    #[must_use]
364    pub fn expires_before(&self) -> Option<DateTime<Utc>> {
365        self.expires_before
366    }
367
368    /// Only return sessions whose access tokens expire after the given time
369    #[must_use]
370    pub fn with_expires_after(mut self, expires_after: DateTime<Utc>) -> Self {
371        self.expires_after = Some(expires_after);
372        self
373    }
374
375    /// Get the expires after filter
376    ///
377    /// Returns [`None`] if no expires after filter was set
378    #[must_use]
379    pub fn expires_after(&self) -> Option<DateTime<Utc>> {
380        self.expires_after
381    }
382
383    /// Only return sessions whose access tokens have, or don't have,
384    /// an expiry time set
385    #[must_use]
386    pub fn with_expires(mut self, expires: bool) -> Self {
387        self.expires = Some(expires);
388        self
389    }
390
391    /// Get the expires filter
392    ///
393    /// Returns [`None`] if no expires filter was set
394    #[must_use]
395    pub fn expires(&self) -> Option<bool> {
396        self.expires
397    }
398}