Skip to main content

shared/models/
oauth_provider_mapping.rs

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}