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}