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 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 {
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 {
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 {
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 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}