Skip to main content

mas_storage_pg/oauth2/
client.rs

1// Copyright 2025, 2026 Element Creations Ltd.
2// Copyright 2024, 2025 New Vector Ltd.
3// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
4//
5// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
6// Please see LICENSE files in the repository root for full details.
7
8use std::{
9    collections::{BTreeMap, BTreeSet},
10    string::ToString,
11};
12
13use async_trait::async_trait;
14use mas_data_model::{Client, Clock, JwksOrJwksUri, UlidExt as _};
15use mas_iana::{jose::JsonWebSignatureAlg, oauth::OAuthClientAuthenticationMethod};
16use mas_jose::jwk::PublicJsonWebKeySet;
17use mas_storage::{
18    Page, Pagination,
19    oauth2::{OAuth2ClientFilter, OAuth2ClientKind, OAuth2ClientRepository},
20    pagination::Node,
21};
22use oauth2_types::{oidc::ApplicationType, requests::GrantType};
23use opentelemetry_semantic_conventions::attribute::DB_QUERY_TEXT;
24use rand::Rng;
25use sea_query::{
26    Expr, ExprTrait, PostgresQueryBuilder, Query, SimpleExpr, enum_def,
27    extension::postgres::PgExpr as _,
28};
29use sea_query_sqlx::SqlxBinder;
30use sqlx::PgConnection;
31use tracing::{Instrument, info_span};
32use ulid::Ulid;
33use url::Url;
34use uuid::Uuid;
35
36use crate::{
37    DatabaseError, DatabaseInconsistencyError,
38    filter::{Filter, StatementExt},
39    iden::{OAuth2Clients, OAuth2Sessions},
40    pagination::QueryBuilderExt,
41    tracing::ExecuteExt,
42};
43
44/// An implementation of [`OAuth2ClientRepository`] for a PostgreSQL connection
45pub struct PgOAuth2ClientRepository<'c> {
46    conn: &'c mut PgConnection,
47}
48
49impl<'c> PgOAuth2ClientRepository<'c> {
50    /// Create a new [`PgOAuth2ClientRepository`] from an active PostgreSQL
51    /// connection
52    pub fn new(conn: &'c mut PgConnection) -> Self {
53        Self { conn }
54    }
55}
56
57#[expect(clippy::struct_excessive_bools)]
58#[derive(Debug, sqlx::FromRow)]
59#[enum_def]
60struct OAuth2ClientLookup {
61    oauth2_client_id: Uuid,
62    metadata_digest: Option<String>,
63    encrypted_client_secret: Option<String>,
64    application_type: Option<String>,
65    redirect_uris: Vec<String>,
66    grant_type_authorization_code: bool,
67    grant_type_refresh_token: bool,
68    grant_type_client_credentials: bool,
69    grant_type_device_code: bool,
70    client_name: Option<String>,
71    logo_uri: Option<String>,
72    client_uri: Option<String>,
73    policy_uri: Option<String>,
74    tos_uri: Option<String>,
75    jwks_uri: Option<String>,
76    jwks: Option<serde_json::Value>,
77    id_token_signed_response_alg: Option<String>,
78    userinfo_signed_response_alg: Option<String>,
79    token_endpoint_auth_method: Option<String>,
80    token_endpoint_auth_signing_alg: Option<String>,
81    initiate_login_uri: Option<String>,
82    is_static: bool,
83}
84
85impl Node<Ulid> for OAuth2ClientLookup {
86    fn cursor(&self) -> Ulid {
87        self.oauth2_client_id.into()
88    }
89}
90
91impl Filter for OAuth2ClientFilter<'_> {
92    fn generate_condition(&self, _has_joins: bool) -> impl sea_query::IntoCondition {
93        sea_query::Condition::all()
94            .add_option(self.kind().map(|kind| {
95                let is_static = matches!(kind, OAuth2ClientKind::Static);
96                Expr::col((OAuth2Clients::Table, OAuth2Clients::IsStatic)).eq(is_static)
97            }))
98            .add_option(self.client_name().map(|client_name| {
99                Expr::col((OAuth2Clients::Table, OAuth2Clients::ClientName))
100                    .ilike(format!("%{client_name}%"))
101            }))
102            .add_option(self.client_uri().map(|client_uri| {
103                Expr::col((OAuth2Clients::Table, OAuth2Clients::ClientUri))
104                    .ilike(format!("%{client_uri}%"))
105            }))
106            .add_option(self.grant_type().map(|grant_type| -> SimpleExpr {
107                let column = match grant_type {
108                    GrantType::AuthorizationCode => OAuth2Clients::GrantTypeAuthorizationCode,
109                    GrantType::RefreshToken => OAuth2Clients::GrantTypeRefreshToken,
110                    GrantType::ClientCredentials => OAuth2Clients::GrantTypeClientCredentials,
111                    GrantType::DeviceCode => OAuth2Clients::GrantTypeDeviceCode,
112                    // The other grant types don't have a dedicated column, so no
113                    // client can declare them: the filter matches nothing.
114                    _ => return Expr::val(false),
115                };
116                Expr::col((OAuth2Clients::Table, column)).eq(true)
117            }))
118            .add_option(self.has_active_sessions().map(|has| -> SimpleExpr {
119                let exists = Expr::exists(
120                    Query::select()
121                        .expr(Expr::cust("1"))
122                        .from(OAuth2Sessions::Table)
123                        .and_where(
124                            Expr::col((OAuth2Sessions::Table, OAuth2Sessions::OAuth2ClientId))
125                                .equals((OAuth2Clients::Table, OAuth2Clients::OAuth2ClientId)),
126                        )
127                        .and_where(
128                            Expr::col((OAuth2Sessions::Table, OAuth2Sessions::FinishedAt))
129                                .is_null(),
130                        )
131                        .take(),
132                );
133                if has { exists } else { exists.not() }
134            }))
135    }
136}
137
138impl TryFrom<OAuth2ClientLookup> for Client {
139    type Error = DatabaseInconsistencyError;
140
141    fn try_from(value: OAuth2ClientLookup) -> Result<Self, Self::Error> {
142        let id = Ulid::from(value.oauth2_client_id);
143
144        let redirect_uris: Result<Vec<Url>, _> =
145            value.redirect_uris.iter().map(|s| s.parse()).collect();
146        let redirect_uris = redirect_uris.map_err(|e| {
147            DatabaseInconsistencyError::on("oauth2_clients")
148                .column("redirect_uris")
149                .row(id)
150                .source(e)
151        })?;
152
153        let application_type = value
154            .application_type
155            .map(|s| s.parse())
156            .transpose()
157            .map_err(|e| {
158                DatabaseInconsistencyError::on("oauth2_clients")
159                    .column("application_type")
160                    .row(id)
161                    .source(e)
162            })?;
163
164        let mut grant_types = Vec::new();
165        if value.grant_type_authorization_code {
166            grant_types.push(GrantType::AuthorizationCode);
167        }
168        if value.grant_type_refresh_token {
169            grant_types.push(GrantType::RefreshToken);
170        }
171        if value.grant_type_client_credentials {
172            grant_types.push(GrantType::ClientCredentials);
173        }
174        if value.grant_type_device_code {
175            grant_types.push(GrantType::DeviceCode);
176        }
177
178        let logo_uri = value.logo_uri.map(|s| s.parse()).transpose().map_err(|e| {
179            DatabaseInconsistencyError::on("oauth2_clients")
180                .column("logo_uri")
181                .row(id)
182                .source(e)
183        })?;
184
185        let client_uri = value
186            .client_uri
187            .map(|s| s.parse())
188            .transpose()
189            .map_err(|e| {
190                DatabaseInconsistencyError::on("oauth2_clients")
191                    .column("client_uri")
192                    .row(id)
193                    .source(e)
194            })?;
195
196        let policy_uri = value
197            .policy_uri
198            .map(|s| s.parse())
199            .transpose()
200            .map_err(|e| {
201                DatabaseInconsistencyError::on("oauth2_clients")
202                    .column("policy_uri")
203                    .row(id)
204                    .source(e)
205            })?;
206
207        let tos_uri = value.tos_uri.map(|s| s.parse()).transpose().map_err(|e| {
208            DatabaseInconsistencyError::on("oauth2_clients")
209                .column("tos_uri")
210                .row(id)
211                .source(e)
212        })?;
213
214        let id_token_signed_response_alg = value
215            .id_token_signed_response_alg
216            .map(|s| s.parse())
217            .transpose()
218            .map_err(|e| {
219                DatabaseInconsistencyError::on("oauth2_clients")
220                    .column("id_token_signed_response_alg")
221                    .row(id)
222                    .source(e)
223            })?;
224
225        let userinfo_signed_response_alg = value
226            .userinfo_signed_response_alg
227            .map(|s| s.parse())
228            .transpose()
229            .map_err(|e| {
230                DatabaseInconsistencyError::on("oauth2_clients")
231                    .column("userinfo_signed_response_alg")
232                    .row(id)
233                    .source(e)
234            })?;
235
236        let token_endpoint_auth_method = value
237            .token_endpoint_auth_method
238            .map(|s| s.parse())
239            .transpose()
240            .map_err(|e| {
241                DatabaseInconsistencyError::on("oauth2_clients")
242                    .column("token_endpoint_auth_method")
243                    .row(id)
244                    .source(e)
245            })?;
246
247        let token_endpoint_auth_signing_alg = value
248            .token_endpoint_auth_signing_alg
249            .map(|s| s.parse())
250            .transpose()
251            .map_err(|e| {
252                DatabaseInconsistencyError::on("oauth2_clients")
253                    .column("token_endpoint_auth_signing_alg")
254                    .row(id)
255                    .source(e)
256            })?;
257
258        let initiate_login_uri = value
259            .initiate_login_uri
260            .map(|s| s.parse())
261            .transpose()
262            .map_err(|e| {
263                DatabaseInconsistencyError::on("oauth2_clients")
264                    .column("initiate_login_uri")
265                    .row(id)
266                    .source(e)
267            })?;
268
269        let jwks = match (value.jwks, value.jwks_uri) {
270            (None, None) => None,
271            (Some(jwks), None) => {
272                let jwks = serde_json::from_value(jwks).map_err(|e| {
273                    DatabaseInconsistencyError::on("oauth2_clients")
274                        .column("jwks")
275                        .row(id)
276                        .source(e)
277                })?;
278                Some(JwksOrJwksUri::Jwks(jwks))
279            }
280            (None, Some(jwks_uri)) => {
281                let jwks_uri = jwks_uri.parse().map_err(|e| {
282                    DatabaseInconsistencyError::on("oauth2_clients")
283                        .column("jwks_uri")
284                        .row(id)
285                        .source(e)
286                })?;
287
288                Some(JwksOrJwksUri::JwksUri(jwks_uri))
289            }
290            _ => {
291                return Err(DatabaseInconsistencyError::on("oauth2_clients")
292                    .column("jwks(_uri)")
293                    .row(id));
294            }
295        };
296
297        Ok(Client {
298            id,
299            client_id: id.to_string(),
300            metadata_digest: value.metadata_digest,
301            encrypted_client_secret: value.encrypted_client_secret,
302            application_type,
303            redirect_uris,
304            grant_types,
305            client_name: value.client_name,
306            logo_uri,
307            client_uri,
308            policy_uri,
309            tos_uri,
310            jwks,
311            id_token_signed_response_alg,
312            userinfo_signed_response_alg,
313            token_endpoint_auth_method,
314            token_endpoint_auth_signing_alg,
315            initiate_login_uri,
316            is_static: value.is_static,
317        })
318    }
319}
320
321#[async_trait]
322impl OAuth2ClientRepository for PgOAuth2ClientRepository<'_> {
323    type Error = DatabaseError;
324
325    #[tracing::instrument(
326        name = "db.oauth2_client.lookup",
327        skip_all,
328        fields(
329            db.query.text,
330            oauth2_client.id = %id,
331        ),
332    )]
333    async fn lookup(&mut self, id: Ulid) -> Result<Option<Client>, Self::Error> {
334        let res = sqlx::query_as!(
335            OAuth2ClientLookup,
336            r#"
337                SELECT oauth2_client_id
338                     , metadata_digest
339                     , encrypted_client_secret
340                     , application_type
341                     , redirect_uris
342                     , grant_type_authorization_code
343                     , grant_type_refresh_token
344                     , grant_type_client_credentials
345                     , grant_type_device_code
346                     , client_name
347                     , logo_uri
348                     , client_uri
349                     , policy_uri
350                     , tos_uri
351                     , jwks_uri
352                     , jwks
353                     , id_token_signed_response_alg
354                     , userinfo_signed_response_alg
355                     , token_endpoint_auth_method
356                     , token_endpoint_auth_signing_alg
357                     , initiate_login_uri
358                     , is_static
359                FROM oauth2_clients c
360
361                WHERE oauth2_client_id = $1
362            "#,
363            Uuid::from(id),
364        )
365        .traced()
366        .fetch_optional(&mut *self.conn)
367        .await?;
368
369        let Some(res) = res else { return Ok(None) };
370
371        Ok(Some(res.try_into()?))
372    }
373
374    #[tracing::instrument(
375        name = "db.oauth2_client.load_batch",
376        skip_all,
377        fields(
378            db.query.text,
379        ),
380    )]
381    async fn load_batch(
382        &mut self,
383        ids: BTreeSet<Ulid>,
384    ) -> Result<BTreeMap<Ulid, Client>, Self::Error> {
385        let ids: Vec<Uuid> = ids.into_iter().map(Uuid::from).collect();
386        let res = sqlx::query_as!(
387            OAuth2ClientLookup,
388            r#"
389                SELECT oauth2_client_id
390                     , metadata_digest
391                     , encrypted_client_secret
392                     , application_type
393                     , redirect_uris
394                     , grant_type_authorization_code
395                     , grant_type_refresh_token
396                     , grant_type_client_credentials
397                     , grant_type_device_code
398                     , client_name
399                     , logo_uri
400                     , client_uri
401                     , policy_uri
402                     , tos_uri
403                     , jwks_uri
404                     , jwks
405                     , id_token_signed_response_alg
406                     , userinfo_signed_response_alg
407                     , token_endpoint_auth_method
408                     , token_endpoint_auth_signing_alg
409                     , initiate_login_uri
410                     , is_static
411                FROM oauth2_clients c
412
413                WHERE oauth2_client_id = ANY($1::uuid[])
414            "#,
415            &ids,
416        )
417        .traced()
418        .fetch_all(&mut *self.conn)
419        .await?;
420
421        res.into_iter()
422            .map(|r| {
423                r.try_into()
424                    .map(|c: Client| (c.id, c))
425                    .map_err(DatabaseError::from)
426            })
427            .collect()
428    }
429
430    #[tracing::instrument(
431        name = "db.oauth2_client.add_or_reuse",
432        skip_all,
433        fields(
434            db.query.text,
435            client.id,
436            client.name = client_name
437        ),
438    )]
439    async fn add_or_reuse(
440        &mut self,
441        rng: &mut (dyn Rng + Send),
442        clock: &dyn Clock,
443        redirect_uris: Vec<Url>,
444        metadata_digest: Option<String>,
445        encrypted_client_secret: Option<String>,
446        application_type: Option<ApplicationType>,
447        grant_types: Vec<GrantType>,
448        client_name: Option<String>,
449        logo_uri: Option<Url>,
450        client_uri: Option<Url>,
451        policy_uri: Option<Url>,
452        tos_uri: Option<Url>,
453        jwks_uri: Option<Url>,
454        jwks: Option<PublicJsonWebKeySet>,
455        id_token_signed_response_alg: Option<JsonWebSignatureAlg>,
456        userinfo_signed_response_alg: Option<JsonWebSignatureAlg>,
457        token_endpoint_auth_method: Option<OAuthClientAuthenticationMethod>,
458        token_endpoint_auth_signing_alg: Option<JsonWebSignatureAlg>,
459        initiate_login_uri: Option<Url>,
460    ) -> Result<(Client, bool), Self::Error> {
461        let now = clock.now();
462        let id = Ulid::from_datetime_with_rng(now, rng);
463
464        if jwks.is_some() && jwks_uri.is_some() {
465            return Err(DatabaseError::invalid_operation());
466        }
467
468        let jwks_json = jwks
469            .as_ref()
470            .map(serde_json::to_value)
471            .transpose()
472            .map_err(DatabaseError::to_invalid_operation)?;
473
474        let redirect_uris_array = redirect_uris.iter().map(Url::to_string).collect::<Vec<_>>();
475
476        let res = sqlx::query_as!(
477            OAuth2ClientLookup,
478            r#"
479                INSERT INTO oauth2_clients
480                    ( oauth2_client_id
481                    , metadata_digest
482                    , encrypted_client_secret
483                    , application_type
484                    , redirect_uris
485                    , grant_type_authorization_code
486                    , grant_type_refresh_token
487                    , grant_type_client_credentials
488                    , grant_type_device_code
489                    , client_name
490                    , logo_uri
491                    , client_uri
492                    , policy_uri
493                    , tos_uri
494                    , jwks_uri
495                    , jwks
496                    , id_token_signed_response_alg
497                    , userinfo_signed_response_alg
498                    , token_endpoint_auth_method
499                    , token_endpoint_auth_signing_alg
500                    , initiate_login_uri
501                    , is_static
502                    )
503                VALUES
504                    ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13,
505                    $14, $15, $16, $17, $18, $19, $20, $21, FALSE)
506                -- RETURNING yields nothing for rows skipped by DO NOTHING, so this
507                -- no-op update is what makes it return the existing client, including
508                -- one committed while this insert was waiting on it
509                ON CONFLICT (metadata_digest) DO UPDATE
510                    SET metadata_digest = EXCLUDED.metadata_digest
511                RETURNING oauth2_client_id
512                        , metadata_digest
513                        , encrypted_client_secret
514                        , application_type
515                        , redirect_uris
516                        , grant_type_authorization_code
517                        , grant_type_refresh_token
518                        , grant_type_client_credentials
519                        , grant_type_device_code
520                        , client_name
521                        , logo_uri
522                        , client_uri
523                        , policy_uri
524                        , tos_uri
525                        , jwks_uri
526                        , jwks
527                        , id_token_signed_response_alg
528                        , userinfo_signed_response_alg
529                        , token_endpoint_auth_method
530                        , token_endpoint_auth_signing_alg
531                        , initiate_login_uri
532                        , is_static
533            "#,
534            Uuid::from(id),
535            metadata_digest,
536            encrypted_client_secret,
537            application_type.as_ref().map(ToString::to_string),
538            &redirect_uris_array,
539            grant_types.contains(&GrantType::AuthorizationCode),
540            grant_types.contains(&GrantType::RefreshToken),
541            grant_types.contains(&GrantType::ClientCredentials),
542            grant_types.contains(&GrantType::DeviceCode),
543            client_name,
544            logo_uri.as_ref().map(Url::as_str),
545            client_uri.as_ref().map(Url::as_str),
546            policy_uri.as_ref().map(Url::as_str),
547            tos_uri.as_ref().map(Url::as_str),
548            jwks_uri.as_ref().map(Url::as_str),
549            jwks_json,
550            id_token_signed_response_alg
551                .as_ref()
552                .map(ToString::to_string),
553            userinfo_signed_response_alg
554                .as_ref()
555                .map(ToString::to_string),
556            token_endpoint_auth_method.as_ref().map(ToString::to_string),
557            token_endpoint_auth_signing_alg
558                .as_ref()
559                .map(ToString::to_string),
560            initiate_login_uri.as_ref().map(Url::as_str),
561        )
562        .traced()
563        .fetch_one(&mut *self.conn)
564        .await?;
565
566        let client: Client = res.try_into()?;
567        tracing::Span::current().record("client.id", tracing::field::display(client.id));
568        let inserted = client.id == id;
569
570        Ok((client, inserted))
571    }
572
573    #[tracing::instrument(
574        name = "db.oauth2_client.upsert_static",
575        skip_all,
576        fields(
577            db.query.text,
578            client.id = %client_id,
579        ),
580    )]
581    async fn upsert_static(
582        &mut self,
583        client_id: Ulid,
584        client_name: Option<String>,
585        client_uri: Option<Url>,
586        client_auth_method: OAuthClientAuthenticationMethod,
587        encrypted_client_secret: Option<String>,
588        jwks: Option<PublicJsonWebKeySet>,
589        jwks_uri: Option<Url>,
590        redirect_uris: Vec<Url>,
591    ) -> Result<Client, Self::Error> {
592        let jwks_json = jwks
593            .as_ref()
594            .map(serde_json::to_value)
595            .transpose()
596            .map_err(DatabaseError::to_invalid_operation)?;
597
598        let client_auth_method = client_auth_method.to_string();
599        let redirect_uris_array = redirect_uris.iter().map(Url::to_string).collect::<Vec<_>>();
600
601        sqlx::query!(
602            r#"
603                INSERT INTO oauth2_clients
604                    ( oauth2_client_id
605                    , encrypted_client_secret
606                    , redirect_uris
607                    , grant_type_authorization_code
608                    , grant_type_refresh_token
609                    , grant_type_client_credentials
610                    , grant_type_device_code
611                    , token_endpoint_auth_method
612                    , jwks
613                    , client_name
614                    , client_uri
615                    , jwks_uri
616                    , is_static
617                    )
618                VALUES
619                    ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, TRUE)
620                ON CONFLICT (oauth2_client_id)
621                DO
622                    UPDATE SET encrypted_client_secret = EXCLUDED.encrypted_client_secret
623                             , redirect_uris = EXCLUDED.redirect_uris
624                             , grant_type_authorization_code = EXCLUDED.grant_type_authorization_code
625                             , grant_type_refresh_token = EXCLUDED.grant_type_refresh_token
626                             , grant_type_client_credentials = EXCLUDED.grant_type_client_credentials
627                             , grant_type_device_code = EXCLUDED.grant_type_device_code
628                             , token_endpoint_auth_method = EXCLUDED.token_endpoint_auth_method
629                             , jwks = EXCLUDED.jwks
630                             , client_name = EXCLUDED.client_name
631                             , client_uri = EXCLUDED.client_uri
632                             , jwks_uri = EXCLUDED.jwks_uri
633                             , is_static = TRUE
634            "#,
635            Uuid::from(client_id),
636            encrypted_client_secret,
637            &redirect_uris_array,
638            true,
639            true,
640            true,
641            true,
642            client_auth_method,
643            jwks_json,
644            client_name,
645            client_uri.as_ref().map(Url::as_str),
646            jwks_uri.as_ref().map(Url::as_str),
647        )
648        .traced()
649        .execute(&mut *self.conn)
650        .await?;
651
652        let jwks = match (jwks, jwks_uri) {
653            (None, None) => None,
654            (Some(jwks), None) => Some(JwksOrJwksUri::Jwks(jwks)),
655            (None, Some(jwks_uri)) => Some(JwksOrJwksUri::JwksUri(jwks_uri)),
656            _ => return Err(DatabaseError::invalid_operation()),
657        };
658
659        Ok(Client {
660            id: client_id,
661            client_id: client_id.to_string(),
662            metadata_digest: None,
663            encrypted_client_secret,
664            application_type: None,
665            redirect_uris,
666            grant_types: vec![
667                GrantType::AuthorizationCode,
668                GrantType::RefreshToken,
669                GrantType::ClientCredentials,
670            ],
671            client_name,
672            logo_uri: None,
673            client_uri: None,
674            policy_uri: None,
675            tos_uri: None,
676            jwks,
677            id_token_signed_response_alg: None,
678            userinfo_signed_response_alg: None,
679            token_endpoint_auth_method: None,
680            token_endpoint_auth_signing_alg: None,
681            initiate_login_uri: None,
682            is_static: true,
683        })
684    }
685
686    #[tracing::instrument(
687        name = "db.oauth2_client.all_static",
688        skip_all,
689        fields(
690            db.query.text,
691        ),
692    )]
693    async fn all_static(&mut self) -> Result<Vec<Client>, Self::Error> {
694        let res = sqlx::query_as!(
695            OAuth2ClientLookup,
696            r#"
697                SELECT oauth2_client_id
698                     , metadata_digest
699                     , encrypted_client_secret
700                     , application_type
701                     , redirect_uris
702                     , grant_type_authorization_code
703                     , grant_type_refresh_token
704                     , grant_type_client_credentials
705                     , grant_type_device_code
706                     , client_name
707                     , logo_uri
708                     , client_uri
709                     , policy_uri
710                     , tos_uri
711                     , jwks_uri
712                     , jwks
713                     , id_token_signed_response_alg
714                     , userinfo_signed_response_alg
715                     , token_endpoint_auth_method
716                     , token_endpoint_auth_signing_alg
717                     , initiate_login_uri
718                     , is_static
719                FROM oauth2_clients c
720                WHERE is_static = TRUE
721            "#,
722        )
723        .traced()
724        .fetch_all(&mut *self.conn)
725        .await?;
726
727        res.into_iter()
728            .map(|r| r.try_into().map_err(DatabaseError::from))
729            .collect()
730    }
731
732    #[tracing::instrument(
733        name = "db.oauth2_client.delete_by_id",
734        skip_all,
735        fields(
736            db.query.text,
737            client.id = %id,
738        ),
739    )]
740    async fn delete_by_id(&mut self, id: Ulid) -> Result<(), Self::Error> {
741        // Delete the authorization grants
742        {
743            let span = info_span!(
744                "db.oauth2_client.delete_by_id.authorization_grants",
745                { DB_QUERY_TEXT } = tracing::field::Empty,
746            );
747
748            sqlx::query!(
749                r#"
750                    DELETE FROM oauth2_authorization_grants
751                    WHERE oauth2_client_id = $1
752                "#,
753                Uuid::from(id),
754            )
755            .record(&span)
756            .execute(&mut *self.conn)
757            .instrument(span)
758            .await?;
759        }
760
761        // Delete the OAuth 2 sessions related data
762        {
763            let span = info_span!(
764                "db.oauth2_client.delete_by_id.access_tokens",
765                { DB_QUERY_TEXT } = tracing::field::Empty,
766            );
767
768            sqlx::query!(
769                r#"
770                    DELETE FROM oauth2_access_tokens
771                    WHERE oauth2_session_id IN (
772                        SELECT oauth2_session_id
773                        FROM oauth2_sessions
774                        WHERE oauth2_client_id = $1
775                    )
776                "#,
777                Uuid::from(id),
778            )
779            .record(&span)
780            .execute(&mut *self.conn)
781            .instrument(span)
782            .await?;
783        }
784
785        {
786            let span = info_span!(
787                "db.oauth2_client.delete_by_id.refresh_tokens",
788                { DB_QUERY_TEXT } = tracing::field::Empty,
789            );
790
791            sqlx::query!(
792                r#"
793                    DELETE FROM oauth2_refresh_tokens
794                    WHERE oauth2_session_id IN (
795                        SELECT oauth2_session_id
796                        FROM oauth2_sessions
797                        WHERE oauth2_client_id = $1
798                    )
799                "#,
800                Uuid::from(id),
801            )
802            .record(&span)
803            .execute(&mut *self.conn)
804            .instrument(span)
805            .await?;
806        }
807
808        {
809            let span = info_span!(
810                "db.oauth2_client.delete_by_id.sessions",
811                { DB_QUERY_TEXT } = tracing::field::Empty,
812            );
813
814            sqlx::query!(
815                r#"
816                    DELETE FROM oauth2_sessions
817                    WHERE oauth2_client_id = $1
818                "#,
819                Uuid::from(id),
820            )
821            .record(&span)
822            .execute(&mut *self.conn)
823            .instrument(span)
824            .await?;
825        }
826
827        // Delete any personal access tokens & sessions owned
828        // by the client
829        {
830            let span = info_span!(
831                "db.oauth2_client.delete_by_id.personal_access_tokens",
832                { DB_QUERY_TEXT } = tracing::field::Empty,
833            );
834
835            sqlx::query!(
836                r#"
837                    DELETE FROM personal_access_tokens
838                    WHERE personal_session_id IN (
839                        SELECT personal_session_id
840                        FROM personal_sessions
841                        WHERE owner_oauth2_client_id = $1
842                    )
843                "#,
844                Uuid::from(id),
845            )
846            .record(&span)
847            .execute(&mut *self.conn)
848            .instrument(span)
849            .await?;
850        }
851        {
852            let span = info_span!(
853                "db.oauth2_client.delete_by_id.personal_sessions",
854                { DB_QUERY_TEXT } = tracing::field::Empty,
855            );
856
857            sqlx::query!(
858                r#"
859                    DELETE FROM personal_sessions
860                    WHERE owner_oauth2_client_id = $1
861                "#,
862                Uuid::from(id),
863            )
864            .record(&span)
865            .execute(&mut *self.conn)
866            .instrument(span)
867            .await?;
868        }
869
870        // Now delete the client itself
871        let res = sqlx::query!(
872            r#"
873                DELETE FROM oauth2_clients
874                WHERE oauth2_client_id = $1
875            "#,
876            Uuid::from(id),
877        )
878        .traced()
879        .execute(&mut *self.conn)
880        .await?;
881
882        DatabaseError::ensure_affected_rows(&res, 1)
883    }
884
885    #[tracing::instrument(
886        name = "db.oauth2_client.list",
887        skip_all,
888        fields(
889            db.query.text,
890        ),
891    )]
892    async fn list(
893        &mut self,
894        filter: OAuth2ClientFilter<'_>,
895        pagination: Pagination,
896    ) -> Result<Page<Client>, Self::Error> {
897        let (sql, arguments) = Query::select()
898            .expr_as(
899                Expr::col((OAuth2Clients::Table, OAuth2Clients::OAuth2ClientId)),
900                OAuth2ClientLookupIden::Oauth2ClientId,
901            )
902            .expr_as(
903                Expr::cust("metadata_digest"),
904                OAuth2ClientLookupIden::MetadataDigest,
905            )
906            .expr_as(
907                Expr::cust("encrypted_client_secret"),
908                OAuth2ClientLookupIden::EncryptedClientSecret,
909            )
910            .expr_as(
911                Expr::cust("application_type"),
912                OAuth2ClientLookupIden::ApplicationType,
913            )
914            .expr_as(
915                Expr::col((OAuth2Clients::Table, OAuth2Clients::RedirectUris)),
916                OAuth2ClientLookupIden::RedirectUris,
917            )
918            .expr_as(
919                Expr::cust("grant_type_authorization_code"),
920                OAuth2ClientLookupIden::GrantTypeAuthorizationCode,
921            )
922            .expr_as(
923                Expr::cust("grant_type_refresh_token"),
924                OAuth2ClientLookupIden::GrantTypeRefreshToken,
925            )
926            .expr_as(
927                Expr::cust("grant_type_client_credentials"),
928                OAuth2ClientLookupIden::GrantTypeClientCredentials,
929            )
930            .expr_as(
931                Expr::cust("grant_type_device_code"),
932                OAuth2ClientLookupIden::GrantTypeDeviceCode,
933            )
934            .expr_as(
935                Expr::col((OAuth2Clients::Table, OAuth2Clients::ClientName)),
936                OAuth2ClientLookupIden::ClientName,
937            )
938            .expr_as(
939                Expr::col((OAuth2Clients::Table, OAuth2Clients::LogoUri)),
940                OAuth2ClientLookupIden::LogoUri,
941            )
942            .expr_as(
943                Expr::col((OAuth2Clients::Table, OAuth2Clients::ClientUri)),
944                OAuth2ClientLookupIden::ClientUri,
945            )
946            .expr_as(Expr::cust("policy_uri"), OAuth2ClientLookupIden::PolicyUri)
947            .expr_as(Expr::cust("tos_uri"), OAuth2ClientLookupIden::TosUri)
948            .expr_as(Expr::cust("jwks_uri"), OAuth2ClientLookupIden::JwksUri)
949            .expr_as(Expr::cust("jwks"), OAuth2ClientLookupIden::Jwks)
950            .expr_as(
951                Expr::cust("id_token_signed_response_alg"),
952                OAuth2ClientLookupIden::IdTokenSignedResponseAlg,
953            )
954            .expr_as(
955                Expr::cust("userinfo_signed_response_alg"),
956                OAuth2ClientLookupIden::UserinfoSignedResponseAlg,
957            )
958            .expr_as(
959                Expr::cust("token_endpoint_auth_method"),
960                OAuth2ClientLookupIden::TokenEndpointAuthMethod,
961            )
962            .expr_as(
963                Expr::cust("token_endpoint_auth_signing_alg"),
964                OAuth2ClientLookupIden::TokenEndpointAuthSigningAlg,
965            )
966            .expr_as(
967                Expr::cust("initiate_login_uri"),
968                OAuth2ClientLookupIden::InitiateLoginUri,
969            )
970            .expr_as(
971                Expr::col((OAuth2Clients::Table, OAuth2Clients::IsStatic)),
972                OAuth2ClientLookupIden::IsStatic,
973            )
974            .from(OAuth2Clients::Table)
975            .apply_filter(filter)
976            .generate_pagination(
977                (OAuth2Clients::Table, OAuth2Clients::OAuth2ClientId),
978                pagination,
979            )
980            .build_sqlx(PostgresQueryBuilder);
981
982        let edges: Vec<OAuth2ClientLookup> = sqlx::query_as_with(&sql, arguments)
983            .traced()
984            .fetch_all(&mut *self.conn)
985            .await?;
986
987        let page = pagination.process(edges).try_map(Client::try_from)?;
988
989        Ok(page)
990    }
991
992    #[tracing::instrument(
993        name = "db.oauth2_client.count",
994        skip_all,
995        fields(
996            db.query.text,
997        ),
998    )]
999    async fn count(&mut self, filter: OAuth2ClientFilter<'_>) -> Result<usize, Self::Error> {
1000        let (sql, arguments) = Query::select()
1001            .expr(Expr::col((OAuth2Clients::Table, OAuth2Clients::OAuth2ClientId)).count())
1002            .from(OAuth2Clients::Table)
1003            .apply_filter(filter)
1004            .build_sqlx(PostgresQueryBuilder);
1005
1006        let count: i64 = sqlx::query_scalar_with(&sql, arguments)
1007            .traced()
1008            .fetch_one(&mut *self.conn)
1009            .await?;
1010
1011        count
1012            .try_into()
1013            .map_err(DatabaseError::to_invalid_operation)
1014    }
1015}