Skip to main content

shared/models/
oauth_provider_mapping.rs

1use crate::{
2    models::{InsertQueryBuilder, UpdateQueryBuilder},
3    prelude::*,
4};
5use compact_str::ToCompactString;
6use garde::Validate;
7use serde::{Deserialize, Serialize};
8use sqlx::{Row, postgres::PgRow};
9use std::{
10    collections::BTreeMap,
11    sync::{Arc, LazyLock},
12};
13use utoipa::ToSchema;
14
15#[derive(ToSchema, Validate, Serialize, Deserialize, Clone)]
16#[serde(tag = "type", rename_all = "snake_case")]
17#[schema(no_recursion)]
18pub enum OAuthProviderMappingMatcher {
19    None,
20    And {
21        #[garde(dive)]
22        matchers: Vec<OAuthProviderMappingMatcher>,
23    },
24    Or {
25        #[garde(dive)]
26        matchers: Vec<OAuthProviderMappingMatcher>,
27    },
28    Not {
29        #[garde(dive)]
30        matcher: Box<OAuthProviderMappingMatcher>,
31    },
32    Scopes {
33        #[garde(length(max = 255), inner(length(max = 255)))]
34        #[schema(max_length = 255)]
35        scopes: Vec<compact_str::CompactString>,
36    },
37    FieldExists {
38        #[garde(
39            length(chars, min = 1, max = 255),
40            custom(crate::utils::validate_json_path)
41        )]
42        #[schema(min_length = 1, max_length = 255)]
43        path: String,
44    },
45    FieldEquals {
46        #[garde(
47            length(chars, min = 1, max = 255),
48            custom(crate::utils::validate_json_path)
49        )]
50        #[schema(min_length = 1, max_length = 255)]
51        path: String,
52        #[garde(length(max = 255))]
53        #[schema(max_length = 255)]
54        equals: compact_str::CompactString,
55    },
56    FieldContains {
57        #[garde(
58            length(chars, min = 1, max = 255),
59            custom(crate::utils::validate_json_path)
60        )]
61        #[schema(min_length = 1, max_length = 255)]
62        path: String,
63        #[garde(length(max = 255))]
64        #[schema(max_length = 255)]
65        contains: compact_str::CompactString,
66    },
67    FieldStartsWith {
68        #[garde(
69            length(chars, min = 1, max = 255),
70            custom(crate::utils::validate_json_path)
71        )]
72        #[schema(min_length = 1, max_length = 255)]
73        path: String,
74        #[garde(length(max = 255))]
75        #[schema(max_length = 255)]
76        starts_with: compact_str::CompactString,
77    },
78    FieldEndsWith {
79        #[garde(
80            length(chars, min = 1, max = 255),
81            custom(crate::utils::validate_json_path)
82        )]
83        #[schema(min_length = 1, max_length = 255)]
84        path: String,
85        #[garde(length(max = 255))]
86        #[schema(max_length = 255)]
87        ends_with: compact_str::CompactString,
88    },
89}
90
91impl OAuthProviderMappingMatcher {
92    pub const MAX_NESTING_DEPTH: usize = 3;
93
94    fn nested_within_limit(&self, depth: usize) -> bool {
95        match self {
96            OAuthProviderMappingMatcher::And { matchers }
97            | OAuthProviderMappingMatcher::Or { matchers } => {
98                depth < Self::MAX_NESTING_DEPTH
99                    && matchers.iter().all(|m| m.nested_within_limit(depth + 1))
100            }
101            OAuthProviderMappingMatcher::Not { matcher } => {
102                depth < Self::MAX_NESTING_DEPTH && matcher.nested_within_limit(depth + 1)
103            }
104            _ => true,
105        }
106    }
107
108    pub fn validate_nesting(value: &Self, _context: &()) -> garde::Result {
109        if value.nested_within_limit(0) {
110            Ok(())
111        } else {
112            Err(garde::Error::new(format!(
113                "matcher may not nest groups more than {} levels deep",
114                Self::MAX_NESTING_DEPTH
115            )))
116        }
117    }
118
119    pub fn validate_optional_nesting(value: &Option<Self>, context: &()) -> garde::Result {
120        match value {
121            Some(matcher) => Self::validate_nesting(matcher, context),
122            None => Ok(()),
123        }
124    }
125
126    fn query_nodes<'a>(path: &str, info: &'a serde_json::Value) -> Vec<&'a serde_json::Value> {
127        match serde_json_path::JsonPath::parse(path) {
128            Ok(path) => path.query(info).all(),
129            Err(err) => {
130                tracing::warn!(
131                    path,
132                    "failed to parse oauth provider mapping matcher path: {err}"
133                );
134
135                Vec::new()
136            }
137        }
138    }
139
140    fn node_equals_str(node: &serde_json::Value, expected: &str) -> bool {
141        match node {
142            serde_json::Value::String(value) => value == expected,
143            node => node.to_compact_string() == expected,
144        }
145    }
146
147    fn node_string(node: &serde_json::Value) -> compact_str::CompactString {
148        match node {
149            serde_json::Value::String(value) => value.to_compact_string(),
150            node => node.to_compact_string(),
151        }
152    }
153
154    pub fn matches(
155        &self,
156        granted_scopes: &[compact_str::CompactString],
157        info: &serde_json::Value,
158    ) -> bool {
159        match self {
160            OAuthProviderMappingMatcher::None => true,
161            OAuthProviderMappingMatcher::And { matchers } => {
162                matchers.iter().all(|m| m.matches(granted_scopes, info))
163            }
164            OAuthProviderMappingMatcher::Or { matchers } => {
165                matchers.iter().any(|m| m.matches(granted_scopes, info))
166            }
167            OAuthProviderMappingMatcher::Not { matcher } => !matcher.matches(granted_scopes, info),
168            OAuthProviderMappingMatcher::Scopes { scopes } => {
169                scopes.iter().all(|scope| granted_scopes.contains(scope))
170            }
171            OAuthProviderMappingMatcher::FieldExists { path } => {
172                !Self::query_nodes(path, info).is_empty()
173            }
174            OAuthProviderMappingMatcher::FieldEquals { path, equals } => {
175                Self::query_nodes(path, info)
176                    .into_iter()
177                    .any(|node| Self::node_equals_str(node, equals))
178            }
179            OAuthProviderMappingMatcher::FieldContains { path, contains } => {
180                Self::query_nodes(path, info)
181                    .into_iter()
182                    .any(|node| match node {
183                        serde_json::Value::Array(values) => values
184                            .iter()
185                            .any(|value| Self::node_equals_str(value, contains)),
186                        node => Self::node_string(node).contains(contains.as_str()),
187                    })
188            }
189            OAuthProviderMappingMatcher::FieldStartsWith { path, starts_with } => {
190                Self::query_nodes(path, info)
191                    .into_iter()
192                    .any(|node| Self::node_string(node).starts_with(starts_with.as_str()))
193            }
194            OAuthProviderMappingMatcher::FieldEndsWith { path, ends_with } => {
195                Self::query_nodes(path, info)
196                    .into_iter()
197                    .any(|node| Self::node_string(node).ends_with(ends_with.as_str()))
198            }
199        }
200    }
201}
202
203#[derive(ToSchema, Validate, Serialize, Deserialize, Clone)]
204#[serde(tag = "type", rename_all = "snake_case")]
205pub enum OAuthProviderMappingType {
206    Role {
207        #[garde(skip)]
208        role_uuid: uuid::Uuid,
209        #[garde(skip)]
210        #[serde(default)]
211        revoke_unmatched: bool,
212    },
213    ServerSubuser {
214        #[garde(skip)]
215        server_uuid: uuid::Uuid,
216        #[garde(custom(crate::permissions::validate_server_permissions))]
217        permissions: Vec<compact_str::CompactString>,
218        #[garde(skip)]
219        ignored_files: Vec<compact_str::CompactString>,
220        #[garde(skip)]
221        #[serde(default)]
222        revoke_unmatched: bool,
223    },
224}
225
226impl OAuthProviderMappingType {
227    pub fn revoke_unmatched(&self) -> bool {
228        match self {
229            OAuthProviderMappingType::Role {
230                revoke_unmatched, ..
231            }
232            | OAuthProviderMappingType::ServerSubuser {
233                revoke_unmatched, ..
234            } => *revoke_unmatched,
235        }
236    }
237}
238
239#[derive(Serialize, Deserialize, Clone)]
240pub struct OAuthProviderMapping {
241    pub uuid: uuid::Uuid,
242    pub oauth_provider: Fetchable<super::oauth_provider::OAuthProvider>,
243
244    pub matcher: OAuthProviderMappingMatcher,
245    pub mapping: OAuthProviderMappingType,
246
247    pub created: chrono::NaiveDateTime,
248
249    extension_data: super::ModelExtensionData,
250}
251
252impl BaseModel for OAuthProviderMapping {
253    const NAME: &'static str = "oauth_provider_mapping";
254
255    fn get_extension_list() -> &'static super::ModelExtensionList {
256        static EXTENSIONS: LazyLock<super::ModelExtensionList> =
257            LazyLock::new(|| parking_lot::RwLock::new(Vec::new()));
258
259        &EXTENSIONS
260    }
261
262    fn get_extension_data(&self) -> &super::ModelExtensionData {
263        &self.extension_data
264    }
265
266    #[inline]
267    fn base_columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
268        let prefix = prefix.unwrap_or_default();
269
270        BTreeMap::from([
271            (
272                "oauth_provider_mappings.uuid",
273                compact_str::format_compact!("{prefix}uuid"),
274            ),
275            (
276                "oauth_provider_mappings.oauth_provider_uuid",
277                compact_str::format_compact!("{prefix}oauth_provider_uuid"),
278            ),
279            (
280                "oauth_provider_mappings.matcher",
281                compact_str::format_compact!("{prefix}matcher"),
282            ),
283            (
284                "oauth_provider_mappings.mapping",
285                compact_str::format_compact!("{prefix}mapping"),
286            ),
287            (
288                "oauth_provider_mappings.created",
289                compact_str::format_compact!("{prefix}created"),
290            ),
291        ])
292    }
293
294    #[inline]
295    fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
296        let prefix = prefix.unwrap_or_default();
297
298        Ok(Self {
299            uuid: row.try_get(compact_str::format_compact!("{prefix}uuid").as_str())?,
300            oauth_provider: super::oauth_provider::OAuthProvider::get_fetchable(
301                row.try_get(compact_str::format_compact!("{prefix}oauth_provider_uuid").as_str())?,
302            ),
303            matcher: serde_json::from_value(
304                row.try_get(compact_str::format_compact!("{prefix}matcher").as_str())?,
305            )?,
306            mapping: serde_json::from_value(
307                row.try_get(compact_str::format_compact!("{prefix}mapping").as_str())?,
308            )?,
309            created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
310            extension_data: Self::map_extensions(prefix, row)?,
311        })
312    }
313}
314
315impl OAuthProviderMapping {
316    pub async fn by_oauth_provider_uuid_uuid(
317        database: &crate::database::Database,
318        oauth_provider_uuid: uuid::Uuid,
319        uuid: uuid::Uuid,
320    ) -> Result<Option<Self>, crate::database::DatabaseError> {
321        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
322            r#"
323            SELECT {}
324            FROM oauth_provider_mappings
325            WHERE oauth_provider_mappings.oauth_provider_uuid = $1 AND oauth_provider_mappings.uuid = $2
326            "#,
327            Self::columns_sql(None)
328        )))
329        .bind(oauth_provider_uuid)
330        .bind(uuid)
331        .fetch_optional(database.read())
332        .await?;
333
334        row.try_map(|row| Self::map(None, &row))
335    }
336
337    pub async fn by_oauth_provider_uuid_with_pagination(
338        database: &crate::database::Database,
339        oauth_provider_uuid: uuid::Uuid,
340        page: i64,
341        per_page: i64,
342    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
343        let offset = (page - 1) * per_page;
344
345        let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
346            r#"
347            SELECT {}, COUNT(*) OVER() AS total_count
348            FROM oauth_provider_mappings
349            WHERE oauth_provider_mappings.oauth_provider_uuid = $1
350            ORDER BY oauth_provider_mappings.created
351            LIMIT $2 OFFSET $3
352            "#,
353            Self::columns_sql(None)
354        )))
355        .bind(oauth_provider_uuid)
356        .bind(per_page)
357        .bind(offset)
358        .fetch_all(database.read())
359        .await?;
360
361        Ok(super::Pagination {
362            total: rows
363                .first()
364                .map_or(Ok(0), |row| row.try_get("total_count"))?,
365            per_page,
366            page,
367            data: rows
368                .into_iter()
369                .map(|row| Self::map(None, &row))
370                .try_collect_vec()?,
371        })
372    }
373
374    pub async fn all_by_oauth_provider_uuid(
375        database: &crate::database::Database,
376        oauth_provider_uuid: uuid::Uuid,
377    ) -> Result<Vec<Self>, crate::database::DatabaseError> {
378        let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
379            r#"
380            SELECT {}
381            FROM oauth_provider_mappings
382            WHERE oauth_provider_mappings.oauth_provider_uuid = $1
383            ORDER BY oauth_provider_mappings.created
384            "#,
385            Self::columns_sql(None)
386        )))
387        .bind(oauth_provider_uuid)
388        .fetch_all(database.read())
389        .await?;
390
391        rows.into_iter()
392            .map(|row| Self::map(None, &row))
393            .try_collect_vec()
394    }
395
396    pub async fn apply_for_user(
397        state: &crate::State,
398        oauth_provider_uuid: uuid::Uuid,
399        user_uuid: uuid::Uuid,
400        granted_scopes: &[compact_str::CompactString],
401        info: &serde_json::Value,
402    ) -> Result<(), crate::database::DatabaseError> {
403        let mappings =
404            Self::all_by_oauth_provider_uuid(&state.database, oauth_provider_uuid).await?;
405        let (matched, unmatched): (Vec<Self>, Vec<Self>) = mappings
406            .into_iter()
407            .partition(|mapping| mapping.matcher.matches(granted_scopes, info));
408        if matched.is_empty() && !unmatched.iter().any(|m| m.mapping.revoke_unmatched()) {
409            return Ok(());
410        }
411
412        let mut user = match super::user::User::by_uuid_optional(&state.database, user_uuid).await?
413        {
414            Some(user) => user,
415            None => return Ok(()),
416        };
417
418        let matched_role_uuids = matched
419            .iter()
420            .filter_map(|mapping| match &mapping.mapping {
421                OAuthProviderMappingType::Role { role_uuid, .. } => Some(*role_uuid),
422                _ => None,
423            })
424            .collect::<Vec<_>>();
425        let matched_server_uuids = matched
426            .iter()
427            .filter_map(|mapping| match &mapping.mapping {
428                OAuthProviderMappingType::ServerSubuser { server_uuid, .. } => Some(*server_uuid),
429                _ => None,
430            })
431            .collect::<Vec<_>>();
432
433        for mapping in matched {
434            if let Err(err) = mapping.apply(state, &mut user).await {
435                tracing::warn!(
436                    mapping = %mapping.uuid,
437                    user = %user.uuid,
438                    "failed to apply oauth provider mapping: {:#?}",
439                    err
440                );
441            }
442        }
443
444        for mapping in unmatched {
445            if !mapping.mapping.revoke_unmatched() {
446                continue;
447            }
448
449            match &mapping.mapping {
450                OAuthProviderMappingType::Role { role_uuid, .. }
451                    if matched_role_uuids.contains(role_uuid) =>
452                {
453                    continue;
454                }
455                OAuthProviderMappingType::ServerSubuser { server_uuid, .. }
456                    if matched_server_uuids.contains(server_uuid) =>
457                {
458                    continue;
459                }
460                _ => {}
461            }
462
463            if let Err(err) = mapping.revert(state, &mut user).await {
464                tracing::warn!(
465                    mapping = %mapping.uuid,
466                    user = %user.uuid,
467                    "failed to revert oauth provider mapping: {:#?}",
468                    err
469                );
470            }
471        }
472
473        Ok(())
474    }
475
476    async fn apply(
477        &self,
478        state: &crate::State,
479        user: &mut super::user::User,
480    ) -> Result<(), crate::database::DatabaseError> {
481        match &self.mapping {
482            OAuthProviderMappingType::Role { role_uuid, .. } => {
483                if super::role::Role::by_uuid_optional_cached(&state.database, *role_uuid)
484                    .await?
485                    .is_none()
486                {
487                    return Ok(());
488                }
489
490                user.update(
491                    state,
492                    super::user::UpdateUserOptions {
493                        role_uuid: Some(Some(*role_uuid)),
494                        ..Default::default()
495                    },
496                )
497                .await
498            }
499            OAuthProviderMappingType::ServerSubuser {
500                server_uuid,
501                permissions,
502                ignored_files,
503                ..
504            } => {
505                let server = match super::server::Server::by_uuid_optional_cached(
506                    &state.database,
507                    *server_uuid,
508                )
509                .await?
510                {
511                    Some(server) => server,
512                    None => return Ok(()),
513                };
514
515                if server.owner.uuid == user.uuid {
516                    return Ok(());
517                }
518
519                if let Some(mut subuser) =
520                    super::server_subuser::ServerSubuser::by_server_uuid_user_uuid(
521                        &state.database,
522                        server.uuid,
523                        user.uuid,
524                    )
525                    .await?
526                {
527                    subuser
528                        .update(
529                            state,
530                            super::server_subuser::UpdateServerSubuserOptions {
531                                permissions: Some(permissions.clone()),
532                                ignored_files: Some(ignored_files.clone()),
533                            },
534                        )
535                        .await?;
536                } else {
537                    super::server_subuser::ServerSubuser::create(
538                        state,
539                        super::server_subuser::CreateServerSubuserOptions {
540                            server: &server,
541                            email: user.email.clone(),
542                            permissions: permissions.clone(),
543                            ignored_files: ignored_files.clone(),
544                        },
545                    )
546                    .await?;
547                }
548
549                Ok(())
550            }
551        }
552    }
553
554    async fn revert(
555        &self,
556        state: &crate::State,
557        user: &mut super::user::User,
558    ) -> Result<(), crate::database::DatabaseError> {
559        match &self.mapping {
560            OAuthProviderMappingType::Role { role_uuid, .. } => {
561                if !user
562                    .role
563                    .as_ref()
564                    .is_some_and(|role| role.uuid == *role_uuid)
565                {
566                    return Ok(());
567                }
568
569                user.update(
570                    state,
571                    super::user::UpdateUserOptions {
572                        role_uuid: Some(None),
573                        ..Default::default()
574                    },
575                )
576                .await
577            }
578            OAuthProviderMappingType::ServerSubuser { server_uuid, .. } => {
579                let server = match super::server::Server::by_uuid_optional_cached(
580                    &state.database,
581                    *server_uuid,
582                )
583                .await?
584                {
585                    Some(server) => server,
586                    None => return Ok(()),
587                };
588
589                if server.owner.uuid == user.uuid {
590                    return Ok(());
591                }
592
593                if let Some(subuser) =
594                    super::server_subuser::ServerSubuser::by_server_uuid_user_uuid(
595                        &state.database,
596                        server.uuid,
597                        user.uuid,
598                    )
599                    .await?
600                {
601                    subuser.delete(state, ()).await?;
602                }
603
604                Ok(())
605            }
606        }
607    }
608
609    pub async fn cleanup_uuid_arrays(
610        database: &crate::database::Database,
611    ) -> Result<u64, crate::database::DatabaseError> {
612        let result = sqlx::query(
613            "DELETE FROM oauth_provider_mappings
614            WHERE (
615                mapping->>'type' = 'role'
616                AND NOT EXISTS (SELECT 1 FROM roles WHERE uuid = (mapping->>'role_uuid')::uuid)
617            ) OR (
618                mapping->>'type' = 'server_subuser'
619                AND NOT EXISTS (SELECT 1 FROM servers WHERE uuid = (mapping->>'server_uuid')::uuid)
620            )",
621        )
622        .execute(database.write())
623        .await?;
624
625        Ok(result.rows_affected())
626    }
627}
628
629#[async_trait::async_trait]
630impl IntoAdminApiObject for OAuthProviderMapping {
631    type AdminApiObject = AdminApiOAuthProviderMapping;
632    type ExtraArgs<'a> = ();
633
634    async fn into_admin_api_object<'a>(
635        self,
636        state: &crate::State,
637        _args: Self::ExtraArgs<'a>,
638    ) -> Result<Self::AdminApiObject, crate::database::DatabaseError> {
639        let api_object = AdminApiOAuthProviderMapping::init_hooks(&self, state).await?;
640
641        let api_object = finish_extendible!(
642            AdminApiOAuthProviderMapping {
643                uuid: self.uuid,
644                matcher: self.matcher,
645                mapping: self.mapping,
646                created: self.created.and_utc(),
647            },
648            api_object,
649            state
650        )?;
651
652        Ok(api_object)
653    }
654}
655
656#[derive(ToSchema, Deserialize, Validate)]
657pub struct CreateOAuthProviderMappingOptions {
658    #[garde(skip)]
659    pub oauth_provider_uuid: uuid::Uuid,
660
661    #[garde(dive, custom(OAuthProviderMappingMatcher::validate_nesting))]
662    pub matcher: OAuthProviderMappingMatcher,
663    #[garde(dive)]
664    pub mapping: OAuthProviderMappingType,
665}
666
667#[async_trait::async_trait]
668impl CreatableModel for OAuthProviderMapping {
669    type CreateOptions<'a> = CreateOAuthProviderMappingOptions;
670    type CreateResult = Self;
671
672    fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
673        static CREATE_LISTENERS: LazyLock<CreateListenerList<OAuthProviderMapping>> =
674            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
675
676        &CREATE_LISTENERS
677    }
678
679    async fn create_with_transaction(
680        state: &crate::State,
681        mut options: Self::CreateOptions<'_>,
682        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
683    ) -> Result<Self, crate::database::DatabaseError> {
684        options.validate()?;
685
686        super::oauth_provider::OAuthProvider::by_uuid_optional_cached(
687            &state.database,
688            options.oauth_provider_uuid,
689        )
690        .await?
691        .ok_or(crate::database::InvalidRelationError("oauth_provider"))?;
692
693        let mut query_builder = InsertQueryBuilder::new("oauth_provider_mappings");
694
695        Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
696
697        query_builder
698            .set("oauth_provider_uuid", options.oauth_provider_uuid)
699            .set("matcher", serde_json::to_value(&options.matcher)?)
700            .set("mapping", serde_json::to_value(&options.mapping)?);
701
702        let row = query_builder
703            .returning(&Self::columns_sql(None))
704            .fetch_one(&mut **transaction)
705            .await?;
706        let mut mapping = Self::map(None, &row)?;
707
708        Self::run_after_create_handlers(&mut mapping, &options, state, transaction).await?;
709
710        Ok(mapping)
711    }
712}
713
714#[derive(ToSchema, Serialize, Deserialize, Validate, Default)]
715pub struct UpdateOAuthProviderMappingOptions {
716    #[garde(dive, custom(OAuthProviderMappingMatcher::validate_optional_nesting))]
717    pub matcher: Option<OAuthProviderMappingMatcher>,
718    #[garde(dive)]
719    pub mapping: Option<OAuthProviderMappingType>,
720}
721
722#[async_trait::async_trait]
723impl UpdatableModel for OAuthProviderMapping {
724    type UpdateOptions = UpdateOAuthProviderMappingOptions;
725
726    fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
727        static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<OAuthProviderMapping>> =
728            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
729
730        &UPDATE_LISTENERS
731    }
732
733    async fn update_with_transaction(
734        &mut self,
735        state: &crate::State,
736        mut options: Self::UpdateOptions,
737        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
738    ) -> Result<(), crate::database::DatabaseError> {
739        options.validate()?;
740
741        let mut query_builder = UpdateQueryBuilder::new("oauth_provider_mappings");
742
743        self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
744            .await?;
745
746        let matcher = options
747            .matcher
748            .as_ref()
749            .map(serde_json::to_value)
750            .transpose()?;
751        let mapping = options
752            .mapping
753            .as_ref()
754            .map(serde_json::to_value)
755            .transpose()?;
756
757        query_builder
758            .set("matcher", matcher)
759            .set("mapping", mapping)
760            .where_eq("uuid", self.uuid);
761
762        query_builder.execute(&mut **transaction).await?;
763
764        if let Some(matcher) = options.matcher {
765            self.matcher = matcher;
766        }
767        if let Some(mapping) = options.mapping {
768            self.mapping = mapping;
769        }
770
771        self.run_after_update_handlers(state, transaction).await?;
772
773        Ok(())
774    }
775}
776
777#[async_trait::async_trait]
778impl DeletableModel for OAuthProviderMapping {
779    type DeleteOptions = ();
780
781    fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
782        static DELETE_LISTENERS: LazyLock<DeleteHandlerList<OAuthProviderMapping>> =
783            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
784
785        &DELETE_LISTENERS
786    }
787
788    async fn delete_with_transaction(
789        &self,
790        state: &crate::State,
791        options: Self::DeleteOptions,
792        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
793    ) -> Result<(), anyhow::Error> {
794        self.run_delete_handlers(&options, state, transaction)
795            .await?;
796
797        sqlx::query(
798            r#"
799            DELETE FROM oauth_provider_mappings
800            WHERE oauth_provider_mappings.uuid = $1
801            "#,
802        )
803        .bind(self.uuid)
804        .execute(&mut **transaction)
805        .await?;
806
807        self.run_after_delete_handlers(&options, state, transaction)
808            .await?;
809
810        Ok(())
811    }
812}
813
814#[schema_extension_derive::extendible]
815#[init_args(OAuthProviderMapping, crate::State)]
816#[hook_args(crate::State)]
817#[derive(ToSchema, Serialize)]
818#[schema(title = "AdminOAuthProviderMapping")]
819pub struct AdminApiOAuthProviderMapping {
820    pub uuid: uuid::Uuid,
821
822    pub matcher: OAuthProviderMappingMatcher,
823    pub mapping: OAuthProviderMappingType,
824
825    pub created: chrono::DateTime<chrono::Utc>,
826}