1use crate::{
2 crypt::EncryptedString,
3 models::{InsertQueryBuilder, UpdateQueryBuilder},
4 prelude::*,
5};
6use futures_util::TryStreamExt;
7use garde::Validate;
8use indexmap::IndexMap;
9use rand::distr::SampleString;
10use serde::{Deserialize, Serialize};
11use sqlx::{Row, postgres::PgRow};
12use std::{
13 collections::BTreeMap,
14 sync::{Arc, LazyLock},
15};
16use utoipa::ToSchema;
17
18#[derive(Serialize, Deserialize, Clone)]
19pub struct OAuthProvider {
20 pub uuid: uuid::Uuid,
21
22 pub name: compact_str::CompactString,
23 pub description: Option<compact_str::CompactString>,
24
25 pub client_id: compact_str::CompactString,
26 pub client_secret: Option<EncryptedString>,
27 pub auth_url: String,
28 pub token_url: String,
29 pub info_url: String,
30 pub info_urls: IndexMap<compact_str::CompactString, String>,
31 pub scopes: Vec<compact_str::CompactString>,
32
33 pub identifier_path: String,
34 pub email_path: Option<String>,
35 pub username_path: Option<String>,
36 pub name_first_path: Option<String>,
37 pub name_last_path: Option<String>,
38 pub avatar_url_template: Option<String>,
39 pub avatar_overwrite: bool,
40
41 pub enabled: bool,
42 pub login_only: bool,
43 pub login_bypass_two_factor: bool,
44 pub link_viewable: bool,
45 pub user_manageable: bool,
46 pub basic_auth: bool,
47 pub pkce: bool,
48
49 pub created: chrono::NaiveDateTime,
50
51 extension_data: super::ModelExtensionData,
52}
53
54impl BaseModel for OAuthProvider {
55 const NAME: &'static str = "oauth_provider";
56
57 fn get_extension_list() -> &'static super::ModelExtensionList {
58 static EXTENSIONS: LazyLock<super::ModelExtensionList> =
59 LazyLock::new(|| parking_lot::RwLock::new(Vec::new()));
60
61 &EXTENSIONS
62 }
63
64 fn get_extension_data(&self) -> &super::ModelExtensionData {
65 &self.extension_data
66 }
67
68 #[inline]
69 fn base_columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
70 let prefix = prefix.unwrap_or_default();
71
72 BTreeMap::from([
73 (
74 "oauth_providers.uuid",
75 compact_str::format_compact!("{prefix}uuid"),
76 ),
77 (
78 "oauth_providers.name",
79 compact_str::format_compact!("{prefix}name"),
80 ),
81 (
82 "oauth_providers.description",
83 compact_str::format_compact!("{prefix}description"),
84 ),
85 (
86 "oauth_providers.client_id",
87 compact_str::format_compact!("{prefix}client_id"),
88 ),
89 (
90 "oauth_providers.client_secret",
91 compact_str::format_compact!("{prefix}client_secret"),
92 ),
93 (
94 "oauth_providers.auth_url",
95 compact_str::format_compact!("{prefix}auth_url"),
96 ),
97 (
98 "oauth_providers.token_url",
99 compact_str::format_compact!("{prefix}token_url"),
100 ),
101 (
102 "oauth_providers.info_url",
103 compact_str::format_compact!("{prefix}info_url"),
104 ),
105 (
106 "oauth_providers.info_urls",
107 compact_str::format_compact!("{prefix}info_urls"),
108 ),
109 (
110 "oauth_providers.scopes",
111 compact_str::format_compact!("{prefix}scopes"),
112 ),
113 (
114 "oauth_providers.identifier_path",
115 compact_str::format_compact!("{prefix}identifier_path"),
116 ),
117 (
118 "oauth_providers.email_path",
119 compact_str::format_compact!("{prefix}email_path"),
120 ),
121 (
122 "oauth_providers.username_path",
123 compact_str::format_compact!("{prefix}username_path"),
124 ),
125 (
126 "oauth_providers.name_first_path",
127 compact_str::format_compact!("{prefix}name_first_path"),
128 ),
129 (
130 "oauth_providers.name_last_path",
131 compact_str::format_compact!("{prefix}name_last_path"),
132 ),
133 (
134 "oauth_providers.avatar_url_template",
135 compact_str::format_compact!("{prefix}avatar_url_template"),
136 ),
137 (
138 "oauth_providers.avatar_overwrite",
139 compact_str::format_compact!("{prefix}avatar_overwrite"),
140 ),
141 (
142 "oauth_providers.enabled",
143 compact_str::format_compact!("{prefix}enabled"),
144 ),
145 (
146 "oauth_providers.login_only",
147 compact_str::format_compact!("{prefix}login_only"),
148 ),
149 (
150 "oauth_providers.login_bypass_two_factor",
151 compact_str::format_compact!("{prefix}login_bypass_two_factor"),
152 ),
153 (
154 "oauth_providers.link_viewable",
155 compact_str::format_compact!("{prefix}link_viewable"),
156 ),
157 (
158 "oauth_providers.user_manageable",
159 compact_str::format_compact!("{prefix}user_manageable"),
160 ),
161 (
162 "oauth_providers.basic_auth",
163 compact_str::format_compact!("{prefix}basic_auth"),
164 ),
165 (
166 "oauth_providers.pkce",
167 compact_str::format_compact!("{prefix}pkce"),
168 ),
169 (
170 "oauth_providers.created",
171 compact_str::format_compact!("{prefix}created"),
172 ),
173 ])
174 }
175
176 #[inline]
177 fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
178 let prefix = prefix.unwrap_or_default();
179
180 Ok(Self {
181 uuid: row.try_get(compact_str::format_compact!("{prefix}uuid").as_str())?,
182 name: row.try_get(compact_str::format_compact!("{prefix}name").as_str())?,
183 description: row
184 .try_get(compact_str::format_compact!("{prefix}description").as_str())?,
185 client_id: row.try_get(compact_str::format_compact!("{prefix}client_id").as_str())?,
186 client_secret: row
187 .try_get(compact_str::format_compact!("{prefix}client_secret").as_str())?,
188 auth_url: row.try_get(compact_str::format_compact!("{prefix}auth_url").as_str())?,
189 token_url: row.try_get(compact_str::format_compact!("{prefix}token_url").as_str())?,
190 info_url: row.try_get(compact_str::format_compact!("{prefix}info_url").as_str())?,
191 info_urls: serde_json::from_value(row.try_get::<serde_json::Value, _>(
192 compact_str::format_compact!("{prefix}info_urls").as_str(),
193 )?)?,
194 scopes: row.try_get(compact_str::format_compact!("{prefix}scopes").as_str())?,
195 identifier_path: row
196 .try_get(compact_str::format_compact!("{prefix}identifier_path").as_str())?,
197 email_path: row.try_get(compact_str::format_compact!("{prefix}email_path").as_str())?,
198 username_path: row
199 .try_get(compact_str::format_compact!("{prefix}username_path").as_str())?,
200 name_first_path: row
201 .try_get(compact_str::format_compact!("{prefix}name_first_path").as_str())?,
202 name_last_path: row
203 .try_get(compact_str::format_compact!("{prefix}name_last_path").as_str())?,
204 avatar_url_template: row
205 .try_get(compact_str::format_compact!("{prefix}avatar_url_template").as_str())?,
206 avatar_overwrite: row
207 .try_get(compact_str::format_compact!("{prefix}avatar_overwrite").as_str())?,
208 enabled: row.try_get(compact_str::format_compact!("{prefix}enabled").as_str())?,
209 login_only: row.try_get(compact_str::format_compact!("{prefix}login_only").as_str())?,
210 login_bypass_two_factor: row.try_get(
211 compact_str::format_compact!("{prefix}login_bypass_two_factor").as_str(),
212 )?,
213 link_viewable: row
214 .try_get(compact_str::format_compact!("{prefix}link_viewable").as_str())?,
215 user_manageable: row
216 .try_get(compact_str::format_compact!("{prefix}user_manageable").as_str())?,
217 basic_auth: row.try_get(compact_str::format_compact!("{prefix}basic_auth").as_str())?,
218 pkce: row.try_get(compact_str::format_compact!("{prefix}pkce").as_str())?,
219 created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
220 extension_data: Self::map_extensions(prefix, row)?,
221 })
222 }
223
224 fn cache_invalidation_keys(&self) -> Vec<compact_str::CompactString> {
225 vec![
226 compact_str::format_compact!("{}::{}", Self::NAME, self.uuid),
227 compact_str::CompactString::const_new("oauth_providers::usable"),
228 ]
229 }
230}
231
232pub struct OAuthProviderInfo {
233 pub info: serde_json::Value,
234 pub sources: IndexMap<compact_str::CompactString, serde_json::Value>,
235}
236
237impl OAuthProviderInfo {
238 pub fn source(&self, source: Option<&str>) -> Option<&serde_json::Value> {
239 match source {
240 Some(source) => self.sources.get(source),
241 None => Some(&self.info),
242 }
243 }
244}
245
246impl OAuthProvider {
247 const MAX_AVATAR_URL_LENGTH: usize = 2048;
248 const MAX_DISCOVERY_SIZE: usize = 64 * 1024;
249 const MAX_INFO_SIZE: usize = 1024 * 1024;
250 const INFO_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
251 const MAX_LOGGED_BODY_SIZE: usize = 4096;
252
253 pub async fn fetch_info(
259 &self,
260 client: &reqwest::Client,
261 access_token: &str,
262 ) -> Result<OAuthProviderInfo, anyhow::Error> {
263 let info = async {
264 match Self::fetch_info_url(client, &self.info_url, access_token).await {
265 Ok(Some(info)) => Ok(info),
266 Ok(None) => Err(anyhow::anyhow!("info url responded with 404")),
267 Err(err) => Err(err),
268 }
269 .map_err(|err| {
270 tracing::warn!(
271 oauth_provider = %self.uuid,
272 "failed to fetch oauth info url: {:#?}",
273 err
274 );
275
276 anyhow::Error::from(crate::response::DisplayError::new(
277 "failed to fetch oauth user info",
278 ))
279 })
280 };
281
282 let sources = futures_util::future::try_join_all(self.info_urls.iter().map(
283 |(key, url)| async move {
284 match Self::fetch_info_url(client, url, access_token).await {
285 Ok(value) => Ok(value.map(|value| (key.clone(), value))),
286 Err(err) => {
287 tracing::warn!(
288 oauth_provider = %self.uuid,
289 key = %key,
290 "failed to fetch additional oauth info url: {:#?}",
291 err
292 );
293
294 Err(anyhow::Error::from(crate::response::DisplayError::new(
295 format!("failed to fetch additional oauth user info ({key})"),
296 )))
297 }
298 }
299 },
300 ));
301
302 let (info, sources) = tokio::try_join!(info, sources)?;
303
304 Ok(OAuthProviderInfo {
305 info,
306 sources: sources.into_iter().flatten().collect(),
307 })
308 }
309
310 async fn fetch_info_url(
311 client: &reqwest::Client,
312 url: &str,
313 access_token: &str,
314 ) -> Result<Option<serde_json::Value>, anyhow::Error> {
315 let response = client
316 .get(url)
317 .bearer_auth(access_token)
318 .timeout(Self::INFO_TIMEOUT)
319 .send()
320 .await?;
321
322 let status = response.status();
323 if status == reqwest::StatusCode::NOT_FOUND {
324 return Ok(None);
325 }
326
327 let mut content = Vec::new();
328 let mut stream = response.bytes_stream();
329
330 while let Some(chunk) = stream.try_next().await? {
331 if content.len() + chunk.len() > Self::MAX_INFO_SIZE {
332 return Err(anyhow::anyhow!("response is too large"));
333 }
334
335 content.extend_from_slice(&chunk);
336 }
337
338 let body = || {
339 crate::utils::slice_up_to(
340 &String::from_utf8_lossy(&content),
341 Self::MAX_LOGGED_BODY_SIZE,
342 )
343 .to_string()
344 };
345
346 if !status.is_success() {
347 return Err(anyhow::anyhow!(
348 "responded with status {status}: {}",
349 body()
350 ));
351 }
352
353 match serde_json::from_slice(&content) {
354 Ok(value) => Ok(Some(value)),
355 Err(err) => Err(anyhow::anyhow!(
356 "responded with invalid json ({err}): {}",
357 body()
358 )),
359 }
360 }
361
362 pub async fn all_with_pagination(
363 database: &crate::database::Database,
364 page: i64,
365 per_page: i64,
366 search: Option<&str>,
367 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
368 super::PaginatedQueryBuilder::new(
369 Self::columns_sql(None),
370 format!(
371 r#"
372 FROM oauth_providers
373 WHERE {search}
374 "#,
375 search = super::search_sql(1, &["oauth_providers.name"], &["oauth_providers.uuid"]),
376 ),
377 "oauth_providers.created, oauth_providers.uuid",
378 )
379 .bind(search)
380 .fetch(database, page, per_page)
381 .await
382 }
383
384 pub async fn exists_usable_except(
385 database: &crate::database::Database,
386 except: Option<uuid::Uuid>,
387 ) -> Result<bool, sqlx::Error> {
388 sqlx::query_scalar!(
389 r#"
390 SELECT EXISTS (
391 SELECT 1
392 FROM oauth_providers
393 WHERE oauth_providers.enabled = true
394 AND ($1::uuid IS NULL OR oauth_providers.uuid != $1)
395 ) AS "exists!"
396 "#,
397 except
398 )
399 .fetch_one(database.read())
400 .await
401 }
402
403 pub async fn all_by_usable(
404 database: &crate::database::Database,
405 ) -> Result<Vec<Self>, crate::database::DatabaseError> {
406 let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
407 r#"
408 SELECT {}
409 FROM oauth_providers
410 WHERE oauth_providers.enabled = true
411 ORDER BY oauth_providers.created, oauth_providers.uuid
412 "#,
413 Self::columns_sql(None)
414 )))
415 .fetch_all(database.read())
416 .await?;
417
418 rows.into_iter()
419 .map(|row| Self::map(None, &row))
420 .try_collect_vec()
421 }
422
423 fn extract_required<'a>(
426 &self,
427 field: &str,
428 path: &str,
429 value: &'a serde_json::Value,
430 ) -> Result<&'a serde_json::Value, anyhow::Error> {
431 match serde_json_path::JsonPath::parse(path)?.query(value).first() {
432 Some(node) => Ok(node),
433 None => {
434 tracing::warn!(
435 oauth_provider = %self.uuid,
436 field,
437 path,
438 "unable to extract {field} from oauth info url response: {value}"
439 );
440
441 Err(crate::response::DisplayError::new(format!(
442 "unable to extract {field} from oauth user info"
443 ))
444 .into())
445 }
446 }
447 }
448
449 pub fn extract_identifier(&self, value: &serde_json::Value) -> Result<String, anyhow::Error> {
450 Ok(
451 match self.extract_required("identifier", &self.identifier_path, value)? {
452 serde_json::Value::String(string) => {
453 crate::utils::truncate_up_to(string.clone(), 255)
454 }
455 val => crate::utils::truncate_up_to(val.to_string(), 255),
456 },
457 )
458 }
459
460 pub fn extract_email(&self, value: &serde_json::Value) -> Result<String, anyhow::Error> {
461 let Some(path) = &self.email_path else {
462 return Ok(format!(
463 "{}@oauth.c7s.rs",
464 rand::distr::Alphanumeric.sample_string(&mut rand::rng(), 10)
465 ));
466 };
467
468 Ok(match self.extract_required("email", path, value)? {
469 serde_json::Value::String(string) => crate::utils::truncate_up_to(string.clone(), 255),
470 val => crate::utils::truncate_up_to(val.to_string(), 255),
471 })
472 }
473
474 pub fn extract_username(&self, value: &serde_json::Value) -> Result<String, anyhow::Error> {
475 let Some(path) = &self.username_path else {
476 return Ok(rand::distr::Alphanumeric.sample_string(&mut rand::rng(), 10));
477 };
478
479 Ok(match self.extract_required("username", path, value)? {
480 serde_json::Value::String(string) => crate::utils::truncate_up_to(string.clone(), 15),
481 val => crate::utils::truncate_up_to(val.to_string(), 15),
482 })
483 }
484
485 fn extract_optional_name(
486 path: Option<&String>,
487 value: &serde_json::Value,
488 ) -> Result<Option<String>, anyhow::Error> {
489 let path = match path {
490 Some(path) => serde_json_path::JsonPath::parse(path)?,
491 None => return Ok(None),
492 };
493
494 Ok(match path.query(value).first() {
495 None | Some(serde_json::Value::Null) => None,
496 Some(serde_json::Value::String(string)) => {
497 if string.is_empty() {
498 None
499 } else {
500 Some(crate::utils::truncate_up_to(string.clone(), 255))
501 }
502 }
503 Some(val) => Some(crate::utils::truncate_up_to(val.to_string(), 255)),
504 })
505 }
506
507 pub fn extract_name_first(
508 &self,
509 value: &serde_json::Value,
510 ) -> Result<Option<String>, anyhow::Error> {
511 Self::extract_optional_name(self.name_first_path.as_ref(), value)
512 }
513
514 pub fn extract_name_last(
515 &self,
516 value: &serde_json::Value,
517 ) -> Result<Option<String>, anyhow::Error> {
518 Self::extract_optional_name(self.name_last_path.as_ref(), value)
519 }
520
521 pub fn extract_avatar_url(
533 &self,
534 value: &serde_json::Value,
535 ) -> Result<Option<String>, anyhow::Error> {
536 match &self.avatar_url_template {
537 Some(template) => Self::resolve_avatar_url_template(template, value),
538 None => Ok(None),
539 }
540 }
541
542 fn resolve_avatar_url_template(
543 template: &str,
544 value: &serde_json::Value,
545 ) -> Result<Option<String>, anyhow::Error> {
546 let verbatim = template.starts_with('{')
547 && template.ends_with('}')
548 && template.matches('{').count() == 1;
549
550 let mut url = String::new();
551 let mut rest = template;
552
553 while let Some(start) = rest.find('{') {
554 let Some(end) = rest[start..].find('}').map(|end| start + end) else {
555 return Err(crate::response::DisplayError::new(
556 "unterminated placeholder in avatar url template",
557 )
558 .into());
559 };
560
561 url.push_str(&rest[..start]);
562
563 let resolved = match serde_json_path::JsonPath::parse(&rest[start + 1..end])?
564 .query(value)
565 .first()
566 {
567 None | Some(serde_json::Value::Null) => return Ok(None),
568 Some(serde_json::Value::String(string)) if string.is_empty() => return Ok(None),
569 Some(serde_json::Value::String(string)) => string.clone(),
570 Some(val) => val.to_string(),
571 };
572
573 if verbatim {
574 url.push_str(&resolved);
575 } else {
576 url.push_str(&urlencoding::encode(&resolved));
577 }
578
579 rest = &rest[end + 1..];
580 }
581
582 url.push_str(rest);
583
584 if url.len() > Self::MAX_AVATAR_URL_LENGTH {
585 return Err(
586 crate::response::DisplayError::new("resolved avatar url is too long").into(),
587 );
588 }
589
590 let url = reqwest::Url::parse(&url)?;
591 if !matches!(url.scheme(), "http" | "https") {
592 return Err(crate::response::DisplayError::new(format!(
593 "avatar url scheme {} is not http or https",
594 url.scheme()
595 ))
596 .into());
597 }
598
599 Ok(Some(url.into()))
600 }
601
602 pub async fn discover(
608 state: &crate::State,
609 url: &reqwest::Url,
610 ) -> Result<DiscoveredOAuthProvider, anyhow::Error> {
611 const WELL_KNOWN_PATH: &str = "/.well-known/openid-configuration";
612
613 let mut url = url.clone();
614 if !url.path().ends_with(WELL_KNOWN_PATH) {
615 let path = format!("{}{WELL_KNOWN_PATH}", url.path().trim_end_matches('/'));
616 url.set_path(&path);
617 url.set_query(None);
618 }
619 url.set_fragment(None);
620
621 let request =
622 match crate::net::outbound_request(&state.env, reqwest::Method::GET, url.clone()) {
623 Ok(request) => request,
624 Err(err) => {
625 return Err(crate::response::DisplayError::new(format!(
626 "failed to fetch {url}: {err:#}"
627 ))
628 .into());
629 }
630 };
631
632 let response = match request
633 .header(reqwest::header::ACCEPT, "application/json")
634 .send()
635 .await
636 {
637 Ok(response) => response,
638 Err(err) => {
639 return Err(crate::response::DisplayError::new(format!(
640 "failed to fetch {url}: {:#}",
641 anyhow::Error::from(err)
642 ))
643 .into());
644 }
645 };
646
647 if !response.status().is_success() {
648 return Err(crate::response::DisplayError::new(format!(
649 "{url} responded with status {}",
650 response.status()
651 ))
652 .into());
653 }
654
655 let mut content = Vec::new();
656 let mut stream = response.bytes_stream();
657
658 while let Some(chunk) = stream.try_next().await? {
659 if content.len() + chunk.len() > Self::MAX_DISCOVERY_SIZE {
660 return Err(crate::response::DisplayError::new(
661 "openid configuration is too large",
662 )
663 .into());
664 }
665
666 content.extend_from_slice(&chunk);
667 }
668
669 let Ok(configuration) = serde_json::from_slice::<OpenIdConfiguration>(&content) else {
670 return Err(crate::response::DisplayError::new(format!(
671 "{url} did not return a valid openid configuration"
672 ))
673 .into());
674 };
675
676 let Some(info_url) = configuration.userinfo_endpoint else {
677 return Err(crate::response::DisplayError::new(
678 "openid configuration does not include a userinfo endpoint",
679 )
680 .into());
681 };
682
683 let lists = |list: &Option<Vec<String>>, item: &str| {
684 list.as_ref()
685 .is_none_or(|list| list.iter().any(|entry| entry == item))
686 };
687 let scope_email = lists(&configuration.scopes_supported, "email");
688 let scope_profile = lists(&configuration.scopes_supported, "profile");
689 let profile_claim =
690 |claim: &str| scope_profile && lists(&configuration.claims_supported, claim);
691
692 let graph_userinfo = reqwest::Url::parse(&info_url)
693 .is_ok_and(|info_url| info_url.host_str() == Some("graph.microsoft.com"));
694
695 let mut scopes = vec![compact_str::CompactString::const_new("openid")];
696 if scope_email {
697 scopes.push("email".into());
698 }
699 if scope_profile {
700 scopes.push("profile".into());
701 }
702
703 let name = reqwest::Url::parse(&configuration.issuer)
704 .ok()
705 .and_then(|issuer| issuer.host_str().map(Into::into))
706 .or_else(|| url.host_str().map(Into::into))
707 .unwrap_or_else(|| configuration.issuer.as_str().into());
708
709 Ok(DiscoveredOAuthProvider {
710 name,
711 auth_url: configuration.authorization_endpoint,
712 token_url: configuration.token_endpoint,
713 info_url,
714 scopes,
715 identifier_path: "$.sub".into(),
716 email_path: (scope_email && lists(&configuration.claims_supported, "email"))
717 .then(|| "$.email".into()),
718 username_path: (profile_claim("preferred_username") && !graph_userinfo)
719 .then(|| "$.preferred_username".into()),
720 name_first_path: profile_claim("given_name").then(|| "$.given_name".into()),
721 name_last_path: profile_claim("family_name").then(|| "$.family_name".into()),
722 avatar_url_template: (profile_claim("picture") && !graph_userinfo)
723 .then(|| "{$.picture}".into()),
724 basic_auth: configuration
725 .token_endpoint_auth_methods_supported
726 .is_some_and(|methods| {
727 !methods.iter().any(|method| method == "client_secret_post")
728 && methods.iter().any(|method| method == "client_secret_basic")
729 }),
730 pkce: configuration
731 .code_challenge_methods_supported
732 .is_some_and(|methods| methods.iter().any(|method| method == "S256")),
733 })
734 }
735}
736
737#[derive(Deserialize)]
738struct OpenIdConfiguration {
739 issuer: String,
740 authorization_endpoint: String,
741 token_endpoint: String,
742 userinfo_endpoint: Option<String>,
743 scopes_supported: Option<Vec<String>>,
744 claims_supported: Option<Vec<String>>,
745 code_challenge_methods_supported: Option<Vec<String>>,
746 token_endpoint_auth_methods_supported: Option<Vec<String>>,
747}
748
749#[derive(ToSchema, Serialize)]
750pub struct DiscoveredOAuthProvider {
751 pub name: compact_str::CompactString,
752
753 pub auth_url: String,
754 pub token_url: String,
755 pub info_url: String,
756 pub scopes: Vec<compact_str::CompactString>,
757
758 pub identifier_path: String,
759 pub email_path: Option<String>,
760 pub username_path: Option<String>,
761 pub name_first_path: Option<String>,
762 pub name_last_path: Option<String>,
763 pub avatar_url_template: Option<String>,
764
765 pub basic_auth: bool,
766 pub pkce: bool,
767}
768
769#[async_trait::async_trait]
770impl IntoAdminApiObject for OAuthProvider {
771 type AdminApiObject = AdminApiOAuthProvider;
772 type ExtraArgs<'a> = ();
773
774 async fn into_admin_api_object<'a>(
775 self,
776 state: &crate::State,
777 _args: Self::ExtraArgs<'a>,
778 ) -> Result<Self::AdminApiObject, crate::database::DatabaseError> {
779 let api_object = AdminApiOAuthProvider::init_hooks(&self, state).await?;
780
781 let api_object = finish_extendible!(
782 AdminApiOAuthProvider {
783 uuid: self.uuid,
784 name: self.name,
785 description: self.description,
786 client_id: self.client_id,
787 client_secret: match self.client_secret {
788 Some(client_secret) => Some(client_secret.decrypt(&state.database).await?),
789 None => None,
790 },
791 auth_url: self.auth_url,
792 token_url: self.token_url,
793 info_url: self.info_url,
794 info_urls: self.info_urls,
795 scopes: self.scopes,
796 identifier_path: self.identifier_path,
797 email_path: self.email_path,
798 username_path: self.username_path,
799 name_first_path: self.name_first_path,
800 name_last_path: self.name_last_path,
801 avatar_url_template: self.avatar_url_template,
802 avatar_overwrite: self.avatar_overwrite,
803 enabled: self.enabled,
804 login_only: self.login_only,
805 login_bypass_two_factor: self.login_bypass_two_factor,
806 link_viewable: self.link_viewable,
807 user_manageable: self.user_manageable,
808 basic_auth: self.basic_auth,
809 pkce: self.pkce,
810 created: self.created.and_utc(),
811 },
812 api_object,
813 state
814 )?;
815
816 Ok(api_object)
817 }
818}
819
820#[async_trait::async_trait]
821impl IntoApiObject for OAuthProvider {
822 type ApiObject = ApiOAuthProvider;
823 type ExtraArgs<'a> = ();
824
825 async fn into_api_object<'a>(
826 self,
827 state: &crate::State,
828 _args: Self::ExtraArgs<'a>,
829 ) -> Result<Self::ApiObject, crate::database::DatabaseError> {
830 let api_object = ApiOAuthProvider::init_hooks(&self, state).await?;
831
832 let api_object = finish_extendible!(
833 ApiOAuthProvider {
834 uuid: self.uuid,
835 name: self.name,
836 link_viewable: self.link_viewable,
837 user_manageable: self.user_manageable,
838 },
839 api_object,
840 state
841 )?;
842
843 Ok(api_object)
844 }
845}
846
847#[async_trait::async_trait]
848impl ByUuid for OAuthProvider {
849 async fn by_uuid(
850 database: &crate::database::Database,
851 uuid: uuid::Uuid,
852 ) -> Result<Self, crate::database::DatabaseError> {
853 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
854 r#"
855 SELECT {}
856 FROM oauth_providers
857 WHERE oauth_providers.uuid = $1
858 "#,
859 Self::columns_sql(None)
860 )))
861 .bind(uuid)
862 .fetch_one(database.read())
863 .await?;
864
865 Self::map(None, &row)
866 }
867
868 async fn by_uuid_with_transaction(
869 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
870 uuid: uuid::Uuid,
871 ) -> Result<Self, crate::database::DatabaseError> {
872 let row = sqlx::query(sqlx::AssertSqlSafe(format!(
873 r#"
874 SELECT {}
875 FROM oauth_providers
876 WHERE oauth_providers.uuid = $1
877 "#,
878 Self::columns_sql(None)
879 )))
880 .bind(uuid)
881 .fetch_one(&mut **transaction)
882 .await?;
883
884 Self::map(None, &row)
885 }
886}
887
888const MAX_INFO_URLS: usize = 8;
889const MAX_INFO_URL_LENGTH: usize = 2048;
890
891pub fn validate_info_urls(
892 info_urls: &IndexMap<compact_str::CompactString, String>,
893 _context: &(),
894) -> garde::Result {
895 if info_urls.len() > MAX_INFO_URLS {
896 return Err(garde::Error::new(compact_str::format_compact!(
897 "an oauth provider cannot have more than {MAX_INFO_URLS} additional info urls"
898 )));
899 }
900
901 for (key, url) in info_urls {
902 if key.is_empty()
903 || key.len() > 64
904 || !key
905 .chars()
906 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
907 {
908 return Err(garde::Error::new(compact_str::format_compact!(
909 "info url key {key:?} must be 1-64 characters of a-z, 0-9, _ or -"
910 )));
911 }
912
913 if url.len() > MAX_INFO_URL_LENGTH {
914 return Err(garde::Error::new(compact_str::format_compact!(
915 "info url {key} must be at most {MAX_INFO_URL_LENGTH} characters"
916 )));
917 }
918
919 if !reqwest::Url::parse(url).is_ok_and(|url| matches!(url.scheme(), "http" | "https")) {
920 return Err(garde::Error::new(compact_str::format_compact!(
921 "info url {key} must be a valid http or https url"
922 )));
923 }
924 }
925
926 Ok(())
927}
928
929#[derive(ToSchema, Deserialize, Validate)]
930pub struct CreateOAuthProviderOptions {
931 #[garde(length(chars, min = 1, max = 255))]
932 #[schema(min_length = 1, max_length = 255)]
933 pub name: compact_str::CompactString,
934 #[garde(length(chars, min = 1, max = 1024))]
935 #[schema(min_length = 1, max_length = 1024)]
936 pub description: Option<compact_str::CompactString>,
937 #[garde(skip)]
938 pub enabled: bool,
939 #[garde(skip)]
940 pub login_only: bool,
941 #[garde(skip)]
942 pub login_bypass_two_factor: bool,
943 #[garde(skip)]
944 pub link_viewable: bool,
945 #[garde(skip)]
946 pub user_manageable: bool,
947 #[garde(skip)]
948 pub basic_auth: bool,
949 #[garde(skip)]
950 #[serde(default)]
951 pub pkce: bool,
952
953 #[garde(length(chars, min = 3, max = 255))]
954 #[schema(min_length = 3, max_length = 255)]
955 pub client_id: compact_str::CompactString,
956 #[garde(length(chars, min = 3, max = 255))]
957 #[schema(min_length = 3, max_length = 255)]
958 pub client_secret: Option<compact_str::CompactString>,
959
960 #[garde(length(chars, min = 3, max = 255))]
961 #[schema(min_length = 3, max_length = 255)]
962 pub auth_url: String,
963 #[garde(length(chars, min = 3, max = 255))]
964 #[schema(min_length = 3, max_length = 255)]
965 pub token_url: String,
966 #[garde(length(chars, min = 3, max = 255))]
967 #[schema(min_length = 3, max_length = 255)]
968 pub info_url: String,
969 #[garde(custom(validate_info_urls))]
970 #[serde(default)]
971 pub info_urls: IndexMap<compact_str::CompactString, String>,
972 #[garde(length(max = 255))]
973 #[schema(max_length = 255)]
974 pub scopes: Vec<compact_str::CompactString>,
975
976 #[garde(
977 length(chars, min = 3, max = 255),
978 custom(crate::utils::validate_json_path)
979 )]
980 #[schema(min_length = 3, max_length = 255)]
981 pub identifier_path: String,
982 #[garde(
983 length(chars, min = 1, max = 255),
984 inner(custom(crate::utils::validate_json_path))
985 )]
986 #[schema(min_length = 1, max_length = 255)]
987 pub email_path: Option<String>,
988 #[garde(
989 length(chars, min = 1, max = 255),
990 inner(custom(crate::utils::validate_json_path))
991 )]
992 #[schema(min_length = 1, max_length = 255)]
993 pub username_path: Option<String>,
994 #[garde(
995 length(chars, min = 1, max = 255),
996 inner(custom(crate::utils::validate_json_path))
997 )]
998 #[schema(min_length = 1, max_length = 255)]
999 pub name_first_path: Option<String>,
1000 #[garde(
1001 length(chars, min = 1, max = 255),
1002 inner(custom(crate::utils::validate_json_path))
1003 )]
1004 #[schema(min_length = 1, max_length = 255)]
1005 pub name_last_path: Option<String>,
1006 #[garde(
1007 length(chars, min = 1, max = 255),
1008 inner(custom(crate::utils::validate_avatar_url_template))
1009 )]
1010 #[schema(min_length = 1, max_length = 255)]
1011 pub avatar_url_template: Option<String>,
1012 #[garde(skip)]
1013 #[serde(default)]
1014 pub avatar_overwrite: bool,
1015}
1016
1017#[async_trait::async_trait]
1018impl CreatableModel for OAuthProvider {
1019 type CreateOptions<'a> = CreateOAuthProviderOptions;
1020 type CreateResult = Self;
1021
1022 fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
1023 static CREATE_LISTENERS: LazyLock<CreateListenerList<OAuthProvider>> =
1024 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
1025
1026 &CREATE_LISTENERS
1027 }
1028
1029 async fn create_with_transaction(
1030 state: &crate::State,
1031 mut options: Self::CreateOptions<'_>,
1032 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
1033 ) -> Result<Self, crate::database::DatabaseError> {
1034 options.validate()?;
1035
1036 let mut query_builder = InsertQueryBuilder::new("oauth_providers");
1037
1038 Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
1039
1040 let encrypted_client_secret = if let Some(ref client_secret) = options.client_secret {
1041 Some(
1042 EncryptedString::from_plaintext(client_secret.to_string(), &state.database)
1043 .await
1044 .map_err(|err| sqlx::Error::Encode(err.into()))?,
1045 )
1046 } else {
1047 None
1048 };
1049
1050 query_builder
1051 .set("name", &options.name)
1052 .set("description", &options.description)
1053 .set("client_id", &options.client_id)
1054 .set("client_secret", encrypted_client_secret)
1055 .set("auth_url", &options.auth_url)
1056 .set("token_url", &options.token_url)
1057 .set("info_url", &options.info_url)
1058 .set("info_urls", OrderedJson(&options.info_urls))
1059 .set("scopes", &options.scopes)
1060 .set("identifier_path", &options.identifier_path)
1061 .set("email_path", &options.email_path)
1062 .set("username_path", &options.username_path)
1063 .set("name_first_path", &options.name_first_path)
1064 .set("name_last_path", &options.name_last_path)
1065 .set("avatar_url_template", &options.avatar_url_template)
1066 .set("avatar_overwrite", options.avatar_overwrite)
1067 .set("enabled", options.enabled)
1068 .set("login_only", options.login_only)
1069 .set("login_bypass_two_factor", options.login_bypass_two_factor)
1070 .set("link_viewable", options.link_viewable)
1071 .set("user_manageable", options.user_manageable)
1072 .set("basic_auth", options.basic_auth)
1073 .set("pkce", options.pkce);
1074
1075 let row = query_builder
1076 .returning(&Self::columns_sql(None))
1077 .fetch_one(&mut **transaction)
1078 .await?;
1079 let mut oauth_provider = Self::map(None, &row)?;
1080
1081 Self::run_after_create_handlers(&mut oauth_provider, &options, state, transaction).await?;
1082
1083 Ok(oauth_provider)
1084 }
1085}
1086
1087#[derive(ToSchema, Serialize, Deserialize, Validate, Clone, Default)]
1088pub struct UpdateOAuthProviderOptions {
1089 #[garde(length(chars, min = 1, max = 255))]
1090 #[schema(min_length = 1, max_length = 255)]
1091 pub name: Option<compact_str::CompactString>,
1092 #[garde(length(chars, min = 1, max = 1024))]
1093 #[schema(min_length = 1, max_length = 1024)]
1094 #[serde(
1095 default,
1096 skip_serializing_if = "Option::is_none",
1097 with = "::serde_with::rust::double_option"
1098 )]
1099 pub description: Option<Option<compact_str::CompactString>>,
1100 #[garde(skip)]
1101 pub enabled: Option<bool>,
1102 #[garde(skip)]
1103 pub login_only: Option<bool>,
1104 #[garde(skip)]
1105 pub login_bypass_two_factor: Option<bool>,
1106 #[garde(skip)]
1107 pub link_viewable: Option<bool>,
1108 #[garde(skip)]
1109 pub user_manageable: Option<bool>,
1110 #[garde(skip)]
1111 pub basic_auth: Option<bool>,
1112 #[garde(skip)]
1113 pub pkce: Option<bool>,
1114
1115 #[garde(length(chars, min = 3, max = 255))]
1116 #[schema(min_length = 3, max_length = 255)]
1117 pub client_id: Option<compact_str::CompactString>,
1118 #[garde(length(chars, min = 3, max = 255))]
1119 #[schema(min_length = 3, max_length = 255)]
1120 #[serde(
1121 default,
1122 skip_serializing_if = "Option::is_none",
1123 with = "::serde_with::rust::double_option"
1124 )]
1125 pub client_secret: Option<Option<compact_str::CompactString>>,
1126
1127 #[garde(length(chars, min = 3, max = 255))]
1128 #[schema(min_length = 3, max_length = 255)]
1129 pub auth_url: Option<String>,
1130 #[garde(length(chars, min = 3, max = 255))]
1131 #[schema(min_length = 3, max_length = 255)]
1132 pub token_url: Option<String>,
1133 #[garde(length(chars, min = 3, max = 255))]
1134 #[schema(min_length = 3, max_length = 255)]
1135 pub info_url: Option<String>,
1136 #[garde(inner(custom(validate_info_urls)))]
1137 pub info_urls: Option<IndexMap<compact_str::CompactString, String>>,
1138 #[garde(length(max = 255))]
1139 #[schema(max_length = 255)]
1140 pub scopes: Option<Vec<compact_str::CompactString>>,
1141
1142 #[garde(
1143 length(chars, min = 3, max = 255),
1144 inner(custom(crate::utils::validate_json_path))
1145 )]
1146 #[schema(min_length = 3, max_length = 255)]
1147 pub identifier_path: Option<String>,
1148 #[garde(
1149 length(chars, min = 1, max = 255),
1150 inner(inner(custom(crate::utils::validate_json_path)))
1151 )]
1152 #[schema(min_length = 1, max_length = 255)]
1153 #[serde(
1154 default,
1155 skip_serializing_if = "Option::is_none",
1156 with = "::serde_with::rust::double_option"
1157 )]
1158 pub email_path: Option<Option<String>>,
1159 #[garde(
1160 length(chars, min = 1, max = 255),
1161 inner(inner(custom(crate::utils::validate_json_path)))
1162 )]
1163 #[schema(min_length = 1, max_length = 255)]
1164 #[serde(
1165 default,
1166 skip_serializing_if = "Option::is_none",
1167 with = "::serde_with::rust::double_option"
1168 )]
1169 pub username_path: Option<Option<String>>,
1170 #[garde(
1171 length(chars, min = 1, max = 255),
1172 inner(inner(custom(crate::utils::validate_json_path)))
1173 )]
1174 #[schema(min_length = 1, max_length = 255)]
1175 #[serde(
1176 default,
1177 skip_serializing_if = "Option::is_none",
1178 with = "::serde_with::rust::double_option"
1179 )]
1180 pub name_first_path: Option<Option<String>>,
1181 #[garde(
1182 length(chars, min = 1, max = 255),
1183 inner(inner(custom(crate::utils::validate_json_path)))
1184 )]
1185 #[schema(min_length = 1, max_length = 255)]
1186 #[serde(
1187 default,
1188 skip_serializing_if = "Option::is_none",
1189 with = "::serde_with::rust::double_option"
1190 )]
1191 pub name_last_path: Option<Option<String>>,
1192 #[garde(
1193 length(chars, min = 1, max = 255),
1194 inner(inner(custom(crate::utils::validate_avatar_url_template)))
1195 )]
1196 #[schema(min_length = 1, max_length = 255)]
1197 #[serde(
1198 default,
1199 skip_serializing_if = "Option::is_none",
1200 with = "::serde_with::rust::double_option"
1201 )]
1202 pub avatar_url_template: Option<Option<String>>,
1203 #[garde(skip)]
1204 pub avatar_overwrite: Option<bool>,
1205}
1206
1207impl UpdateOAuthProviderOptions {
1208 pub fn changes_identity(&self, oauth_provider: &OAuthProvider) -> bool {
1211 fn differs<T: PartialEq>(new: &Option<T>, current: &T) -> bool {
1212 new.as_ref().is_some_and(|new| new != current)
1213 }
1214
1215 differs(&self.client_id, &oauth_provider.client_id)
1216 || differs(&self.auth_url, &oauth_provider.auth_url)
1217 || differs(&self.token_url, &oauth_provider.token_url)
1218 || differs(&self.info_url, &oauth_provider.info_url)
1219 || differs(&self.info_urls, &oauth_provider.info_urls)
1220 || differs(&self.scopes, &oauth_provider.scopes)
1221 || differs(&self.identifier_path, &oauth_provider.identifier_path)
1222 || differs(&self.login_only, &oauth_provider.login_only)
1223 || differs(
1224 &self.login_bypass_two_factor,
1225 &oauth_provider.login_bypass_two_factor,
1226 )
1227 || differs(&self.user_manageable, &oauth_provider.user_manageable)
1228 }
1229}
1230
1231#[async_trait::async_trait]
1232impl UpdatableModel for OAuthProvider {
1233 type UpdateOptions = UpdateOAuthProviderOptions;
1234
1235 fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
1236 static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<OAuthProvider>> =
1237 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
1238
1239 &UPDATE_LISTENERS
1240 }
1241
1242 async fn update_with_transaction(
1243 &mut self,
1244 state: &crate::State,
1245 mut options: Self::UpdateOptions,
1246 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
1247 ) -> Result<(), crate::database::DatabaseError> {
1248 options.validate()?;
1249
1250 let mut query_builder = UpdateQueryBuilder::new("oauth_providers");
1251
1252 self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
1253 .await?;
1254
1255 let encrypted_client_secret = match options.client_secret {
1256 Some(Some(ref client_secret)) => Some(Some(
1257 EncryptedString::from_plaintext(client_secret.to_string(), &state.database)
1258 .await
1259 .map_err(|err| sqlx::Error::Encode(err.into()))?,
1260 )),
1261 Some(None) => Some(None),
1262 None => None,
1263 };
1264
1265 query_builder
1266 .set("name", options.name.as_ref())
1267 .set(
1268 "description",
1269 options.description.as_ref().map(|d| d.as_ref()),
1270 )
1271 .set("client_id", options.client_id.as_ref())
1272 .set("client_secret", encrypted_client_secret.clone())
1273 .set("auth_url", options.auth_url.as_ref())
1274 .set("token_url", options.token_url.as_ref())
1275 .set("info_url", options.info_url.as_ref())
1276 .set("info_urls", options.info_urls.as_ref().map(OrderedJson))
1277 .set("scopes", options.scopes.as_ref())
1278 .set("identifier_path", options.identifier_path.as_ref())
1279 .set(
1280 "email_path",
1281 options.email_path.as_ref().map(|e| e.as_ref()),
1282 )
1283 .set(
1284 "username_path",
1285 options.username_path.as_ref().map(|u| u.as_ref()),
1286 )
1287 .set(
1288 "name_first_path",
1289 options.name_first_path.as_ref().map(|n| n.as_ref()),
1290 )
1291 .set(
1292 "name_last_path",
1293 options.name_last_path.as_ref().map(|n| n.as_ref()),
1294 )
1295 .set(
1296 "avatar_url_template",
1297 options.avatar_url_template.as_ref().map(|a| a.as_ref()),
1298 )
1299 .set("avatar_overwrite", options.avatar_overwrite)
1300 .set("enabled", options.enabled)
1301 .set("login_only", options.login_only)
1302 .set("login_bypass_two_factor", options.login_bypass_two_factor)
1303 .set("link_viewable", options.link_viewable)
1304 .set("user_manageable", options.user_manageable)
1305 .set("basic_auth", options.basic_auth)
1306 .set("pkce", options.pkce)
1307 .where_eq("uuid", self.uuid);
1308
1309 query_builder.execute(&mut **transaction).await?;
1310
1311 if let Some(name) = options.name {
1312 self.name = name;
1313 }
1314 if let Some(description) = options.description {
1315 self.description = description;
1316 }
1317 if let Some(enabled) = options.enabled {
1318 self.enabled = enabled;
1319 }
1320 if let Some(login_only) = options.login_only {
1321 self.login_only = login_only;
1322 }
1323 if let Some(login_bypass_two_factor) = options.login_bypass_two_factor {
1324 self.login_bypass_two_factor = login_bypass_two_factor;
1325 }
1326 if let Some(link_viewable) = options.link_viewable {
1327 self.link_viewable = link_viewable;
1328 }
1329 if let Some(user_manageable) = options.user_manageable {
1330 self.user_manageable = user_manageable;
1331 }
1332 if let Some(basic_auth) = options.basic_auth {
1333 self.basic_auth = basic_auth;
1334 }
1335 if let Some(pkce) = options.pkce {
1336 self.pkce = pkce;
1337 }
1338 if let Some(client_id) = options.client_id {
1339 self.client_id = client_id;
1340 }
1341 if let Some(client_secret) = encrypted_client_secret {
1342 self.client_secret = client_secret;
1343 }
1344 if let Some(auth_url) = options.auth_url {
1345 self.auth_url = auth_url;
1346 }
1347 if let Some(token_url) = options.token_url {
1348 self.token_url = token_url;
1349 }
1350 if let Some(info_url) = options.info_url {
1351 self.info_url = info_url;
1352 }
1353 if let Some(info_urls) = options.info_urls {
1354 self.info_urls = info_urls;
1355 }
1356 if let Some(scopes) = options.scopes {
1357 self.scopes = scopes;
1358 }
1359 if let Some(identifier_path) = options.identifier_path {
1360 self.identifier_path = identifier_path;
1361 }
1362 if let Some(email_path) = options.email_path {
1363 self.email_path = email_path;
1364 }
1365 if let Some(username_path) = options.username_path {
1366 self.username_path = username_path;
1367 }
1368 if let Some(name_first_path) = options.name_first_path {
1369 self.name_first_path = name_first_path;
1370 }
1371 if let Some(name_last_path) = options.name_last_path {
1372 self.name_last_path = name_last_path;
1373 }
1374 if let Some(avatar_url_template) = options.avatar_url_template {
1375 self.avatar_url_template = avatar_url_template;
1376 }
1377 if let Some(avatar_overwrite) = options.avatar_overwrite {
1378 self.avatar_overwrite = avatar_overwrite;
1379 }
1380
1381 self.run_after_update_handlers(state, transaction).await?;
1382
1383 Ok(())
1384 }
1385}
1386
1387#[async_trait::async_trait]
1388impl DeletableModel for OAuthProvider {
1389 type DeleteOptions = ();
1390
1391 fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
1392 static DELETE_LISTENERS: LazyLock<DeleteHandlerList<OAuthProvider>> =
1393 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
1394
1395 &DELETE_LISTENERS
1396 }
1397
1398 async fn delete_with_transaction(
1399 &self,
1400 state: &crate::State,
1401 options: Self::DeleteOptions,
1402 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
1403 ) -> Result<(), anyhow::Error> {
1404 self.run_delete_handlers(&options, state, transaction)
1405 .await?;
1406
1407 sqlx::query(
1408 r#"
1409 DELETE FROM oauth_providers
1410 WHERE oauth_providers.uuid = $1
1411 "#,
1412 )
1413 .bind(self.uuid)
1414 .execute(&mut **transaction)
1415 .await?;
1416
1417 self.run_after_delete_handlers(&options, state, transaction)
1418 .await?;
1419
1420 Ok(())
1421 }
1422}
1423
1424#[derive(Validate)]
1425pub struct DuplicateOAuthProviderOptions {
1426 #[garde(length(chars, min = 1, max = 255))]
1427 pub name: compact_str::CompactString,
1428}
1429
1430#[async_trait::async_trait]
1431impl DuplicableModel for OAuthProvider {
1432 type DuplicateOptions<'a> = DuplicateOAuthProviderOptions;
1433 type DuplicateResult = Self;
1434
1435 fn get_duplicate_handlers() -> &'static LazyLock<DuplicateHandlerList<Self>> {
1436 static DUPLICATE_LISTENERS: LazyLock<DuplicateHandlerList<OAuthProvider>> =
1437 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
1438
1439 &DUPLICATE_LISTENERS
1440 }
1441
1442 async fn duplicate_with_transaction(
1443 &self,
1444 state: &crate::State,
1445 options: Self::DuplicateOptions<'_>,
1446 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
1447 ) -> Result<Self, crate::database::DatabaseError> {
1448 options.validate()?;
1449
1450 self.run_duplicate_handlers(&options, state, transaction)
1451 .await?;
1452
1453 let mut query_builder = InsertQueryBuilder::new("oauth_providers");
1454
1455 query_builder
1456 .set("name", &options.name)
1457 .set("description", &self.description)
1458 .set("client_id", &self.client_id)
1459 .set("client_secret", self.client_secret.clone())
1460 .set("auth_url", &self.auth_url)
1461 .set("token_url", &self.token_url)
1462 .set("info_url", &self.info_url)
1463 .set("info_urls", OrderedJson(&self.info_urls))
1464 .set("scopes", &self.scopes)
1465 .set("identifier_path", &self.identifier_path)
1466 .set("email_path", &self.email_path)
1467 .set("username_path", &self.username_path)
1468 .set("name_first_path", &self.name_first_path)
1469 .set("name_last_path", &self.name_last_path)
1470 .set("avatar_url_template", &self.avatar_url_template)
1471 .set("avatar_overwrite", self.avatar_overwrite)
1472 .set("enabled", self.enabled)
1473 .set("login_only", self.login_only)
1474 .set("login_bypass_two_factor", self.login_bypass_two_factor)
1475 .set("link_viewable", self.link_viewable)
1476 .set("user_manageable", self.user_manageable)
1477 .set("basic_auth", self.basic_auth)
1478 .set("pkce", self.pkce);
1479
1480 let row = query_builder
1481 .returning(&Self::columns_sql(None))
1482 .fetch_one(&mut **transaction)
1483 .await?;
1484 let mut oauth_provider = Self::map(None, &row)?;
1485
1486 sqlx::query!(
1487 "INSERT INTO oauth_provider_mappings (oauth_provider_uuid, matcher, mapping)
1488 SELECT $1, oauth_provider_mappings.matcher, oauth_provider_mappings.mapping
1489 FROM oauth_provider_mappings
1490 WHERE oauth_provider_mappings.oauth_provider_uuid = $2",
1491 oauth_provider.uuid,
1492 self.uuid,
1493 )
1494 .execute(&mut **transaction)
1495 .await?;
1496
1497 self.run_after_duplicate_handlers(&mut oauth_provider, &options, state, transaction)
1498 .await?;
1499
1500 Ok(oauth_provider)
1501 }
1502}
1503
1504#[schema_extension_derive::extendible]
1505#[init_args(OAuthProvider, crate::State)]
1506#[hook_args(crate::State)]
1507#[derive(ToSchema, Serialize)]
1508#[schema(title = "AdminOAuthProvider")]
1509pub struct AdminApiOAuthProvider {
1510 pub uuid: uuid::Uuid,
1511
1512 pub name: compact_str::CompactString,
1513 pub description: Option<compact_str::CompactString>,
1514
1515 pub client_id: compact_str::CompactString,
1516 pub client_secret: Option<compact_str::CompactString>,
1517 pub auth_url: String,
1518 pub token_url: String,
1519 pub info_url: String,
1520 pub info_urls: IndexMap<compact_str::CompactString, String>,
1521 pub scopes: Vec<compact_str::CompactString>,
1522
1523 pub identifier_path: String,
1524 pub email_path: Option<String>,
1525 pub username_path: Option<String>,
1526 pub name_first_path: Option<String>,
1527 pub name_last_path: Option<String>,
1528 pub avatar_url_template: Option<String>,
1529 pub avatar_overwrite: bool,
1530
1531 pub enabled: bool,
1532 pub login_only: bool,
1533 pub login_bypass_two_factor: bool,
1534 pub link_viewable: bool,
1535 pub user_manageable: bool,
1536 pub basic_auth: bool,
1537 pub pkce: bool,
1538
1539 pub created: chrono::DateTime<chrono::Utc>,
1540}
1541
1542#[schema_extension_derive::extendible]
1543#[init_args(OAuthProvider, crate::State)]
1544#[hook_args(crate::State)]
1545#[derive(ToSchema, Serialize)]
1546#[schema(title = "OAuthProvider")]
1547pub struct ApiOAuthProvider {
1548 pub uuid: uuid::Uuid,
1549
1550 pub name: compact_str::CompactString,
1551
1552 pub link_viewable: bool,
1553 pub user_manageable: bool,
1554}