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        err,
333    )]
334    async fn lookup(&mut self, id: Ulid) -> Result<Option<Client>, Self::Error> {
335        let res = sqlx::query_as!(
336            OAuth2ClientLookup,
337            r#"
338                SELECT oauth2_client_id
339                     , metadata_digest
340                     , encrypted_client_secret
341                     , application_type
342                     , redirect_uris
343                     , grant_type_authorization_code
344                     , grant_type_refresh_token
345                     , grant_type_client_credentials
346                     , grant_type_device_code
347                     , client_name
348                     , logo_uri
349                     , client_uri
350                     , policy_uri
351                     , tos_uri
352                     , jwks_uri
353                     , jwks
354                     , id_token_signed_response_alg
355                     , userinfo_signed_response_alg
356                     , token_endpoint_auth_method
357                     , token_endpoint_auth_signing_alg
358                     , initiate_login_uri
359                     , is_static
360                FROM oauth2_clients c
361
362                WHERE oauth2_client_id = $1
363            "#,
364            Uuid::from(id),
365        )
366        .traced()
367        .fetch_optional(&mut *self.conn)
368        .await?;
369
370        let Some(res) = res else { return Ok(None) };
371
372        Ok(Some(res.try_into()?))
373    }
374
375    #[tracing::instrument(
376        name = "db.oauth2_client.load_batch",
377        skip_all,
378        fields(
379            db.query.text,
380        ),
381        err,
382    )]
383    async fn load_batch(
384        &mut self,
385        ids: BTreeSet<Ulid>,
386    ) -> Result<BTreeMap<Ulid, Client>, Self::Error> {
387        let ids: Vec<Uuid> = ids.into_iter().map(Uuid::from).collect();
388        let res = sqlx::query_as!(
389            OAuth2ClientLookup,
390            r#"
391                SELECT oauth2_client_id
392                     , metadata_digest
393                     , encrypted_client_secret
394                     , application_type
395                     , redirect_uris
396                     , grant_type_authorization_code
397                     , grant_type_refresh_token
398                     , grant_type_client_credentials
399                     , grant_type_device_code
400                     , client_name
401                     , logo_uri
402                     , client_uri
403                     , policy_uri
404                     , tos_uri
405                     , jwks_uri
406                     , jwks
407                     , id_token_signed_response_alg
408                     , userinfo_signed_response_alg
409                     , token_endpoint_auth_method
410                     , token_endpoint_auth_signing_alg
411                     , initiate_login_uri
412                     , is_static
413                FROM oauth2_clients c
414
415                WHERE oauth2_client_id = ANY($1::uuid[])
416            "#,
417            &ids,
418        )
419        .traced()
420        .fetch_all(&mut *self.conn)
421        .await?;
422
423        res.into_iter()
424            .map(|r| {
425                r.try_into()
426                    .map(|c: Client| (c.id, c))
427                    .map_err(DatabaseError::from)
428            })
429            .collect()
430    }
431
432    #[tracing::instrument(
433        name = "db.oauth2_client.add_or_reuse",
434        skip_all,
435        fields(
436            db.query.text,
437            client.id,
438            client.name = client_name
439        ),
440        err,
441    )]
442    async fn add_or_reuse(
443        &mut self,
444        rng: &mut (dyn Rng + Send),
445        clock: &dyn Clock,
446        redirect_uris: Vec<Url>,
447        metadata_digest: Option<String>,
448        encrypted_client_secret: Option<String>,
449        application_type: Option<ApplicationType>,
450        grant_types: Vec<GrantType>,
451        client_name: Option<String>,
452        logo_uri: Option<Url>,
453        client_uri: Option<Url>,
454        policy_uri: Option<Url>,
455        tos_uri: Option<Url>,
456        jwks_uri: Option<Url>,
457        jwks: Option<PublicJsonWebKeySet>,
458        id_token_signed_response_alg: Option<JsonWebSignatureAlg>,
459        userinfo_signed_response_alg: Option<JsonWebSignatureAlg>,
460        token_endpoint_auth_method: Option<OAuthClientAuthenticationMethod>,
461        token_endpoint_auth_signing_alg: Option<JsonWebSignatureAlg>,
462        initiate_login_uri: Option<Url>,
463    ) -> Result<(Client, bool), Self::Error> {
464        let now = clock.now();
465        let id = Ulid::from_datetime_with_rng(now, rng);
466
467        if jwks.is_some() && jwks_uri.is_some() {
468            return Err(DatabaseError::invalid_operation());
469        }
470
471        let jwks_json = jwks
472            .as_ref()
473            .map(serde_json::to_value)
474            .transpose()
475            .map_err(DatabaseError::to_invalid_operation)?;
476
477        let redirect_uris_array = redirect_uris.iter().map(Url::to_string).collect::<Vec<_>>();
478
479        let res = sqlx::query_as!(
480            OAuth2ClientLookup,
481            r#"
482                INSERT INTO oauth2_clients
483                    ( oauth2_client_id
484                    , metadata_digest
485                    , encrypted_client_secret
486                    , application_type
487                    , redirect_uris
488                    , grant_type_authorization_code
489                    , grant_type_refresh_token
490                    , grant_type_client_credentials
491                    , grant_type_device_code
492                    , client_name
493                    , logo_uri
494                    , client_uri
495                    , policy_uri
496                    , tos_uri
497                    , jwks_uri
498                    , jwks
499                    , id_token_signed_response_alg
500                    , userinfo_signed_response_alg
501                    , token_endpoint_auth_method
502                    , token_endpoint_auth_signing_alg
503                    , initiate_login_uri
504                    , is_static
505                    )
506                VALUES
507                    ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13,
508                    $14, $15, $16, $17, $18, $19, $20, $21, FALSE)
509                -- RETURNING yields nothing for rows skipped by DO NOTHING, so this
510                -- no-op update is what makes it return the existing client, including
511                -- one committed while this insert was waiting on it
512                ON CONFLICT (metadata_digest) DO UPDATE
513                    SET metadata_digest = EXCLUDED.metadata_digest
514                RETURNING oauth2_client_id
515                        , metadata_digest
516                        , encrypted_client_secret
517                        , application_type
518                        , redirect_uris
519                        , grant_type_authorization_code
520                        , grant_type_refresh_token
521                        , grant_type_client_credentials
522                        , grant_type_device_code
523                        , client_name
524                        , logo_uri
525                        , client_uri
526                        , policy_uri
527                        , tos_uri
528                        , jwks_uri
529                        , jwks
530                        , id_token_signed_response_alg
531                        , userinfo_signed_response_alg
532                        , token_endpoint_auth_method
533                        , token_endpoint_auth_signing_alg
534                        , initiate_login_uri
535                        , is_static
536            "#,
537            Uuid::from(id),
538            metadata_digest,
539            encrypted_client_secret,
540            application_type.as_ref().map(ToString::to_string),
541            &redirect_uris_array,
542            grant_types.contains(&GrantType::AuthorizationCode),
543            grant_types.contains(&GrantType::RefreshToken),
544            grant_types.contains(&GrantType::ClientCredentials),
545            grant_types.contains(&GrantType::DeviceCode),
546            client_name,
547            logo_uri.as_ref().map(Url::as_str),
548            client_uri.as_ref().map(Url::as_str),
549            policy_uri.as_ref().map(Url::as_str),
550            tos_uri.as_ref().map(Url::as_str),
551            jwks_uri.as_ref().map(Url::as_str),
552            jwks_json,
553            id_token_signed_response_alg
554                .as_ref()
555                .map(ToString::to_string),
556            userinfo_signed_response_alg
557                .as_ref()
558                .map(ToString::to_string),
559            token_endpoint_auth_method.as_ref().map(ToString::to_string),
560            token_endpoint_auth_signing_alg
561                .as_ref()
562                .map(ToString::to_string),
563            initiate_login_uri.as_ref().map(Url::as_str),
564        )
565        .traced()
566        .fetch_one(&mut *self.conn)
567        .await?;
568
569        let client: Client = res.try_into()?;
570        tracing::Span::current().record("client.id", tracing::field::display(client.id));
571        let inserted = client.id == id;
572
573        Ok((client, inserted))
574    }
575
576    #[tracing::instrument(
577        name = "db.oauth2_client.upsert_static",
578        skip_all,
579        fields(
580            db.query.text,
581            client.id = %client_id,
582        ),
583        err,
584    )]
585    async fn upsert_static(
586        &mut self,
587        client_id: Ulid,
588        client_name: Option<String>,
589        client_uri: Option<Url>,
590        client_auth_method: OAuthClientAuthenticationMethod,
591        encrypted_client_secret: Option<String>,
592        jwks: Option<PublicJsonWebKeySet>,
593        jwks_uri: Option<Url>,
594        redirect_uris: Vec<Url>,
595    ) -> Result<Client, Self::Error> {
596        let jwks_json = jwks
597            .as_ref()
598            .map(serde_json::to_value)
599            .transpose()
600            .map_err(DatabaseError::to_invalid_operation)?;
601
602        let client_auth_method = client_auth_method.to_string();
603        let redirect_uris_array = redirect_uris.iter().map(Url::to_string).collect::<Vec<_>>();
604
605        sqlx::query!(
606            r#"
607                INSERT INTO oauth2_clients
608                    ( oauth2_client_id
609                    , encrypted_client_secret
610                    , redirect_uris
611                    , grant_type_authorization_code
612                    , grant_type_refresh_token
613                    , grant_type_client_credentials
614                    , grant_type_device_code
615                    , token_endpoint_auth_method
616                    , jwks
617                    , client_name
618                    , client_uri
619                    , jwks_uri
620                    , is_static
621                    )
622                VALUES
623                    ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, TRUE)
624                ON CONFLICT (oauth2_client_id)
625                DO
626                    UPDATE SET encrypted_client_secret = EXCLUDED.encrypted_client_secret
627                             , redirect_uris = EXCLUDED.redirect_uris
628                             , grant_type_authorization_code = EXCLUDED.grant_type_authorization_code
629                             , grant_type_refresh_token = EXCLUDED.grant_type_refresh_token
630                             , grant_type_client_credentials = EXCLUDED.grant_type_client_credentials
631                             , grant_type_device_code = EXCLUDED.grant_type_device_code
632                             , token_endpoint_auth_method = EXCLUDED.token_endpoint_auth_method
633                             , jwks = EXCLUDED.jwks
634                             , client_name = EXCLUDED.client_name
635                             , client_uri = EXCLUDED.client_uri
636                             , jwks_uri = EXCLUDED.jwks_uri
637                             , is_static = TRUE
638            "#,
639            Uuid::from(client_id),
640            encrypted_client_secret,
641            &redirect_uris_array,
642            true,
643            true,
644            true,
645            true,
646            client_auth_method,
647            jwks_json,
648            client_name,
649            client_uri.as_ref().map(Url::as_str),
650            jwks_uri.as_ref().map(Url::as_str),
651        )
652        .traced()
653        .execute(&mut *self.conn)
654        .await?;
655
656        let jwks = match (jwks, jwks_uri) {
657            (None, None) => None,
658            (Some(jwks), None) => Some(JwksOrJwksUri::Jwks(jwks)),
659            (None, Some(jwks_uri)) => Some(JwksOrJwksUri::JwksUri(jwks_uri)),
660            _ => return Err(DatabaseError::invalid_operation()),
661        };
662
663        Ok(Client {
664            id: client_id,
665            client_id: client_id.to_string(),
666            metadata_digest: None,
667            encrypted_client_secret,
668            application_type: None,
669            redirect_uris,
670            grant_types: vec![
671                GrantType::AuthorizationCode,
672                GrantType::RefreshToken,
673                GrantType::ClientCredentials,
674            ],
675            client_name,
676            logo_uri: None,
677            client_uri: None,
678            policy_uri: None,
679            tos_uri: None,
680            jwks,
681            id_token_signed_response_alg: None,
682            userinfo_signed_response_alg: None,
683            token_endpoint_auth_method: None,
684            token_endpoint_auth_signing_alg: None,
685            initiate_login_uri: None,
686            is_static: true,
687        })
688    }
689
690    #[tracing::instrument(
691        name = "db.oauth2_client.all_static",
692        skip_all,
693        fields(
694            db.query.text,
695        ),
696        err,
697    )]
698    async fn all_static(&mut self) -> Result<Vec<Client>, Self::Error> {
699        let res = sqlx::query_as!(
700            OAuth2ClientLookup,
701            r#"
702                SELECT oauth2_client_id
703                     , metadata_digest
704                     , encrypted_client_secret
705                     , application_type
706                     , redirect_uris
707                     , grant_type_authorization_code
708                     , grant_type_refresh_token
709                     , grant_type_client_credentials
710                     , grant_type_device_code
711                     , client_name
712                     , logo_uri
713                     , client_uri
714                     , policy_uri
715                     , tos_uri
716                     , jwks_uri
717                     , jwks
718                     , id_token_signed_response_alg
719                     , userinfo_signed_response_alg
720                     , token_endpoint_auth_method
721                     , token_endpoint_auth_signing_alg
722                     , initiate_login_uri
723                     , is_static
724                FROM oauth2_clients c
725                WHERE is_static = TRUE
726            "#,
727        )
728        .traced()
729        .fetch_all(&mut *self.conn)
730        .await?;
731
732        res.into_iter()
733            .map(|r| r.try_into().map_err(DatabaseError::from))
734            .collect()
735    }
736
737    #[tracing::instrument(
738        name = "db.oauth2_client.delete_by_id",
739        skip_all,
740        fields(
741            db.query.text,
742            client.id = %id,
743        ),
744        err,
745    )]
746    async fn delete_by_id(&mut self, id: Ulid) -> Result<(), Self::Error> {
747        // Delete the authorization grants
748        {
749            let span = info_span!(
750                "db.oauth2_client.delete_by_id.authorization_grants",
751                { DB_QUERY_TEXT } = tracing::field::Empty,
752            );
753
754            sqlx::query!(
755                r#"
756                    DELETE FROM oauth2_authorization_grants
757                    WHERE oauth2_client_id = $1
758                "#,
759                Uuid::from(id),
760            )
761            .record(&span)
762            .execute(&mut *self.conn)
763            .instrument(span)
764            .await?;
765        }
766
767        // Delete the OAuth 2 sessions related data
768        {
769            let span = info_span!(
770                "db.oauth2_client.delete_by_id.access_tokens",
771                { DB_QUERY_TEXT } = tracing::field::Empty,
772            );
773
774            sqlx::query!(
775                r#"
776                    DELETE FROM oauth2_access_tokens
777                    WHERE oauth2_session_id IN (
778                        SELECT oauth2_session_id
779                        FROM oauth2_sessions
780                        WHERE oauth2_client_id = $1
781                    )
782                "#,
783                Uuid::from(id),
784            )
785            .record(&span)
786            .execute(&mut *self.conn)
787            .instrument(span)
788            .await?;
789        }
790
791        {
792            let span = info_span!(
793                "db.oauth2_client.delete_by_id.refresh_tokens",
794                { DB_QUERY_TEXT } = tracing::field::Empty,
795            );
796
797            sqlx::query!(
798                r#"
799                    DELETE FROM oauth2_refresh_tokens
800                    WHERE oauth2_session_id IN (
801                        SELECT oauth2_session_id
802                        FROM oauth2_sessions
803                        WHERE oauth2_client_id = $1
804                    )
805                "#,
806                Uuid::from(id),
807            )
808            .record(&span)
809            .execute(&mut *self.conn)
810            .instrument(span)
811            .await?;
812        }
813
814        {
815            let span = info_span!(
816                "db.oauth2_client.delete_by_id.sessions",
817                { DB_QUERY_TEXT } = tracing::field::Empty,
818            );
819
820            sqlx::query!(
821                r#"
822                    DELETE FROM oauth2_sessions
823                    WHERE oauth2_client_id = $1
824                "#,
825                Uuid::from(id),
826            )
827            .record(&span)
828            .execute(&mut *self.conn)
829            .instrument(span)
830            .await?;
831        }
832
833        // Delete any personal access tokens & sessions owned
834        // by the client
835        {
836            let span = info_span!(
837                "db.oauth2_client.delete_by_id.personal_access_tokens",
838                { DB_QUERY_TEXT } = tracing::field::Empty,
839            );
840
841            sqlx::query!(
842                r#"
843                    DELETE FROM personal_access_tokens
844                    WHERE personal_session_id IN (
845                        SELECT personal_session_id
846                        FROM personal_sessions
847                        WHERE owner_oauth2_client_id = $1
848                    )
849                "#,
850                Uuid::from(id),
851            )
852            .record(&span)
853            .execute(&mut *self.conn)
854            .instrument(span)
855            .await?;
856        }
857        {
858            let span = info_span!(
859                "db.oauth2_client.delete_by_id.personal_sessions",
860                { DB_QUERY_TEXT } = tracing::field::Empty,
861            );
862
863            sqlx::query!(
864                r#"
865                    DELETE FROM personal_sessions
866                    WHERE owner_oauth2_client_id = $1
867                "#,
868                Uuid::from(id),
869            )
870            .record(&span)
871            .execute(&mut *self.conn)
872            .instrument(span)
873            .await?;
874        }
875
876        // Now delete the client itself
877        let res = sqlx::query!(
878            r#"
879                DELETE FROM oauth2_clients
880                WHERE oauth2_client_id = $1
881            "#,
882            Uuid::from(id),
883        )
884        .traced()
885        .execute(&mut *self.conn)
886        .await?;
887
888        DatabaseError::ensure_affected_rows(&res, 1)
889    }
890
891    #[tracing::instrument(
892        name = "db.oauth2_client.list",
893        skip_all,
894        fields(
895            db.query.text,
896        ),
897        err,
898    )]
899    async fn list(
900        &mut self,
901        filter: OAuth2ClientFilter<'_>,
902        pagination: Pagination,
903    ) -> Result<Page<Client>, Self::Error> {
904        let (sql, arguments) = Query::select()
905            .expr_as(
906                Expr::col((OAuth2Clients::Table, OAuth2Clients::OAuth2ClientId)),
907                OAuth2ClientLookupIden::Oauth2ClientId,
908            )
909            .expr_as(
910                Expr::cust("metadata_digest"),
911                OAuth2ClientLookupIden::MetadataDigest,
912            )
913            .expr_as(
914                Expr::cust("encrypted_client_secret"),
915                OAuth2ClientLookupIden::EncryptedClientSecret,
916            )
917            .expr_as(
918                Expr::cust("application_type"),
919                OAuth2ClientLookupIden::ApplicationType,
920            )
921            .expr_as(
922                Expr::col((OAuth2Clients::Table, OAuth2Clients::RedirectUris)),
923                OAuth2ClientLookupIden::RedirectUris,
924            )
925            .expr_as(
926                Expr::cust("grant_type_authorization_code"),
927                OAuth2ClientLookupIden::GrantTypeAuthorizationCode,
928            )
929            .expr_as(
930                Expr::cust("grant_type_refresh_token"),
931                OAuth2ClientLookupIden::GrantTypeRefreshToken,
932            )
933            .expr_as(
934                Expr::cust("grant_type_client_credentials"),
935                OAuth2ClientLookupIden::GrantTypeClientCredentials,
936            )
937            .expr_as(
938                Expr::cust("grant_type_device_code"),
939                OAuth2ClientLookupIden::GrantTypeDeviceCode,
940            )
941            .expr_as(
942                Expr::col((OAuth2Clients::Table, OAuth2Clients::ClientName)),
943                OAuth2ClientLookupIden::ClientName,
944            )
945            .expr_as(
946                Expr::col((OAuth2Clients::Table, OAuth2Clients::LogoUri)),
947                OAuth2ClientLookupIden::LogoUri,
948            )
949            .expr_as(
950                Expr::col((OAuth2Clients::Table, OAuth2Clients::ClientUri)),
951                OAuth2ClientLookupIden::ClientUri,
952            )
953            .expr_as(Expr::cust("policy_uri"), OAuth2ClientLookupIden::PolicyUri)
954            .expr_as(Expr::cust("tos_uri"), OAuth2ClientLookupIden::TosUri)
955            .expr_as(Expr::cust("jwks_uri"), OAuth2ClientLookupIden::JwksUri)
956            .expr_as(Expr::cust("jwks"), OAuth2ClientLookupIden::Jwks)
957            .expr_as(
958                Expr::cust("id_token_signed_response_alg"),
959                OAuth2ClientLookupIden::IdTokenSignedResponseAlg,
960            )
961            .expr_as(
962                Expr::cust("userinfo_signed_response_alg"),
963                OAuth2ClientLookupIden::UserinfoSignedResponseAlg,
964            )
965            .expr_as(
966                Expr::cust("token_endpoint_auth_method"),
967                OAuth2ClientLookupIden::TokenEndpointAuthMethod,
968            )
969            .expr_as(
970                Expr::cust("token_endpoint_auth_signing_alg"),
971                OAuth2ClientLookupIden::TokenEndpointAuthSigningAlg,
972            )
973            .expr_as(
974                Expr::cust("initiate_login_uri"),
975                OAuth2ClientLookupIden::InitiateLoginUri,
976            )
977            .expr_as(
978                Expr::col((OAuth2Clients::Table, OAuth2Clients::IsStatic)),
979                OAuth2ClientLookupIden::IsStatic,
980            )
981            .from(OAuth2Clients::Table)
982            .apply_filter(filter)
983            .generate_pagination(
984                (OAuth2Clients::Table, OAuth2Clients::OAuth2ClientId),
985                pagination,
986            )
987            .build_sqlx(PostgresQueryBuilder);
988
989        let edges: Vec<OAuth2ClientLookup> = sqlx::query_as_with(&sql, arguments)
990            .traced()
991            .fetch_all(&mut *self.conn)
992            .await?;
993
994        let page = pagination.process(edges).try_map(Client::try_from)?;
995
996        Ok(page)
997    }
998
999    #[tracing::instrument(
1000        name = "db.oauth2_client.count",
1001        skip_all,
1002        fields(
1003            db.query.text,
1004        ),
1005        err,
1006    )]
1007    async fn count(&mut self, filter: OAuth2ClientFilter<'_>) -> Result<usize, Self::Error> {
1008        let (sql, arguments) = Query::select()
1009            .expr(Expr::col((OAuth2Clients::Table, OAuth2Clients::OAuth2ClientId)).count())
1010            .from(OAuth2Clients::Table)
1011            .apply_filter(filter)
1012            .build_sqlx(PostgresQueryBuilder);
1013
1014        let count: i64 = sqlx::query_scalar_with(&sql, arguments)
1015            .traced()
1016            .fetch_one(&mut *self.conn)
1017            .await?;
1018
1019        count
1020            .try_into()
1021            .map_err(DatabaseError::to_invalid_operation)
1022    }
1023}