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