Skip to main content

shared/models/
oauth_provider.rs

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    /// Fetches the info url and every additional info url with the user's access token.
254    ///
255    /// An additional info url responding with 404 is left out of the sources, so matchers on it
256    /// fail as for a user outside of e.g. a discord guild. Any other failure aborts, since mappings
257    /// revoking on a transient error would strip access the user still has.
258    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    /// Looks up a required field in the info url response. The response never ends up in the
424    /// returned error, as that is shown to whoever is logging in, so it is logged instead.
425    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    /// Resolves `avatar_url_template` against a provider's user info response, substituting every
522    /// `{$.json.path}` placeholder with the value found at that path.
523    ///
524    /// A template that is nothing but a single placeholder yields that value verbatim, for the
525    /// providers that hand back a whole avatar url (`{$.picture}`). Anywhere else the placeholder
526    /// stands for one path or query component and its value is percent-encoded, so that a provider
527    /// returning a crafted value cannot rewrite the rest of the url
528    /// (`https://cdn.discordapp.com/avatars/{$.id}/{$.avatar}.png`).
529    ///
530    /// Yields `None` when no template is configured or when any placeholder resolves to nothing,
531    /// which is the ordinary "this user has no avatar" case rather than an error.
532    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    /// Fetches an OpenID Connect discovery document and derives provider settings from it.
603    ///
604    /// `url` may be either the issuer or the full `/.well-known/openid-configuration` url. Claim
605    /// paths are only filled in when the provider advertises the claim, as a configured path that
606    /// is missing from the userinfo response fails the login.
607    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    /// Whether this changes how the provider identifies users or how they may sign in with it,
1209    /// which amounts to taking over every linked account.
1210    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}