Skip to main content

mas_storage/upstream_oauth2/
link.rs

1// Copyright 2025, 2026 Element Creations Ltd.
2// Copyright 2024, 2025 New Vector Ltd.
3// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
4//
5// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
6// Please see LICENSE files in the repository root for full details.
7
8use async_trait::async_trait;
9use mas_data_model::{Clock, UpstreamOAuthLink, UpstreamOAuthProvider, User};
10use rand_core::RngCore;
11use ulid::Ulid;
12
13use crate::{Pagination, pagination::Page, repository_impl};
14
15/// Filter parameters for listing upstream OAuth links
16#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
17pub struct UpstreamOAuthLinkFilter<'a> {
18    // XXX: we might also want to filter for links without a user linked to them
19    user: Option<&'a User>,
20    provider: Option<&'a UpstreamOAuthProvider>,
21    provider_enabled: Option<bool>,
22    subject: Option<&'a str>,
23    human_account_name: Option<&'a str>,
24}
25
26impl<'a> UpstreamOAuthLinkFilter<'a> {
27    /// Create a new [`UpstreamOAuthLinkFilter`] with default values
28    #[must_use]
29    pub fn new() -> Self {
30        Self::default()
31    }
32
33    /// Set the user who owns the upstream OAuth links
34    #[must_use]
35    pub fn for_user(mut self, user: &'a User) -> Self {
36        self.user = Some(user);
37        self
38    }
39
40    /// Get the user filter
41    ///
42    /// Returns [`None`] if no filter was set
43    #[must_use]
44    pub fn user(&self) -> Option<&User> {
45        self.user
46    }
47
48    /// Set the upstream OAuth provider for which to list links
49    #[must_use]
50    pub fn for_provider(mut self, provider: &'a UpstreamOAuthProvider) -> Self {
51        self.provider = Some(provider);
52        self
53    }
54
55    /// Get the upstream OAuth provider filter
56    ///
57    /// Returns [`None`] if no filter was set
58    #[must_use]
59    pub fn provider(&self) -> Option<&UpstreamOAuthProvider> {
60        self.provider
61    }
62
63    /// Set whether to filter for enabled providers
64    #[must_use]
65    pub const fn enabled_providers_only(mut self) -> Self {
66        self.provider_enabled = Some(true);
67        self
68    }
69
70    /// Set whether to filter for disabled providers
71    #[must_use]
72    pub const fn disabled_providers_only(mut self) -> Self {
73        self.provider_enabled = Some(false);
74        self
75    }
76
77    /// Get the provider enabled filter
78    #[must_use]
79    pub const fn provider_enabled(&self) -> Option<bool> {
80        self.provider_enabled
81    }
82
83    /// Set the subject filter
84    #[must_use]
85    pub const fn for_subject(mut self, subject: &'a str) -> Self {
86        self.subject = Some(subject);
87        self
88    }
89
90    /// Get the subject filter
91    #[must_use]
92    pub const fn subject(&self) -> Option<&str> {
93        self.subject
94    }
95
96    /// Only return links whose `human_account_name` matches the given substring
97    /// (case-insensitive)
98    #[must_use]
99    pub fn matching_human_account_name(mut self, human_account_name: &'a str) -> Self {
100        self.human_account_name = Some(human_account_name);
101        self
102    }
103
104    /// Get the human account name filter
105    ///
106    /// Returns [`None`] if no filter was set
107    #[must_use]
108    pub fn human_account_name(&self) -> Option<&'a str> {
109        self.human_account_name
110    }
111}
112
113/// An [`UpstreamOAuthLinkRepository`] helps interacting with
114/// [`UpstreamOAuthLink`] with the storage backend
115#[async_trait]
116pub trait UpstreamOAuthLinkRepository: Send + Sync {
117    /// The error type returned by the repository
118    type Error;
119
120    /// Lookup an upstream OAuth link by its ID
121    ///
122    /// Returns `None` if the link does not exist
123    ///
124    /// # Parameters
125    ///
126    /// * `id`: The ID of the upstream OAuth link to lookup
127    ///
128    /// # Errors
129    ///
130    /// Returns [`Self::Error`] if the underlying repository fails
131    async fn lookup(&mut self, id: Ulid) -> Result<Option<UpstreamOAuthLink>, Self::Error>;
132
133    /// Find an upstream OAuth link for a provider by its subject
134    ///
135    /// Returns `None` if no matching upstream OAuth link was found
136    ///
137    /// # Parameters
138    ///
139    /// * `upstream_oauth_provider`: The upstream OAuth provider on which to
140    ///   find the link
141    /// * `subject`: The subject of the upstream OAuth link to find
142    ///
143    /// # Errors
144    ///
145    /// Returns [`Self::Error`] if the underlying repository fails
146    async fn find_by_subject(
147        &mut self,
148        upstream_oauth_provider: &UpstreamOAuthProvider,
149        subject: &str,
150    ) -> Result<Option<UpstreamOAuthLink>, Self::Error>;
151
152    /// Add a new upstream OAuth link
153    ///
154    /// Returns the newly created upstream OAuth link
155    ///
156    /// # Parameters
157    ///
158    /// * `rng`: The random number generator to use
159    /// * `clock`: The clock used to generate timestamps
160    /// * `upsream_oauth_provider`: The upstream OAuth provider for which to
161    ///   create the link
162    /// * `subject`: The subject of the upstream OAuth link to create
163    /// * `human_account_name`: A human-readable name for the upstream account
164    ///
165    /// # Errors
166    ///
167    /// Returns [`Self::Error`] if the underlying repository fails
168    async fn add(
169        &mut self,
170        rng: &mut (dyn RngCore + Send),
171        clock: &dyn Clock,
172        upstream_oauth_provider: &UpstreamOAuthProvider,
173        subject: String,
174        human_account_name: Option<String>,
175    ) -> Result<UpstreamOAuthLink, Self::Error>;
176
177    /// Associate an upstream OAuth link to a user
178    ///
179    /// Returns the updated upstream OAuth link
180    ///
181    /// # Parameters
182    ///
183    /// * `upstream_oauth_link`: The upstream OAuth link to update
184    /// * `user`: The user to associate to the upstream OAuth link
185    ///
186    /// # Errors
187    ///
188    /// Returns [`Self::Error`] if the underlying repository fails
189    async fn associate_to_user(
190        &mut self,
191        upstream_oauth_link: &UpstreamOAuthLink,
192        user: &User,
193    ) -> Result<(), Self::Error>;
194
195    /// List [`UpstreamOAuthLink`] with the given filter and pagination
196    ///
197    /// # Parameters
198    ///
199    /// * `filter`: The filter to apply
200    /// * `pagination`: The pagination parameters
201    ///
202    /// # Errors
203    ///
204    /// Returns [`Self::Error`] if the underlying repository fails
205    async fn list(
206        &mut self,
207        filter: UpstreamOAuthLinkFilter<'_>,
208        pagination: Pagination,
209    ) -> Result<Page<UpstreamOAuthLink>, Self::Error>;
210
211    /// Count the number of [`UpstreamOAuthLink`] with the given filter
212    ///
213    /// # Parameters
214    ///
215    /// * `filter`: The filter to apply
216    ///
217    /// # Errors
218    ///
219    /// Returns [`Self::Error`] if the underlying repository fails
220    async fn count(&mut self, filter: UpstreamOAuthLinkFilter<'_>) -> Result<usize, Self::Error>;
221
222    /// Delete a [`UpstreamOAuthLink`]
223    ///
224    /// # Parameters
225    ///
226    /// * `clock`: The clock used to generate timestamps
227    /// * `upstream_oauth_link`: The [`UpstreamOAuthLink`] to delete
228    ///
229    /// # Errors
230    ///
231    /// Returns [`Self::Error`] if the underlying repository fails
232    async fn remove(
233        &mut self,
234        clock: &dyn Clock,
235        upstream_oauth_link: UpstreamOAuthLink,
236    ) -> Result<(), Self::Error>;
237
238    /// Cleanup orphaned upstream OAuth links
239    ///
240    /// This will delete orphaned links (where `user_id IS NULL`) with IDs up to
241    /// and including `until`. Uses ULID cursor-based pagination for efficiency.
242    ///
243    /// Returns the number of links deleted and the cursor for the next batch
244    ///
245    /// # Parameters
246    ///
247    /// * `since`: The cursor to start from (exclusive), or `None` to start from
248    ///   the beginning
249    /// * `until`: The maximum ULID to delete (inclusive upper bound)
250    /// * `limit`: The maximum number of links to delete in this batch
251    ///
252    /// # Errors
253    ///
254    /// Returns [`Self::Error`] if the underlying repository fails
255    async fn cleanup_orphaned(
256        &mut self,
257        since: Option<Ulid>,
258        until: Ulid,
259        limit: usize,
260    ) -> Result<(usize, Option<Ulid>), Self::Error>;
261}
262
263repository_impl!(UpstreamOAuthLinkRepository:
264    async fn lookup(&mut self, id: Ulid) -> Result<Option<UpstreamOAuthLink>, Self::Error>;
265
266    async fn find_by_subject(
267        &mut self,
268        upstream_oauth_provider: &UpstreamOAuthProvider,
269        subject: &str,
270    ) -> Result<Option<UpstreamOAuthLink>, Self::Error>;
271
272    async fn add(
273        &mut self,
274        rng: &mut (dyn RngCore + Send),
275        clock: &dyn Clock,
276        upstream_oauth_provider: &UpstreamOAuthProvider,
277        subject: String,
278        human_account_name: Option<String>,
279    ) -> Result<UpstreamOAuthLink, Self::Error>;
280
281    async fn associate_to_user(
282        &mut self,
283        upstream_oauth_link: &UpstreamOAuthLink,
284        user: &User,
285    ) -> Result<(), Self::Error>;
286
287    async fn list(
288        &mut self,
289        filter: UpstreamOAuthLinkFilter<'_>,
290        pagination: Pagination,
291    ) -> Result<Page<UpstreamOAuthLink>, Self::Error>;
292
293    async fn count(&mut self, filter: UpstreamOAuthLinkFilter<'_>) -> Result<usize, Self::Error>;
294
295    async fn remove(&mut self, clock: &dyn Clock, upstream_oauth_link: UpstreamOAuthLink) -> Result<(), Self::Error>;
296
297    async fn cleanup_orphaned(
298        &mut self,
299        since: Option<Ulid>,
300        until: Ulid,
301        limit: usize,
302    ) -> Result<(usize, Option<Ulid>), Self::Error>;
303);