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
This commit is contained in:
Livio Spring
2025-03-14 07:05:45 +01:00
committed by GitHub
parent e36f402e09
commit 61c4b1c3fd
2 changed files with 59 additions and 20 deletions

View File

@@ -14,7 +14,7 @@ import { ExhaustedService } from './exhausted.service';
import { AuthInterceptor, AuthInterceptorProvider, NewConnectWebAuthInterceptor } from './interceptors/auth.interceptor'; import { AuthInterceptor, AuthInterceptorProvider, NewConnectWebAuthInterceptor } from './interceptors/auth.interceptor';
import { ExhaustedGrpcInterceptor } from './interceptors/exhausted.grpc.interceptor'; import { ExhaustedGrpcInterceptor } from './interceptors/exhausted.grpc.interceptor';
import { I18nInterceptor } from './interceptors/i18n.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 { StorageService } from './storage.service';
import { UserServiceClient } from '../proto/generated/zitadel/user/v2/User_serviceServiceClientPb'; import { UserServiceClient } from '../proto/generated/zitadel/user/v2/User_serviceServiceClientPb';
//@ts-ignore //@ts-ignore
@@ -46,6 +46,7 @@ export class GrpcService {
private readonly exhaustedService: ExhaustedService, private readonly exhaustedService: ExhaustedService,
private readonly authInterceptor: AuthInterceptor, private readonly authInterceptor: AuthInterceptor,
private readonly authInterceptorProvider: AuthInterceptorProvider, private readonly authInterceptorProvider: AuthInterceptorProvider,
private readonly orgInterceptorProvider: OrgInterceptorProvider,
) {} ) {}
public loadAppEnvironment(): Promise<any> { public loadAppEnvironment(): Promise<any> {
@@ -62,7 +63,7 @@ export class GrpcService {
const interceptors = { const interceptors = {
unaryInterceptors: [ unaryInterceptors: [
new ExhaustedGrpcInterceptor(this.exhaustedService, this.envService), new ExhaustedGrpcInterceptor(this.exhaustedService, this.envService),
new OrgInterceptor(this.storageService), new OrgInterceptor(this.orgInterceptorProvider),
this.authInterceptor, this.authInterceptor,
new I18nInterceptor(this.translate), new I18nInterceptor(this.translate),
], ],
@@ -103,8 +104,15 @@ export class GrpcService {
baseUrl: env.api, baseUrl: env.api,
interceptors: [NewConnectWebAuthInterceptor(this.authInterceptorProvider)], interceptors: [NewConnectWebAuthInterceptor(this.authInterceptorProvider)],
}); });
const transportOldAPIs = createGrpcWebTransport({
baseUrl: env.api,
interceptors: [
NewConnectWebAuthInterceptor(this.authInterceptorProvider),
NewConnectWebOrgInterceptor(this.orgInterceptorProvider),
],
});
this.userNew = createUserServiceClient(transport); this.userNew = createUserServiceClient(transport);
this.mgmtNew = createManagementServiceClient(transport); this.mgmtNew = createManagementServiceClient(transportOldAPIs);
this.authNew = createAuthServiceClient(transport); this.authNew = createAuthServiceClient(transport);
const authConfig: AuthConfig = { const authConfig: AuthConfig = {

View File

@@ -3,24 +3,55 @@ import { Request, RpcError, StatusCode, UnaryInterceptor, UnaryResponse } from '
import { Org } from 'src/app/proto/generated/zitadel/org_pb'; import { Org } from 'src/app/proto/generated/zitadel/org_pb';
import { StorageKey, StorageLocation, StorageService } from '../storage.service'; 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'; const ORG_HEADER_KEY = 'x-zitadel-orgid';
@Injectable({ providedIn: 'root' }) @Injectable({ providedIn: 'root' })
export class OrgInterceptor<TReq = unknown, TResp = unknown> implements UnaryInterceptor<TReq, TResp> { export class OrgInterceptor<TReq = unknown, TResp = unknown> implements UnaryInterceptor<TReq, TResp> {
constructor(private readonly storageService: StorageService) {} constructor(private readonly orgInterceptorProvider: OrgInterceptorProvider) {}
public async intercept(request: Request<TReq, TResp>, invoker: any): Promise<UnaryResponse<TReq, TResp>> { public async intercept(request: Request<TReq, TResp>, invoker: any): Promise<UnaryResponse<TReq, TResp>> {
const metadata = request.getMetadata(); const metadata = request.getMetadata();
const org: Org.AsObject | null = this.storageService.getItem(StorageKey.organization, StorageLocation.session); const orgId = this.orgInterceptorProvider.getOrgId();
if (orgId) {
if (org) { metadata[ORG_HEADER_KEY] = orgId;
metadata[ORG_HEADER_KEY] = `${org.id}`; }
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;
} }
try {
return await invoker(request);
} catch (error: any) {
if ( if (
error instanceof RpcError && error instanceof RpcError &&
error.code === StatusCode.PERMISSION_DENIED && error.code === StatusCode.PERMISSION_DENIED &&
@@ -28,7 +59,7 @@ export class OrgInterceptor<TReq = unknown, TResp = unknown> implements UnaryInt
) { ) {
this.storageService.removeItem(StorageKey.organization, StorageLocation.session); this.storageService.removeItem(StorageKey.organization, StorageLocation.session);
} }
throw error; throw error;
} };
}
} }