From 61c4b1c3fd3a4260d91d8626a604d2d4177edb34 Mon Sep 17 00:00:00 2001 From: Livio Spring Date: Fri, 14 Mar 2025 07:05:45 +0100 Subject: [PATCH] fix(console): allow management of metadata of users of other organizations again (#9490) # Which Problems Are Solved With the recent change in Console to use the User V2 API (https://github.com/zitadel/zitadel/pull/9312), some functionality still needs to call the management API, which requires the organization context. The context was not passed anymore, leading to error in cases where the calling user (e.g. an IAM_OWNER) was not part of the same organization. # How the Problems Are Solved Added an interceptor to provide the `x-zitadel-orgid` header for the new management client. # Additional Changes None # Additional Context - closes #9488 --- console/src/app/services/grpc.service.ts | 14 +++- .../services/interceptors/org.interceptor.ts | 65 ++++++++++++++----- 2 files changed, 59 insertions(+), 20 deletions(-) diff --git a/console/src/app/services/grpc.service.ts b/console/src/app/services/grpc.service.ts index e554e2b647..52332eae15 100644 --- a/console/src/app/services/grpc.service.ts +++ b/console/src/app/services/grpc.service.ts @@ -14,7 +14,7 @@ import { ExhaustedService } from './exhausted.service'; import { AuthInterceptor, AuthInterceptorProvider, NewConnectWebAuthInterceptor } from './interceptors/auth.interceptor'; import { ExhaustedGrpcInterceptor } from './interceptors/exhausted.grpc.interceptor'; import { I18nInterceptor } from './interceptors/i18n.interceptor'; -import { OrgInterceptor } from './interceptors/org.interceptor'; +import { NewConnectWebOrgInterceptor, OrgInterceptor, OrgInterceptorProvider } from './interceptors/org.interceptor'; import { StorageService } from './storage.service'; import { UserServiceClient } from '../proto/generated/zitadel/user/v2/User_serviceServiceClientPb'; //@ts-ignore @@ -46,6 +46,7 @@ export class GrpcService { private readonly exhaustedService: ExhaustedService, private readonly authInterceptor: AuthInterceptor, private readonly authInterceptorProvider: AuthInterceptorProvider, + private readonly orgInterceptorProvider: OrgInterceptorProvider, ) {} public loadAppEnvironment(): Promise { @@ -62,7 +63,7 @@ export class GrpcService { const interceptors = { unaryInterceptors: [ new ExhaustedGrpcInterceptor(this.exhaustedService, this.envService), - new OrgInterceptor(this.storageService), + new OrgInterceptor(this.orgInterceptorProvider), this.authInterceptor, new I18nInterceptor(this.translate), ], @@ -103,8 +104,15 @@ export class GrpcService { baseUrl: env.api, interceptors: [NewConnectWebAuthInterceptor(this.authInterceptorProvider)], }); + const transportOldAPIs = createGrpcWebTransport({ + baseUrl: env.api, + interceptors: [ + NewConnectWebAuthInterceptor(this.authInterceptorProvider), + NewConnectWebOrgInterceptor(this.orgInterceptorProvider), + ], + }); this.userNew = createUserServiceClient(transport); - this.mgmtNew = createManagementServiceClient(transport); + this.mgmtNew = createManagementServiceClient(transportOldAPIs); this.authNew = createAuthServiceClient(transport); const authConfig: AuthConfig = { diff --git a/console/src/app/services/interceptors/org.interceptor.ts b/console/src/app/services/interceptors/org.interceptor.ts index 2e85b8f4b9..e9e9745b12 100644 --- a/console/src/app/services/interceptors/org.interceptor.ts +++ b/console/src/app/services/interceptors/org.interceptor.ts @@ -3,32 +3,63 @@ import { Request, RpcError, StatusCode, UnaryInterceptor, UnaryResponse } from ' import { Org } from 'src/app/proto/generated/zitadel/org_pb'; import { StorageKey, StorageLocation, StorageService } from '../storage.service'; +import { ConnectError, Interceptor } from '@connectrpc/connect'; +import { firstValueFrom, identity, Observable, Subject } from 'rxjs'; +import { debounceTime, filter, map } from 'rxjs/operators'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; const ORG_HEADER_KEY = 'x-zitadel-orgid'; @Injectable({ providedIn: 'root' }) export class OrgInterceptor implements UnaryInterceptor { - constructor(private readonly storageService: StorageService) {} + constructor(private readonly orgInterceptorProvider: OrgInterceptorProvider) {} public async intercept(request: Request, invoker: any): Promise> { const metadata = request.getMetadata(); - const org: Org.AsObject | null = this.storageService.getItem(StorageKey.organization, StorageLocation.session); - - if (org) { - metadata[ORG_HEADER_KEY] = `${org.id}`; + const orgId = this.orgInterceptorProvider.getOrgId(); + if (orgId) { + metadata[ORG_HEADER_KEY] = orgId; } - try { - return await invoker(request); - } catch (error: any) { - if ( - error instanceof RpcError && - error.code === StatusCode.PERMISSION_DENIED && - error.message.startsWith("Organisation doesn't exist") - ) { - this.storageService.removeItem(StorageKey.organization, StorageLocation.session); - } - throw error; - } + return invoker(request).catch(this.orgInterceptorProvider.handleError); } } + +export function NewConnectWebOrgInterceptor(orgInterceptorProvider: OrgInterceptorProvider): Interceptor { + return (next) => async (req) => { + if (!req.header.get(ORG_HEADER_KEY)) { + const orgId = orgInterceptorProvider.getOrgId(); + if (orgId) { + req.header.set(ORG_HEADER_KEY, orgId); + } + } + + return next(req).catch(orgInterceptorProvider.handleError); + }; +} + +@Injectable({ providedIn: 'root' }) +export class OrgInterceptorProvider { + constructor(private storageService: StorageService) {} + + getOrgId() { + const org: Org.AsObject | null = this.storageService.getItem(StorageKey.organization, StorageLocation.session); + return org?.id; + } + + handleError = (error: any): never => { + if (!(error instanceof RpcError) && !(error instanceof ConnectError)) { + throw error; + } + + if ( + error instanceof RpcError && + error.code === StatusCode.PERMISSION_DENIED && + error.message.startsWith("Organisation doesn't exist") + ) { + this.storageService.removeItem(StorageKey.organization, StorageLocation.session); + } + + throw error; + }; +}