1use crate::{
2 models::{InsertQueryBuilder, UpdateQueryBuilder},
3 prelude::*,
4};
5use garde::Validate;
6use serde::{Deserialize, Serialize};
7use sqlx::{Row, postgres::PgRow};
8use std::{
9 collections::BTreeMap,
10 sync::{Arc, LazyLock},
11};
12use utoipa::ToSchema;
13
14#[derive(ToSchema, Validate, Serialize, Deserialize, Clone)]
15#[serde(tag = "type", rename_all = "snake_case")]
16pub enum OAuthProviderMappingType {
17 Role {
18 #[garde(skip)]
19 role_uuid: uuid::Uuid,
20 },
21 ServerSubuser {
22 #[garde(skip)]
23 server_uuid: uuid::Uuid,
24 #[garde(custom(crate::permissions::validate_server_permissions))]
25 permissions: Vec<compact_str::CompactString>,
26 #[garde(skip)]
27 ignored_files: Vec<compact_str::CompactString>,
28 },
29}
30
31#[derive(Serialize, Deserialize, Clone)]
32pub struct OAuthProviderMapping {
33 pub uuid: uuid::Uuid,
34 pub oauth_provider: Fetchable<super::oauth_provider::OAuthProvider>,
35
36 pub scopes: Vec<compact_str::CompactString>,
37 pub mapping: OAuthProviderMappingType,
38
39 pub created: chrono::NaiveDateTime,
40
41 extension_data: super::ModelExtensionData,
42}
43
44impl BaseModel for OAuthProviderMapping {
45 const NAME: &'static str = "oauth_provider_mapping";
46
47 fn get_extension_list() -> &'static super::ModelExtensionList {
48 static EXTENSIONS: LazyLock<super::ModelExtensionList> =
49 LazyLock::new(|| parking_lot::RwLock::new(Vec::new()));
50
51 &EXTENSIONS
52 }
53
54 fn get_extension_data(&self) -> &super::ModelExtensionData {
55 &self.extension_data
56 }
57
58 #[inline]
59 fn base_columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
60 let prefix = prefix.unwrap_or_default();
61
62 BTreeMap::from([
63 (
64 "oauth_provider_mappings.uuid",
65 compact_str::format_compact!("{prefix}uuid"),
66 ),
67 (
68 "oauth_provider_mappings.oauth_provider_uuid",
69 compact_str::format_compact!("{prefix}oauth_provider_uuid"),
70 ),
71 (
72 "oauth_provider_mappings.scopes",
73 compact_str::format_compact!("{prefix}scopes"),
74 ),
75 (
76 "oauth_provider_mappings.mapping",
77 compact_str::format_compact!("{prefix}mapping"),
78 ),
79 (
80 "oauth_provider_mappings.created",
81 compact_str::format_compact!("{prefix}created"),
82 ),
83 ])
84 }
85
86 #[inline]
87 fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
88 let prefix = prefix.unwrap_or_default();
89
90 Ok(Self {
91 uuid: row.try_get(compact_str::format_compact!("{prefix}uuid").as_str())?,
92 oauth_provider: super::oauth_provider::OAuthProvider::get_fetchable(
93 row.try_get(compact_str::format_compact!("{prefix}oauth_provider_uuid").as_str())?,
94 ),
95 scopes: row.try_get(compact_str::format_compact!("{prefix}scopes").as_str())?,
96 mapping: serde_json::from_value(
97 row.try_get(compact_str::format_compact!("{prefix}mapping").as_str())?,
98 )?,
99 created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
100 extension_data: Self::map_extensions(prefix, row)?,
101 })
102 }
103}
104
105impl OAuthProviderMapping {
106 pub async fn by_oauth_provider_uuid_uuid(
107 database: &crate::database::Database,
108 oauth_provider_uuid: uuid::Uuid,
109 uuid: uuid::Uuid,
110 ) -> Result<Option<Self>, crate::database::DatabaseError> {
111 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
112 r#"
113 SELECT {}
114 FROM oauth_provider_mappings
115 WHERE oauth_provider_mappings.oauth_provider_uuid = $1 AND oauth_provider_mappings.uuid = $2
116 "#,
117 Self::columns_sql(None)
118 )))
119 .bind(oauth_provider_uuid)
120 .bind(uuid)
121 .fetch_optional(database.read())
122 .await?;
123
124 row.try_map(|row| Self::map(None, &row))
125 }
126
127 pub async fn by_oauth_provider_uuid_with_pagination(
128 database: &crate::database::Database,
129 oauth_provider_uuid: uuid::Uuid,
130 page: i64,
131 per_page: i64,
132 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
133 let offset = (page - 1) * per_page;
134
135 let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
136 r#"
137 SELECT {}, COUNT(*) OVER() AS total_count
138 FROM oauth_provider_mappings
139 WHERE oauth_provider_mappings.oauth_provider_uuid = $1
140 ORDER BY oauth_provider_mappings.created
141 LIMIT $2 OFFSET $3
142 "#,
143 Self::columns_sql(None)
144 )))
145 .bind(oauth_provider_uuid)
146 .bind(per_page)
147 .bind(offset)
148 .fetch_all(database.read())
149 .await?;
150
151 Ok(super::Pagination {
152 total: rows
153 .first()
154 .map_or(Ok(0), |row| row.try_get("total_count"))?,
155 per_page,
156 page,
157 data: rows
158 .into_iter()
159 .map(|row| Self::map(None, &row))
160 .try_collect_vec()?,
161 })
162 }
163
164 pub async fn by_applicable_for_scopes(
165 database: &crate::database::Database,
166 oauth_provider_uuid: uuid::Uuid,
167 granted_scopes: &[compact_str::CompactString],
168 ) -> Result<Vec<Self>, crate::database::DatabaseError> {
169 let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
170 r#"
171 SELECT {}
172 FROM oauth_provider_mappings
173 WHERE oauth_provider_mappings.oauth_provider_uuid = $1
174 AND oauth_provider_mappings.scopes::text[] <@ $2::text[]
175 ORDER BY oauth_provider_mappings.created
176 "#,
177 Self::columns_sql(None)
178 )))
179 .bind(oauth_provider_uuid)
180 .bind(granted_scopes)
181 .fetch_all(database.read())
182 .await?;
183
184 rows.into_iter()
185 .map(|row| Self::map(None, &row))
186 .try_collect_vec()
187 }
188
189 pub async fn apply_for_user(
190 state: &crate::State,
191 oauth_provider_uuid: uuid::Uuid,
192 user_uuid: uuid::Uuid,
193 granted_scopes: &[compact_str::CompactString],
194 ) -> Result<(), crate::database::DatabaseError> {
195 let mappings =
196 Self::by_applicable_for_scopes(&state.database, oauth_provider_uuid, granted_scopes)
197 .await?;
198 if mappings.is_empty() {
199 return Ok(());
200 }
201
202 let mut user =
203 match super::user::User::by_uuid_optional_cached(&state.database, user_uuid).await? {
204 Some(user) => user,
205 None => return Ok(()),
206 };
207
208 for mapping in mappings {
209 if let Err(err) = mapping.apply(state, &mut user).await {
210 tracing::warn!(
211 mapping = %mapping.uuid,
212 user = %user.uuid,
213 "failed to apply oauth provider mapping: {:#?}",
214 err
215 );
216 }
217 }
218
219 Ok(())
220 }
221
222 async fn apply(
223 &self,
224 state: &crate::State,
225 user: &mut super::user::User,
226 ) -> Result<(), crate::database::DatabaseError> {
227 match &self.mapping {
228 OAuthProviderMappingType::Role { role_uuid } => {
229 if super::role::Role::by_uuid_optional_cached(&state.database, *role_uuid)
230 .await?
231 .is_none()
232 {
233 return Ok(());
234 }
235
236 user.update(
237 state,
238 super::user::UpdateUserOptions {
239 role_uuid: Some(Some(*role_uuid)),
240 ..Default::default()
241 },
242 )
243 .await
244 }
245 OAuthProviderMappingType::ServerSubuser {
246 server_uuid,
247 permissions,
248 ignored_files,
249 } => {
250 let server = match super::server::Server::by_uuid_optional_cached(
251 &state.database,
252 *server_uuid,
253 )
254 .await?
255 {
256 Some(server) => server,
257 None => return Ok(()),
258 };
259
260 if server.owner.uuid == user.uuid {
261 return Ok(());
262 }
263
264 if let Some(mut subuser) =
265 super::server_subuser::ServerSubuser::by_server_uuid_user_uuid(
266 &state.database,
267 server.uuid,
268 user.uuid,
269 )
270 .await?
271 {
272 subuser
273 .update(
274 state,
275 super::server_subuser::UpdateServerSubuserOptions {
276 permissions: Some(permissions.clone()),
277 ignored_files: Some(ignored_files.clone()),
278 },
279 )
280 .await?;
281 } else {
282 super::server_subuser::ServerSubuser::create(
283 state,
284 super::server_subuser::CreateServerSubuserOptions {
285 server: &server,
286 email: user.email.clone(),
287 permissions: permissions.clone(),
288 ignored_files: ignored_files.clone(),
289 },
290 )
291 .await?;
292 }
293
294 Ok(())
295 }
296 }
297 }
298
299 pub async fn cleanup_uuid_arrays(
300 database: &crate::database::Database,
301 ) -> Result<u64, crate::database::DatabaseError> {
302 let result = sqlx::query(
303 "DELETE FROM oauth_provider_mappings
304 WHERE (
305 mapping->>'type' = 'role'
306 AND NOT EXISTS (SELECT 1 FROM roles WHERE uuid = (mapping->>'role_uuid')::uuid)
307 ) OR (
308 mapping->>'type' = 'server_subuser'
309 AND NOT EXISTS (SELECT 1 FROM servers WHERE uuid = (mapping->>'server_uuid')::uuid)
310 )",
311 )
312 .execute(database.write())
313 .await?;
314
315 Ok(result.rows_affected())
316 }
317}
318
319#[async_trait::async_trait]
320impl IntoAdminApiObject for OAuthProviderMapping {
321 type AdminApiObject = AdminApiOAuthProviderMapping;
322 type ExtraArgs<'a> = ();
323
324 async fn into_admin_api_object<'a>(
325 self,
326 state: &crate::State,
327 _args: Self::ExtraArgs<'a>,
328 ) -> Result<Self::AdminApiObject, crate::database::DatabaseError> {
329 let api_object = AdminApiOAuthProviderMapping::init_hooks(&self, state).await?;
330
331 let api_object = finish_extendible!(
332 AdminApiOAuthProviderMapping {
333 uuid: self.uuid,
334 scopes: self.scopes,
335 mapping: self.mapping,
336 created: self.created.and_utc(),
337 },
338 api_object,
339 state
340 )?;
341
342 Ok(api_object)
343 }
344}
345
346#[derive(ToSchema, Deserialize, Validate)]
347pub struct CreateOAuthProviderMappingOptions {
348 #[garde(skip)]
349 pub oauth_provider_uuid: uuid::Uuid,
350
351 #[garde(length(max = 255))]
352 #[schema(max_length = 255)]
353 pub scopes: Vec<compact_str::CompactString>,
354 #[garde(dive)]
355 pub mapping: OAuthProviderMappingType,
356}
357
358#[async_trait::async_trait]
359impl CreatableModel for OAuthProviderMapping {
360 type CreateOptions<'a> = CreateOAuthProviderMappingOptions;
361 type CreateResult = Self;
362
363 fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
364 static CREATE_LISTENERS: LazyLock<CreateListenerList<OAuthProviderMapping>> =
365 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
366
367 &CREATE_LISTENERS
368 }
369
370 async fn create_with_transaction(
371 state: &crate::State,
372 mut options: Self::CreateOptions<'_>,
373 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
374 ) -> Result<Self, crate::database::DatabaseError> {
375 options.validate()?;
376
377 super::oauth_provider::OAuthProvider::by_uuid_optional_cached(
378 &state.database,
379 options.oauth_provider_uuid,
380 )
381 .await?
382 .ok_or(crate::database::InvalidRelationError("oauth_provider"))?;
383
384 let mut query_builder = InsertQueryBuilder::new("oauth_provider_mappings");
385
386 Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
387
388 query_builder
389 .set("oauth_provider_uuid", options.oauth_provider_uuid)
390 .set("scopes", &options.scopes)
391 .set("mapping", serde_json::to_value(&options.mapping)?);
392
393 let row = query_builder
394 .returning(&Self::columns_sql(None))
395 .fetch_one(&mut **transaction)
396 .await?;
397 let mut mapping = Self::map(None, &row)?;
398
399 Self::run_after_create_handlers(&mut mapping, &options, state, transaction).await?;
400
401 Ok(mapping)
402 }
403}
404
405#[derive(ToSchema, Serialize, Deserialize, Validate, Default)]
406pub struct UpdateOAuthProviderMappingOptions {
407 #[garde(length(max = 255))]
408 #[schema(max_length = 255)]
409 pub scopes: Option<Vec<compact_str::CompactString>>,
410 #[garde(dive)]
411 pub mapping: Option<OAuthProviderMappingType>,
412}
413
414#[async_trait::async_trait]
415impl UpdatableModel for OAuthProviderMapping {
416 type UpdateOptions = UpdateOAuthProviderMappingOptions;
417
418 fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
419 static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<OAuthProviderMapping>> =
420 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
421
422 &UPDATE_LISTENERS
423 }
424
425 async fn update_with_transaction(
426 &mut self,
427 state: &crate::State,
428 mut options: Self::UpdateOptions,
429 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
430 ) -> Result<(), crate::database::DatabaseError> {
431 options.validate()?;
432
433 let mut query_builder = UpdateQueryBuilder::new("oauth_provider_mappings");
434
435 self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
436 .await?;
437
438 let mapping = options
439 .mapping
440 .as_ref()
441 .map(serde_json::to_value)
442 .transpose()?;
443
444 query_builder
445 .set("scopes", options.scopes.as_ref())
446 .set("mapping", mapping)
447 .where_eq("uuid", self.uuid);
448
449 query_builder.execute(&mut **transaction).await?;
450
451 if let Some(scopes) = options.scopes {
452 self.scopes = scopes;
453 }
454 if let Some(mapping) = options.mapping {
455 self.mapping = mapping;
456 }
457
458 self.run_after_update_handlers(state, transaction).await?;
459
460 Ok(())
461 }
462}
463
464#[async_trait::async_trait]
465impl DeletableModel for OAuthProviderMapping {
466 type DeleteOptions = ();
467
468 fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
469 static DELETE_LISTENERS: LazyLock<DeleteHandlerList<OAuthProviderMapping>> =
470 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
471
472 &DELETE_LISTENERS
473 }
474
475 async fn delete_with_transaction(
476 &self,
477 state: &crate::State,
478 options: Self::DeleteOptions,
479 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
480 ) -> Result<(), anyhow::Error> {
481 self.run_delete_handlers(&options, state, transaction)
482 .await?;
483
484 sqlx::query(
485 r#"
486 DELETE FROM oauth_provider_mappings
487 WHERE oauth_provider_mappings.uuid = $1
488 "#,
489 )
490 .bind(self.uuid)
491 .execute(&mut **transaction)
492 .await?;
493
494 self.run_after_delete_handlers(&options, state, transaction)
495 .await?;
496
497 Ok(())
498 }
499}
500
501#[schema_extension_derive::extendible]
502#[init_args(OAuthProviderMapping, crate::State)]
503#[hook_args(crate::State)]
504#[derive(ToSchema, Serialize)]
505#[schema(title = "AdminOAuthProviderMapping")]
506pub struct AdminApiOAuthProviderMapping {
507 pub uuid: uuid::Uuid,
508
509 pub scopes: Vec<compact_str::CompactString>,
510 pub mapping: OAuthProviderMappingType,
511
512 pub created: chrono::DateTime<chrono::Utc>,
513}