1use crate::{
2 auth_n::{
3 certificates::get_certification_provider,
4 oidc::{
5 code_verification,
6 error::{OIDCError, OIDCErrorCode},
7 extract::{body::OAuthTokenBody, client_app::find_client_app},
8 routes::scope::verify_requested_scope_is_subset,
9 schemas,
10 },
11 },
12 extract::path_tenant::{ProjectIdentifier, TenantIdentifier},
13 services::AppState,
14};
15use axum::{
16 Json,
17 extract::State,
18 response::{IntoResponse, Response},
19};
20use axum_extra::{TypedHeader, extract::Cached, headers::UserAgent, routing::TypedPath};
21use haste_fhir_client::{
22 request::{FHIRSearchTypeRequest, SearchRequest},
23 url::{Parameter, ParsedParameter, ParsedParameters},
24};
25use haste_fhir_model::r4::generated::{
26 resources::{ClientApplication, ResourceType},
27 terminology::ClientapplicationGrantType,
28};
29use haste_fhir_search::SearchEngine;
30use haste_fhir_terminology::FHIRTerminology;
31use haste_jwt::{
32 AuthorId, AuthorKind, ProjectId, TenantId, UserRole, VersionId,
33 claims::{SubscriptionTier, UserTokenClaims},
34 scopes::{OIDCScope, Scope, Scopes},
35};
36use haste_repository::{
37 Repository,
38 admin::{ProjectAuthAdmin, TenantAuthAdmin},
39 types::{
40 SupportedFHIRVersions,
41 authorization_code::{
42 AuthorizationCodeKind, AuthorizationCodeSearchClaims, CreateAuthorizationCode,
43 },
44 scope::{ClientId, CreateScope, ScopeSearchClaims, UserId},
45 tenant::CreateTenant,
46 user::{User, UserRole as RepoUserRole},
47 },
48};
49use jsonwebtoken::{Algorithm, Header};
50use serde::{Deserialize, Serialize};
51use serde_json::json;
52use std::{sync::Arc, time::Duration};
53
54#[derive(TypedPath)]
55#[typed_path("/token")]
56pub struct TokenPath;
57
58#[derive(Serialize, Deserialize, Debug)]
59pub enum TokenType {
60 Bearer,
61}
62
63pub static TOKEN_EXPIRATION: usize = 7200; pub static REFRESH_TOKEN_EXPIRATION: usize = 43200; #[derive(Serialize, Deserialize, Debug)]
67pub struct TokenResponse {
68 pub access_token: String,
69 #[serde(skip_serializing_if = "Option::is_none")]
70 refresh_token: Option<String>,
71 #[serde(skip_serializing_if = "Option::is_none")]
72 pub id_token: Option<String>,
73 token_type: TokenType,
74 expires_in: usize,
75}
76
77struct TokenResponseArguments {
78 user_id: String,
79 user_role: UserRole,
80 user_kind: AuthorKind,
81 client_id: String,
82 scopes: Scopes,
83 tenant: TenantId,
84 project: ProjectId,
85 membership: Option<String>,
86 access_policy_version_ids: Vec<VersionId>,
87}
88
89async fn create_token_response<Repo: Repository>(
90 user_agent: &Option<TypedHeader<UserAgent>>,
91 repo: &Repo,
92 client_app: &ClientApplication,
93 grant_type_used: &schemas::token_body::OAuth2TokenBodyGrantType,
94 args: TokenResponseArguments,
95) -> Result<TokenResponse, OIDCError> {
96 let cert_provider = get_certification_provider();
97 let encoding_key = cert_provider.encoding_key().map_err(|_e| {
98 OIDCError::new(
99 OIDCErrorCode::ServerError,
100 Some("Failed to create access token. No encoding key available.".to_string()),
101 None,
102 )
103 })?;
104
105 let tenant = TenantAuthAdmin::<CreateTenant, _, _, _, _>::read(
106 repo,
107 &TenantId::System,
108 &args.tenant.as_ref().to_string(),
109 )
110 .await
111 .map_err(|_e| {
112 OIDCError::new(
113 OIDCErrorCode::ServerError,
114 Some("Failed to retrieve tenant information.".to_string()),
115 None,
116 )
117 })?
118 .ok_or_else(|| {
119 OIDCError::new(
120 OIDCErrorCode::ServerError,
121 Some("Tenant not found.".to_string()),
122 None,
123 )
124 })?;
125
126 let mut header = Header::new(Algorithm::RS256);
127 header.kid = Some(encoding_key.kid.clone());
128
129 let token = jsonwebtoken::encode(
130 &header,
131 &UserTokenClaims {
132 sub: AuthorId::new(args.user_id.clone()),
133 exp: (chrono::Utc::now() + chrono::Duration::seconds(TOKEN_EXPIRATION as i64))
134 .timestamp() as usize,
135 aud: args.client_id.clone(),
136 scope: args.scopes.clone(),
137 tenant: args.tenant.clone(),
138 subscription_tier: SubscriptionTier::try_from(tenant.subscription_tier).map_err(
139 |e| OIDCError::new(OIDCErrorCode::ServerError, Some(e.to_string()), None),
140 )?,
141 project: Some(args.project.clone()),
142 user_role: args.user_role,
143 user_id: AuthorId::new(args.user_id.clone()),
144 membership: args.membership.clone(),
145 resource_type: args.user_kind,
146 access_policy_version_ids: args.access_policy_version_ids,
147 },
148 &encoding_key.encoding_key,
149 )
150 .map_err(|_| {
151 OIDCError::new(
152 OIDCErrorCode::ServerError,
153 Some("Failed to create access token.".to_string()),
154 None,
155 )
156 })?;
157
158 let mut response = TokenResponse {
159 access_token: token.clone(),
160 id_token: None,
161 expires_in: TOKEN_EXPIRATION,
162 refresh_token: None,
163 token_type: TokenType::Bearer,
164 };
165
166 if args.scopes.contains_scope(&Scope::OIDC(OIDCScope::OpenId)) {
167 response.id_token = Some(token);
168 }
169
170 if (&args.scopes.0)
172 .iter()
173 .find(|s| **s == Scope::OIDC(OIDCScope::OfflineAccess))
174 .is_some()
175 && client_app
176 .grantType
177 .iter()
178 .find(|gt| {
179 let discriminator = std::mem::discriminant(gt.as_ref());
180 let offline_discriminator =
181 std::mem::discriminant(&ClientapplicationGrantType::Refresh_token(None));
182 discriminator == offline_discriminator
183 })
184 .is_some()
185 && *grant_type_used != schemas::token_body::OAuth2TokenBodyGrantType::ClientCredentials
188 {
189 let existing_refresh_tokens_for_agent =
190 ProjectAuthAdmin::<CreateAuthorizationCode, _, _, _, _>::search(
191 repo,
192 &args.tenant,
193 &args.project,
194 &AuthorizationCodeSearchClaims {
195 client_id: Some(args.client_id.clone()),
196 user_id: Some(args.user_id.clone()),
197 kind: Some(AuthorizationCodeKind::RefreshToken),
198 code: None,
199 user_agent: user_agent.as_ref().map(|ua| ua.as_str().to_string()),
200 is_expired: None,
201 },
202 )
203 .await
204 .map_err(|_| {
205 OIDCError::new(
206 OIDCErrorCode::ServerError,
207 Some("Failed to retrieve existing refresh tokens.".to_string()),
208 None,
209 )
210 })?;
211
212 for existing_token in existing_refresh_tokens_for_agent {
213 ProjectAuthAdmin::<CreateAuthorizationCode, _, _, _, _>::delete(
214 repo,
215 &args.tenant,
216 &args.project,
217 &existing_token.code,
218 )
219 .await
220 .map_err(|_e| {
221 OIDCError::new(
222 OIDCErrorCode::ServerError,
223 Some("Failed to delete existing refresh token.".to_string()),
224 None,
225 )
226 })?;
227 }
228
229 let refresh_token = ProjectAuthAdmin::create(
230 repo,
231 &args.tenant,
232 &args.project,
233 CreateAuthorizationCode {
234 membership: args.membership,
235 user_id: args.user_id,
236 expires_in: Duration::from_secs(REFRESH_TOKEN_EXPIRATION as u64),
237 kind: AuthorizationCodeKind::RefreshToken,
238 client_id: Some(args.client_id),
239 pkce_code_challenge: None,
240 pkce_code_challenge_method: None,
241 redirect_uri: None,
242 meta: Some(sqlx::types::Json(json!({
243 "user_agent": user_agent.as_ref().map(|ua| ua.to_string()),
244 }))),
245 },
246 )
247 .await
248 .map_err(|_e| {
249 OIDCError::new(
250 OIDCErrorCode::ServerError,
251 Some("Failed to create refresh token.".to_string()),
252 None,
253 )
254 })?;
255
256 response.refresh_token = Some(refresh_token.code);
257 }
258
259 Ok(response)
260}
261
262async fn get_approved_scopes<Repo: Repository>(
263 repo: &Repo,
264 tenant: &TenantId,
265 project: &ProjectId,
266 user_id: UserId,
267 client_id: ClientId,
268) -> Result<Scopes, OIDCError> {
269 let approved_scopes = ProjectAuthAdmin::<CreateScope, _, _, _, _>::search(
270 repo,
271 &tenant,
272 &project,
273 &ScopeSearchClaims {
274 user_: Some(user_id),
275 client: Some(client_id),
276 },
277 )
278 .await
279 .map_err(|_e| {
280 OIDCError::new(
281 OIDCErrorCode::ServerError,
282 Some("Failed to retrieve user's approved scopes.".to_string()),
283 None,
284 )
285 })?
286 .get(0)
287 .map(|s| s.scope.clone())
288 .unwrap_or_else(|| Default::default());
289
290 Ok(approved_scopes)
291}
292
293fn validate_client_grant_type(
294 client_app: &ClientApplication,
295 grant_type: &ClientapplicationGrantType,
296) -> Result<(), OIDCError> {
297 if client_app
298 .grantType
299 .iter()
300 .find(|gt| {
301 let discriminator = std::mem::discriminant(gt.as_ref());
302 let requested_discriminator = std::mem::discriminant(grant_type);
303 discriminator == requested_discriminator
304 })
305 .is_none()
306 {
307 return Err(OIDCError::new(
308 OIDCErrorCode::AccessDenied,
309 Some("Client application is not authorized for the requested grant type.".to_string()),
310 None,
311 ));
312 }
313
314 Ok(())
315}
316
317fn verify_client(
318 client_app: &ClientApplication,
319 token_request_body: &schemas::token_body::OAuth2TokenBody,
320) -> Result<(), OIDCError> {
321 match token_request_body.grant_type {
323 schemas::token_body::OAuth2TokenBodyGrantType::ClientCredentials => {
324 validate_client_grant_type(
325 client_app,
326 &ClientapplicationGrantType::Client_credentials(None),
327 )?;
328 }
329 schemas::token_body::OAuth2TokenBodyGrantType::RefreshToken => {
330 validate_client_grant_type(
331 client_app,
332 &ClientapplicationGrantType::Refresh_token(None),
333 )?;
334 }
335 schemas::token_body::OAuth2TokenBodyGrantType::AuthorizationCode => {
336 validate_client_grant_type(
337 client_app,
338 &ClientapplicationGrantType::Authorization_code(None),
339 )?;
340 }
341 }
342
343 if client_app.id.as_ref() != token_request_body.client_id.as_ref() {
344 return Err(OIDCError::new(
345 OIDCErrorCode::AccessDenied,
346 Some("Invalid credentials".to_string()),
347 None,
348 ));
349 }
350
351 if client_app
352 .secret
353 .as_ref()
354 .and_then(|s| s.value.as_ref().map(String::as_str))
355 != token_request_body
356 .client_secret
357 .as_ref()
358 .map(String::as_str)
359 {
360 return Err(OIDCError::new(
361 OIDCErrorCode::AccessDenied,
362 Some("Invalid credentials".to_string()),
363 None,
364 ));
365 }
366
367 Ok(())
368}
369
370async fn find_users_access_policy_version_ids<Search: SearchEngine>(
371 search: &Search,
372 tenant: &TenantId,
373 project: &ProjectId,
374 user_id: &str,
375 user_type: &ResourceType,
376) -> Result<Vec<VersionId>, OIDCError> {
377 let access_policies = search
378 .search(
379 &SupportedFHIRVersions::R4,
380 &tenant,
381 &project,
382 &SearchRequest::Type(FHIRSearchTypeRequest {
383 resource_type: ResourceType::AccessPolicyV2,
384 parameters: ParsedParameters::new(vec![ParsedParameter::Resource(Parameter {
385 name: "link".to_string(),
386 value: vec![format!("{}/{}", user_type.as_ref(), user_id)],
387 modifier: None,
388 chains: None,
389 })]),
390 }),
391 None,
392 )
393 .await
394 .map_err(|_e| {
395 OIDCError::new(
396 OIDCErrorCode::ServerError,
397 Some("Failed to search for user's access policies.".to_string()),
398 None,
399 )
400 })?;
401
402 Ok(access_policies
403 .entries
404 .into_iter()
405 .map(|ap| ap.version_id)
406 .collect())
407}
408
409#[derive(PartialEq, Eq)]
410pub enum ClientCredentialsMethod {
411 BasicAuth,
412 Body,
413}
414
415pub async fn client_credentials_to_token_response<
416 Repo: Repository + Send + Sync,
417 Search: SearchEngine + Send + Sync,
418 Terminology: FHIRTerminology + Send + Sync,
419>(
420 state: &AppState<Repo, Search, Terminology>,
421 tenant: &TenantId,
422 project: &ProjectId,
423 user_agent: &Option<TypedHeader<UserAgent>>,
424 token_body: &schemas::token_body::OAuth2TokenBody,
425 method: ClientCredentialsMethod,
426) -> Result<TokenResponse, OIDCError> {
427 let Some(client_id) = &token_body.client_id else {
428 return Err(OIDCError::new(
429 OIDCErrorCode::InvalidRequest,
430 Some("client_id is required for client_credentials grant type.".to_string()),
431 token_body.redirect_uri.clone(),
432 ));
433 };
434
435 let client_app =
436 find_client_app(state, tenant.clone(), project.clone(), client_id.clone()).await?;
437
438 verify_client(&client_app, &token_body)?;
439
440 if method == ClientCredentialsMethod::BasicAuth {
442 validate_client_grant_type(&client_app, &ClientapplicationGrantType::Basic_auth(None))?;
443 }
444
445 let client_app_scopes = client_app
446 .scope
447 .as_ref()
448 .and_then(|s| s.value.as_ref().map(String::as_str))
449 .unwrap_or_default();
450
451 let requested_scopes = Scopes::from(
452 token_body
453 .scope
454 .clone()
455 .unwrap_or_else(|| client_app_scopes.to_string()),
456 );
457
458 verify_requested_scope_is_subset(
459 &requested_scopes,
460 &Scopes::try_from(client_app_scopes).map_err(|_| {
461 OIDCError::new(
462 OIDCErrorCode::InvalidScope,
463 Some("Client application's configured scopes are invalid.".to_string()),
464 None,
465 )
466 })?,
467 )?;
468
469 let response = create_token_response(
470 user_agent,
471 &*state.repo,
472 &client_app,
473 &token_body.grant_type,
474 TokenResponseArguments {
475 user_id: client_app.id.clone().unwrap_or_default(),
476 user_role: UserRole::Member,
477 user_kind: AuthorKind::ClientApplication,
478 client_id: client_app.id.clone().unwrap_or_default(),
479 scopes: requested_scopes,
480 tenant: tenant.clone(),
481 project: project.clone(),
482 membership: None,
483 access_policy_version_ids: find_users_access_policy_version_ids(
484 state.search.as_ref(),
485 &tenant,
486 &project,
487 client_id,
488 &ResourceType::ClientApplication,
489 )
490 .await?,
491 },
492 )
493 .await?;
494
495 Ok(response)
496}
497
498pub async fn token<
499 Repo: Repository + Send + Sync,
500 Search: SearchEngine + Send + Sync,
501 Terminology: FHIRTerminology + Send + Sync,
502>(
503 _: TokenPath,
504 user_agent: Option<TypedHeader<UserAgent>>,
505 Cached(TenantIdentifier { tenant }): Cached<TenantIdentifier>,
506 Cached(ProjectIdentifier { project }): Cached<ProjectIdentifier>,
507 State(state): State<Arc<AppState<Repo, Search, Terminology>>>,
508 OAuthTokenBody(token_body): OAuthTokenBody,
509) -> Result<Response, OIDCError> {
510 match &token_body.grant_type {
511 schemas::token_body::OAuth2TokenBodyGrantType::ClientCredentials => {
512 let response = client_credentials_to_token_response(
513 &*state,
514 &tenant,
515 &project,
516 &user_agent,
517 &token_body,
518 ClientCredentialsMethod::Body,
519 )
520 .await?;
521
522 Ok(Json(response).into_response())
523 }
524 schemas::token_body::OAuth2TokenBodyGrantType::RefreshToken => {
525 let Some(client_id) = &token_body.client_id else {
526 return Err(OIDCError::new(
527 OIDCErrorCode::InvalidRequest,
528 Some("client_id is required for refresh_token grant type.".to_string()),
529 token_body.redirect_uri.clone(),
530 ));
531 };
532 let refresh_token = &token_body.refresh_token.as_ref().ok_or_else(|| {
533 OIDCError::new(
534 OIDCErrorCode::InvalidRequest,
535 Some("refresh_token is required for refresh_token grant type.".to_string()),
536 token_body.redirect_uri.clone(),
537 )
538 })?;
539
540 let client_app =
541 find_client_app(&state, tenant.clone(), project.clone(), client_id.clone()).await?;
542
543 verify_client(&client_app, &token_body)?;
544
545 let code = code_verification::retrieve_and_verify_code(
546 &*state.repo,
547 &tenant,
548 &project,
549 &client_app,
550 AuthorizationCodeKind::RefreshToken,
551 &refresh_token,
552 None,
553 None,
554 )
555 .await
556 .map_err(|_e| {
557 OIDCError::new(
558 OIDCErrorCode::InvalidGrant,
559 Some("Invalid refresh token.".to_string()),
560 token_body.redirect_uri.clone(),
561 )
562 })?;
563
564 if code.kind != AuthorizationCodeKind::RefreshToken {
565 return Err(OIDCError::new(
566 OIDCErrorCode::InvalidGrant,
567 Some("Invalid refresh token.".to_string()),
568 token_body.redirect_uri.clone(),
569 ));
570 }
571
572 if code.is_expired.unwrap_or(true) {
573 return Err(OIDCError::new(
574 OIDCErrorCode::InvalidGrant,
575 Some("Refresh token has expired.".to_string()),
576 token_body.redirect_uri.clone(),
577 ));
578 }
579
580 let approved_scopes = get_approved_scopes(
581 &*state.repo,
582 &tenant,
583 &project,
584 UserId::new(code.user_id.clone()),
585 ClientId::new(client_id.clone()),
586 )
587 .await?;
588
589 ProjectAuthAdmin::<CreateAuthorizationCode, _, _, _, _>::delete(
590 &*state.repo,
591 &tenant,
592 &project,
593 &refresh_token,
594 )
595 .await
596 .map_err(|_e| {
597 OIDCError::new(
598 OIDCErrorCode::ServerError,
599 Some("Failed to delete used refresh token.".to_string()),
600 token_body.redirect_uri.clone(),
601 )
602 })?;
603
604 let user =
605 TenantAuthAdmin::<_, User, _, _, _>::read(&*state.repo, &tenant, &code.user_id)
606 .await
607 .map_err(|_e| {
608 OIDCError::new(
609 OIDCErrorCode::ServerError,
610 Some("Failed to retrieve user.".to_string()),
611 token_body.redirect_uri.clone(),
612 )
613 })?;
614
615 let response = create_token_response(
616 &user_agent,
617 &*state.repo,
618 &client_app,
619 &token_body.grant_type,
620 TokenResponseArguments {
621 user_id: code.user_id,
622 user_kind: AuthorKind::Membership,
623 user_role: match user.map(|u| u.role) {
624 Some(RepoUserRole::Admin) => UserRole::Admin,
625 Some(RepoUserRole::Member) => UserRole::Member,
626 Some(RepoUserRole::Owner) => UserRole::Owner,
627 None => UserRole::Member,
628 },
629 client_id: client_id.clone(),
630 scopes: approved_scopes.clone(),
631 tenant: tenant.clone(),
632 project: project.clone(),
633 access_policy_version_ids: match code.membership.as_ref() {
634 Some(membership) => {
635 find_users_access_policy_version_ids(
636 state.search.as_ref(),
637 &tenant,
638 &project,
639 &membership,
640 &ResourceType::Membership,
641 )
642 .await?
643 }
644 None => vec![],
645 },
646 membership: code.membership,
647 },
648 )
649 .await?;
650
651 Ok(Json(response).into_response())
652 }
653 schemas::token_body::OAuth2TokenBodyGrantType::AuthorizationCode => {
654 let Some(client_id) = &token_body.client_id else {
655 return Err(OIDCError::new(
656 OIDCErrorCode::InvalidRequest,
657 Some("client_id is required for authorization_code grant type.".to_string()),
658 token_body.redirect_uri.clone(),
659 ));
660 };
661
662 let code = token_body.code.as_ref().ok_or_else(|| {
663 OIDCError::new(
664 OIDCErrorCode::InvalidRequest,
665 Some("code is required for authorization_code grant type.".to_string()),
666 None,
667 )
668 })?;
669 let code_verifier = token_body.code_verifier.as_ref().ok_or_else(|| {
670 OIDCError::new(
671 OIDCErrorCode::InvalidRequest,
672 Some(
673 "code_verifier is required for authorization_code grant type.".to_string(),
674 ),
675 None,
676 )
677 })?;
678 let redirect_uri = token_body.redirect_uri.as_ref().ok_or_else(|| {
679 OIDCError::new(
680 OIDCErrorCode::InvalidRequest,
681 Some("redirect_uri is required for authorization_code grant type.".to_string()),
682 None,
683 )
684 })?;
685
686 let client_app =
687 find_client_app(&state, tenant.clone(), project.clone(), client_id.clone()).await?;
688
689 verify_client(&client_app, &token_body)?;
690
691 let code = code_verification::retrieve_and_verify_code(
692 &*state.repo,
693 &tenant,
694 &project,
695 &client_app,
696 AuthorizationCodeKind::OAuth2CodeGrant,
697 &code,
698 Some(&redirect_uri),
699 Some(&code_verifier),
700 )
701 .await
702 .map_err(|_| {
703 OIDCError::new(
704 OIDCErrorCode::AccessDenied,
705 Some("Invalid authorization code.".to_string()),
706 None,
707 )
708 })?;
709
710 if code.kind != AuthorizationCodeKind::OAuth2CodeGrant {
711 return Err(OIDCError::new(
712 OIDCErrorCode::InvalidGrant,
713 Some("Invalid authorization code.".to_string()),
714 None,
715 ));
716 }
717
718 if code.is_expired.unwrap_or(true) {
719 return Err(OIDCError::new(
720 OIDCErrorCode::AccessDenied,
721 Some("Authorization code has expired.".to_string()),
722 None,
723 ));
724 }
725
726 let approved_scopes = get_approved_scopes(
727 &*state.repo,
728 &tenant,
729 &project,
730 UserId::new(code.user_id.clone()),
731 ClientId::new(client_id.clone()),
732 )
733 .await?;
734
735 ProjectAuthAdmin::<CreateAuthorizationCode, _, _, _, _>::delete(
737 &*state.repo,
738 &tenant,
739 &project,
740 &code.code,
741 )
742 .await
743 .map_err(|_e| {
744 OIDCError::new(
745 OIDCErrorCode::ServerError,
746 Some("Failed to delete used authorization code.".to_string()),
747 None,
748 )
749 })?;
750
751 let user =
752 TenantAuthAdmin::<_, User, _, _, _>::read(&*state.repo, &tenant, &code.user_id)
753 .await
754 .map_err(|_e| {
755 OIDCError::new(
756 OIDCErrorCode::ServerError,
757 Some("Failed to retrieve user.".to_string()),
758 None,
759 )
760 })?;
761
762 let response = create_token_response(
763 &user_agent,
764 &*state.repo,
765 &client_app,
766 &token_body.grant_type,
767 TokenResponseArguments {
768 user_id: code.user_id,
769 user_kind: AuthorKind::Membership,
770 user_role: match user.map(|u| u.role) {
771 Some(RepoUserRole::Admin) => UserRole::Admin,
772 Some(RepoUserRole::Member) => UserRole::Member,
773 Some(RepoUserRole::Owner) => UserRole::Owner,
774 None => UserRole::Member,
775 },
776 client_id: client_id.clone(),
777 scopes: approved_scopes.clone(),
778 tenant: tenant.clone(),
779 project: project.clone(),
780 access_policy_version_ids: match code.membership.as_ref() {
781 Some(membership) => {
782 find_users_access_policy_version_ids(
783 state.search.as_ref(),
784 &tenant,
785 &project,
786 &membership,
787 &ResourceType::Membership,
788 )
789 .await?
790 }
791 None => vec![],
792 },
793 membership: code.membership,
794 },
795 )
796 .await?;
797
798 Ok(Json(response).into_response())
799 }
800 }
801}