1use 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
44pub struct PgOAuth2ClientRepository<'c> {
46 conn: &'c mut PgConnection,
47}
48
49impl<'c> PgOAuth2ClientRepository<'c> {
50 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 _ => 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 {
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 {
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 {
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 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}