Skip to content

Commit

Permalink
[server] Introduce RequestContext
Browse files Browse the repository at this point in the history
  • Loading branch information
geropl committed Nov 8, 2023
1 parent 6c0bb90 commit b1546cc
Show file tree
Hide file tree
Showing 21 changed files with 454 additions and 174 deletions.
4 changes: 1 addition & 3 deletions components/gitpod-protocol/src/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@

export const IAnalyticsWriter = Symbol("IAnalyticsWriter");

type Identity =
| { userId: string | number; anonymousId?: string | number }
| { userId?: string | number; anonymousId: string | number };
type Identity = { userId?: string | number; anonymousId?: string | number; subjectId?: string };

interface Message {
messageId?: string;
Expand Down
1 change: 1 addition & 0 deletions components/gitpod-protocol/src/util/logging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface LogContext {
organizationId?: string;
sessionId?: string;
userId?: string;
subjectId?: string;
workspaceId?: string;
instanceId?: string;
}
Expand Down
13 changes: 0 additions & 13 deletions components/server/src/api/handler-context-augmentation.d.ts

This file was deleted.

18 changes: 9 additions & 9 deletions components/server/src/api/hello-service-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
*/

import { HandlerContext, ServiceImpl } from "@connectrpc/connect";
import { User } from "@gitpod/gitpod-protocol";
import { HelloService } from "@gitpod/public-api/lib/gitpod/experimental/v1/dummy_connect";
import {
LotsOfRepliesRequest,
Expand All @@ -14,27 +13,28 @@ import {
SayHelloResponse,
} from "@gitpod/public-api/lib/gitpod/experimental/v1/dummy_pb";
import { injectable } from "inversify";
import { ctx } from "../util/request-context";

@injectable()
export class HelloServiceAPI implements ServiceImpl<typeof HelloService> {
async sayHello(req: SayHelloRequest, context: HandlerContext): Promise<SayHelloResponse> {
async sayHello(req: SayHelloRequest, _: HandlerContext): Promise<SayHelloResponse> {
const response = new SayHelloResponse();
response.reply = "Hello " + this.getSubject(context);
response.reply = "Hello " + getSubject();
return response;
}
async *lotsOfReplies(req: LotsOfRepliesRequest, context: HandlerContext): AsyncGenerator<LotsOfRepliesResponse> {
async *lotsOfReplies(req: LotsOfRepliesRequest, _: HandlerContext): AsyncGenerator<LotsOfRepliesResponse> {
let count = req.previousCount || 0;
while (!context.signal.aborted) {
while (!ctx().signal.aborted) {
const response = new LotsOfRepliesResponse();
response.reply = `Hello ${this.getSubject(context)} ${count}`;
response.reply = `Hello ${getSubject()} ${count}`;
response.count = count;
yield response;
count++;
await new Promise((resolve) => setTimeout(resolve, 30000));
}
}
}

private getSubject(context: HandlerContext): string {
return User.getName(context.user) || "World";
}
function getSubject(): string {
return ctx().subjectId?.toString() || "World";
}
63 changes: 32 additions & 31 deletions components/server/src/api/organization-service-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ import {
import { PublicAPIConverter } from "@gitpod/gitpod-protocol/lib/public-api-converter";
import { OrganizationService } from "../orgs/organization-service";
import { PaginationResponse } from "@gitpod/public-api/lib/gitpod/v1/pagination_pb";
import { ctx, userId } from "../util/request-context";
import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";

@injectable()
export class OrganizationServiceAPI implements ServiceImpl<typeof OrganizationServiceInterface> {
Expand All @@ -49,18 +51,20 @@ export class OrganizationServiceAPI implements ServiceImpl<typeof OrganizationSe
private readonly apiConverter: PublicAPIConverter,
) {}

async createOrganization(
req: CreateOrganizationRequest,
context: HandlerContext,
): Promise<CreateOrganizationResponse> {
const org = await this.orgService.createOrganization(context.user.id, req.name);
async createOrganization(req: CreateOrganizationRequest, _: HandlerContext): Promise<CreateOrganizationResponse> {
// TODO(gpl) This mimicks the current behavior of adding the subjectId as owner
const ownerId = ctx().subjectId?.userId();
if (!ownerId) {
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "No userId available");
}
const org = await this.orgService.createOrganization(ownerId, req.name);
const response = new CreateOrganizationResponse();
response.organization = this.apiConverter.toOrganization(org);
return response;
}

async getOrganization(req: GetOrganizationRequest, context: HandlerContext): Promise<GetOrganizationResponse> {
const org = await this.orgService.getOrganization(context.user.id, req.organizationId);
async getOrganization(req: GetOrganizationRequest, _: HandlerContext): Promise<GetOrganizationResponse> {
const org = await this.orgService.getOrganization(userId(), req.organizationId);
const response = new GetOrganizationResponse();
response.organization = this.apiConverter.toOrganization(org);
return response;
Expand All @@ -70,7 +74,7 @@ export class OrganizationServiceAPI implements ServiceImpl<typeof OrganizationSe
req: UpdateOrganizationRequest,
context: HandlerContext,
): Promise<UpdateOrganizationResponse> {
const org = await this.orgService.updateOrganization(context.user.id, req.organizationId, {
const org = await this.orgService.updateOrganization(userId(), req.organizationId, {
name: req.name,
});
return new UpdateOrganizationResponse({
Expand All @@ -83,7 +87,7 @@ export class OrganizationServiceAPI implements ServiceImpl<typeof OrganizationSe
context: HandlerContext,
): Promise<ListOrganizationsResponse> {
const orgs = await this.orgService.listOrganizations(
context.user.id,
userId(),
{
limit: req.pagination?.pageSize || 100,
offset: (req.pagination?.page || 0) * (req.pagination?.pageSize || 0),
Expand All @@ -97,46 +101,43 @@ export class OrganizationServiceAPI implements ServiceImpl<typeof OrganizationSe
return response;
}

async deleteOrganization(
req: DeleteOrganizationRequest,
context: HandlerContext,
): Promise<DeleteOrganizationResponse> {
await this.orgService.deleteOrganization(context.user.id, req.organizationId);
async deleteOrganization(req: DeleteOrganizationRequest, _: HandlerContext): Promise<DeleteOrganizationResponse> {
await this.orgService.deleteOrganization(userId(), req.organizationId);
return new DeleteOrganizationResponse();
}

async getOrganizationInvitation(
req: GetOrganizationInvitationRequest,
context: HandlerContext,
_: HandlerContext,
): Promise<GetOrganizationInvitationResponse> {
const invitation = await this.orgService.getOrCreateInvite(context.user.id, req.organizationId);
const invitation = await this.orgService.getOrCreateInvite(userId(), req.organizationId);
const response = new GetOrganizationInvitationResponse();
response.invitationId = invitation.id;
return response;
}

async joinOrganization(req: JoinOrganizationRequest, context: HandlerContext): Promise<JoinOrganizationResponse> {
const orgId = await this.orgService.joinOrganization(context.user.id, req.invitationId);
async joinOrganization(req: JoinOrganizationRequest, _: HandlerContext): Promise<JoinOrganizationResponse> {
const orgId = await this.orgService.joinOrganization(userId(), req.invitationId);
const result = new JoinOrganizationResponse();
result.organizationId = orgId;
return result;
}

async resetOrganizationInvitation(
req: ResetOrganizationInvitationRequest,
context: HandlerContext,
_: HandlerContext,
): Promise<ResetOrganizationInvitationResponse> {
const inviteId = await this.orgService.resetInvite(context.user.id, req.organizationId);
const inviteId = await this.orgService.resetInvite(userId(), req.organizationId);
const result = new ResetOrganizationInvitationResponse();
result.invitationId = inviteId.id;
return result;
}

async listOrganizationMembers(
req: ListOrganizationMembersRequest,
context: HandlerContext,
_: HandlerContext,
): Promise<ListOrganizationMembersResponse> {
const members = await this.orgService.listMembers(context.user.id, req.organizationId);
const members = await this.orgService.listMembers(userId(), req.organizationId);
//TODO pagination
const response = new ListOrganizationMembersResponse();
response.members = members.map((member) => this.apiConverter.toOrganizationMember(member));
Expand All @@ -147,16 +148,16 @@ export class OrganizationServiceAPI implements ServiceImpl<typeof OrganizationSe

async updateOrganizationMember(
req: UpdateOrganizationMemberRequest,
context: HandlerContext,
_: HandlerContext,
): Promise<UpdateOrganizationMemberResponse> {
await this.orgService.addOrUpdateMember(
context.user.id,
userId(),
req.organizationId,
req.userId,
this.apiConverter.fromOrgMemberRole(req.role),
);
const member = await this.orgService
.listMembers(context.user.id, req.organizationId)
.listMembers(userId(), req.organizationId)
.then((members) => members.find((member) => member.userId === req.userId));
return new UpdateOrganizationMemberResponse({
member: member && this.apiConverter.toOrganizationMember(member),
Expand All @@ -165,27 +166,27 @@ export class OrganizationServiceAPI implements ServiceImpl<typeof OrganizationSe

async deleteOrganizationMember(
req: DeleteOrganizationMemberRequest,
context: HandlerContext,
_: HandlerContext,
): Promise<DeleteOrganizationMemberResponse> {
await this.orgService.removeOrganizationMember(context.user.id, req.organizationId, req.userId);
await this.orgService.removeOrganizationMember(userId(), req.organizationId, req.userId);
return new DeleteOrganizationMemberResponse();
}

async getOrganizationSettings(
req: GetOrganizationSettingsRequest,
context: HandlerContext,
_: HandlerContext,
): Promise<GetOrganizationSettingsResponse> {
const settings = await this.orgService.getSettings(context.user.id, req.organizationId);
const settings = await this.orgService.getSettings(userId(), req.organizationId);
const response = new GetOrganizationSettingsResponse();
response.settings = this.apiConverter.toOrganizationSettings(settings);
return response;
}

async updateOrganizationSettings(
req: UpdateOrganizationSettingsRequest,
context: HandlerContext,
_: HandlerContext,
): Promise<UpdateOrganizationSettingsResponse> {
const settings = await this.orgService.updateSettings(context.user.id, req.organizationId, {
const settings = await this.orgService.updateSettings(userId(), req.organizationId, {
workspaceSharingDisabled: req.settings?.workspaceSharingDisabled,
defaultWorkspaceImage: req.settings?.defaultWorkspaceImage,
});
Expand Down
59 changes: 35 additions & 24 deletions components/server/src/api/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,20 @@ import { Config } from "../config";
import { grpcServerHandled, grpcServerHandling, grpcServerStarted } from "../prometheus-metrics";
import { SessionHandler } from "../session-handler";
import { UserService } from "../user/user-service";
import { LogContextOptions, runWithLogContext } from "../util/log-context";
import { wrapAsyncGenerator } from "../util/request-context";
import {
RequestContext,
runWithChildContext,
runWithRequestContext,
wrapAsyncGenerator,
} from "../util/request-context";
import { HelloServiceAPI } from "./hello-service-api";
import { OrganizationServiceAPI } from "./organization-service-api";
import { RateLimited } from "./rate-limited";
import { APIStatsService as StatsServiceAPI } from "./stats";
import { APITeamsService as TeamsServiceAPI } from "./teams";
import { APIUserService as UserServiceAPI } from "./user";
import { WorkspaceServiceAPI } from "./workspace-service-api";
import { SubjectId } from "../auth/subject-id";

decorate(injectable(), PublicAPIConverter);

Expand Down Expand Up @@ -127,17 +132,16 @@ export class API {
return {
get(target, prop) {
return (...args: any[]) => {
const logContext: LogContextOptions & {
requestId?: string;
contextTimeMs: number;
grpc_service: string;
grpc_method: string;
} = {
contextTimeMs: performance.now(),
grpc_service,
grpc_method: prop as string,
const connectContext = args[1] as HandlerContext;
const requestContext: RequestContext = {
requestId: v4(),
requestKind: "public-api",
requestMethod: `${grpc_service}/${prop as string}`,
startTime: performance.now(),
signal: connectContext.signal,
};
const withRequestContext = <T>(fn: () => T): T => runWithLogContext("public-api", logContext, fn);

const withRequestContext = <T>(fn: () => T): T => runWithRequestContext(requestContext, fn);

const method = type.methods[prop as string];
if (!method) {
Expand All @@ -161,8 +165,6 @@ export class API {
grpc_type = "bidi_stream";
}

logContext.requestId = v4();

grpcServerStarted.labels(grpc_service, grpc_method, grpc_type).inc();
const stopTimer = grpcServerHandling.startTimer({ grpc_service, grpc_method, grpc_type });
const done = (err?: ConnectError) => {
Expand All @@ -176,7 +178,7 @@ export class API {
if (reason != err && err.code === Code.Internal) {
log.error("public api: unexpected internal error", reason);
err = new ConnectError(
`Oops! Something went wrong. Please quote the request ID ${logContext.requestId} when reaching out to Gitpod Support.`,
`Oops! Something went wrong. Please quote the request ID ${requestContext.requestId} when reaching out to Gitpod Support.`,
Code.Internal,
// pass metadata to preserve the application error
err.metadata,
Expand All @@ -186,8 +188,6 @@ export class API {
throw err;
};

const context = args[1] as HandlerContext;

const rateLimit = async (subjectId: string) => {
const key = `${grpc_service}/${grpc_method}`;
const options = self.config.rateLimits?.[key] || RateLimited.getOptions(target, prop);
Expand All @@ -208,28 +208,39 @@ export class API {
}
};

const apply = async <T>(): Promise<T> => {
const subjectId = await self.verify(context);
await rateLimit(subjectId);
context.user = await self.ensureFgaMigration(subjectId);
const auth = async () => {
const userId = await self.verify(connectContext);
await rateLimit(userId);
await self.ensureFgaMigration(userId);

return SubjectId.fromUserId(userId);
};

const apply = async <T>(): Promise<T> => {
return Reflect.apply(target[prop as any], target, args);
};
if (grpc_type === "unary" || grpc_type === "client_stream") {
return withRequestContext(async () => {
try {
const promise = await apply<Promise<any>>();
const result = await promise;
const subjectId = await auth();
const result = await runWithChildContext({ subjectId }, async () => {
const promise = await apply<Promise<any>>();
return await promise;
});
done();
return result;
} catch (e) {
handleError(e);
}
});
}

let subjectId: SubjectId | undefined = undefined;
return wrapAsyncGenerator(
(async function* () {
try {
subjectId = await auth();

const generator = await apply<AsyncGenerator<any>>();
for await (const item of generator) {
yield item;
Expand All @@ -239,7 +250,7 @@ export class API {
handleError(e);
}
})(),
withRequestContext,
(f) => runWithChildContext({ subjectId }, f),
);
};
},
Expand Down
5 changes: 3 additions & 2 deletions components/server/src/api/workspace-service-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { GetWorkspaceRequest, GetWorkspaceResponse } from "@gitpod/public-api/li
import { inject, injectable } from "inversify";
import { WorkspaceService } from "../workspace/workspace-service";
import { PublicAPIConverter } from "@gitpod/gitpod-protocol/lib/public-api-converter";
import { userId } from "../util/request-context";

@injectable()
export class WorkspaceServiceAPI implements ServiceImpl<typeof WorkspaceServiceInterface> {
Expand All @@ -19,8 +20,8 @@ export class WorkspaceServiceAPI implements ServiceImpl<typeof WorkspaceServiceI
@inject(PublicAPIConverter)
private readonly apiConverter: PublicAPIConverter;

async getWorkspace(req: GetWorkspaceRequest, context: HandlerContext): Promise<GetWorkspaceResponse> {
const info = await this.workspaceService.getWorkspace(context.user.id, req.id);
async getWorkspace(req: GetWorkspaceRequest, _: HandlerContext): Promise<GetWorkspaceResponse> {
const info = await this.workspaceService.getWorkspace(userId(), req.id);
const response = new GetWorkspaceResponse();
response.item = this.apiConverter.toWorkspace(info);
return response;
Expand Down
Loading

0 comments on commit b1546cc

Please sign in to comment.