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);