Skip to content

Commit afbc36b

Browse files
committed
[server] Introduce RequestContext
1 parent 6c0bb90 commit afbc36b

21 files changed

+464
-177
lines changed

components/gitpod-protocol/src/analytics.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@
66

77
export const IAnalyticsWriter = Symbol("IAnalyticsWriter");
88

9-
type Identity =
10-
| { userId: string | number; anonymousId?: string | number }
11-
| { userId?: string | number; anonymousId: string | number };
9+
type Identity = { userId?: string | number; anonymousId?: string | number; subjectId?: string };
1210

1311
interface Message {
1412
messageId?: string;

components/gitpod-protocol/src/util/logging.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export interface LogContext {
1717
organizationId?: string;
1818
sessionId?: string;
1919
userId?: string;
20+
subjectId?: string;
2021
workspaceId?: string;
2122
instanceId?: string;
2223
}

components/server/src/api/handler-context-augmentation.d.ts

Lines changed: 0 additions & 13 deletions
This file was deleted.

components/server/src/api/hello-service-api.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
*/
66

77
import { HandlerContext, ServiceImpl } from "@connectrpc/connect";
8-
import { User } from "@gitpod/gitpod-protocol";
98
import { HelloService } from "@gitpod/public-api/lib/gitpod/experimental/v1/dummy_connect";
109
import {
1110
LotsOfRepliesRequest,
@@ -14,27 +13,28 @@ import {
1413
SayHelloResponse,
1514
} from "@gitpod/public-api/lib/gitpod/experimental/v1/dummy_pb";
1615
import { injectable } from "inversify";
16+
import { ctx } from "../util/request-context";
1717

1818
@injectable()
1919
export class HelloServiceAPI implements ServiceImpl<typeof HelloService> {
20-
async sayHello(req: SayHelloRequest, context: HandlerContext): Promise<SayHelloResponse> {
20+
async sayHello(req: SayHelloRequest, _: HandlerContext): Promise<SayHelloResponse> {
2121
const response = new SayHelloResponse();
22-
response.reply = "Hello " + this.getSubject(context);
22+
response.reply = "Hello " + getSubject();
2323
return response;
2424
}
25-
async *lotsOfReplies(req: LotsOfRepliesRequest, context: HandlerContext): AsyncGenerator<LotsOfRepliesResponse> {
25+
async *lotsOfReplies(req: LotsOfRepliesRequest, _: HandlerContext): AsyncGenerator<LotsOfRepliesResponse> {
2626
let count = req.previousCount || 0;
27-
while (!context.signal.aborted) {
27+
while (!ctx().signal.aborted) {
2828
const response = new LotsOfRepliesResponse();
29-
response.reply = `Hello ${this.getSubject(context)} ${count}`;
29+
response.reply = `Hello ${getSubject()} ${count}`;
3030
response.count = count;
3131
yield response;
3232
count++;
3333
await new Promise((resolve) => setTimeout(resolve, 30000));
3434
}
3535
}
36+
}
3637

37-
private getSubject(context: HandlerContext): string {
38-
return User.getName(context.user) || "World";
39-
}
38+
function getSubject(): string {
39+
return ctx().subjectId?.toString() || "World";
4040
}

components/server/src/api/organization-service-api.ts

Lines changed: 32 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ import {
3939
import { PublicAPIConverter } from "@gitpod/gitpod-protocol/lib/public-api-converter";
4040
import { OrganizationService } from "../orgs/organization-service";
4141
import { PaginationResponse } from "@gitpod/public-api/lib/gitpod/v1/pagination_pb";
42+
import { ctx, userId } from "../util/request-context";
43+
import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
4244

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

52-
async createOrganization(
53-
req: CreateOrganizationRequest,
54-
context: HandlerContext,
55-
): Promise<CreateOrganizationResponse> {
56-
const org = await this.orgService.createOrganization(context.user.id, req.name);
54+
async createOrganization(req: CreateOrganizationRequest, _: HandlerContext): Promise<CreateOrganizationResponse> {
55+
// TODO(gpl) This mimicks the current behavior of adding the subjectId as owner
56+
const ownerId = ctx().subjectId?.userId();
57+
if (!ownerId) {
58+
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "No userId available");
59+
}
60+
const org = await this.orgService.createOrganization(ownerId, req.name);
5761
const response = new CreateOrganizationResponse();
5862
response.organization = this.apiConverter.toOrganization(org);
5963
return response;
6064
}
6165

62-
async getOrganization(req: GetOrganizationRequest, context: HandlerContext): Promise<GetOrganizationResponse> {
63-
const org = await this.orgService.getOrganization(context.user.id, req.organizationId);
66+
async getOrganization(req: GetOrganizationRequest, _: HandlerContext): Promise<GetOrganizationResponse> {
67+
const org = await this.orgService.getOrganization(userId(), req.organizationId);
6468
const response = new GetOrganizationResponse();
6569
response.organization = this.apiConverter.toOrganization(org);
6670
return response;
@@ -70,7 +74,7 @@ export class OrganizationServiceAPI implements ServiceImpl<typeof OrganizationSe
7074
req: UpdateOrganizationRequest,
7175
context: HandlerContext,
7276
): Promise<UpdateOrganizationResponse> {
73-
const org = await this.orgService.updateOrganization(context.user.id, req.organizationId, {
77+
const org = await this.orgService.updateOrganization(userId(), req.organizationId, {
7478
name: req.name,
7579
});
7680
return new UpdateOrganizationResponse({
@@ -83,7 +87,7 @@ export class OrganizationServiceAPI implements ServiceImpl<typeof OrganizationSe
8387
context: HandlerContext,
8488
): Promise<ListOrganizationsResponse> {
8589
const orgs = await this.orgService.listOrganizations(
86-
context.user.id,
90+
userId(),
8791
{
8892
limit: req.pagination?.pageSize || 100,
8993
offset: (req.pagination?.page || 0) * (req.pagination?.pageSize || 0),
@@ -97,46 +101,43 @@ export class OrganizationServiceAPI implements ServiceImpl<typeof OrganizationSe
97101
return response;
98102
}
99103

100-
async deleteOrganization(
101-
req: DeleteOrganizationRequest,
102-
context: HandlerContext,
103-
): Promise<DeleteOrganizationResponse> {
104-
await this.orgService.deleteOrganization(context.user.id, req.organizationId);
104+
async deleteOrganization(req: DeleteOrganizationRequest, _: HandlerContext): Promise<DeleteOrganizationResponse> {
105+
await this.orgService.deleteOrganization(userId(), req.organizationId);
105106
return new DeleteOrganizationResponse();
106107
}
107108

108109
async getOrganizationInvitation(
109110
req: GetOrganizationInvitationRequest,
110-
context: HandlerContext,
111+
_: HandlerContext,
111112
): Promise<GetOrganizationInvitationResponse> {
112-
const invitation = await this.orgService.getOrCreateInvite(context.user.id, req.organizationId);
113+
const invitation = await this.orgService.getOrCreateInvite(userId(), req.organizationId);
113114
const response = new GetOrganizationInvitationResponse();
114115
response.invitationId = invitation.id;
115116
return response;
116117
}
117118

118-
async joinOrganization(req: JoinOrganizationRequest, context: HandlerContext): Promise<JoinOrganizationResponse> {
119-
const orgId = await this.orgService.joinOrganization(context.user.id, req.invitationId);
119+
async joinOrganization(req: JoinOrganizationRequest, _: HandlerContext): Promise<JoinOrganizationResponse> {
120+
const orgId = await this.orgService.joinOrganization(userId(), req.invitationId);
120121
const result = new JoinOrganizationResponse();
121122
result.organizationId = orgId;
122123
return result;
123124
}
124125

125126
async resetOrganizationInvitation(
126127
req: ResetOrganizationInvitationRequest,
127-
context: HandlerContext,
128+
_: HandlerContext,
128129
): Promise<ResetOrganizationInvitationResponse> {
129-
const inviteId = await this.orgService.resetInvite(context.user.id, req.organizationId);
130+
const inviteId = await this.orgService.resetInvite(userId(), req.organizationId);
130131
const result = new ResetOrganizationInvitationResponse();
131132
result.invitationId = inviteId.id;
132133
return result;
133134
}
134135

135136
async listOrganizationMembers(
136137
req: ListOrganizationMembersRequest,
137-
context: HandlerContext,
138+
_: HandlerContext,
138139
): Promise<ListOrganizationMembersResponse> {
139-
const members = await this.orgService.listMembers(context.user.id, req.organizationId);
140+
const members = await this.orgService.listMembers(userId(), req.organizationId);
140141
//TODO pagination
141142
const response = new ListOrganizationMembersResponse();
142143
response.members = members.map((member) => this.apiConverter.toOrganizationMember(member));
@@ -147,16 +148,16 @@ export class OrganizationServiceAPI implements ServiceImpl<typeof OrganizationSe
147148

148149
async updateOrganizationMember(
149150
req: UpdateOrganizationMemberRequest,
150-
context: HandlerContext,
151+
_: HandlerContext,
151152
): Promise<UpdateOrganizationMemberResponse> {
152153
await this.orgService.addOrUpdateMember(
153-
context.user.id,
154+
userId(),
154155
req.organizationId,
155156
req.userId,
156157
this.apiConverter.fromOrgMemberRole(req.role),
157158
);
158159
const member = await this.orgService
159-
.listMembers(context.user.id, req.organizationId)
160+
.listMembers(userId(), req.organizationId)
160161
.then((members) => members.find((member) => member.userId === req.userId));
161162
return new UpdateOrganizationMemberResponse({
162163
member: member && this.apiConverter.toOrganizationMember(member),
@@ -165,27 +166,27 @@ export class OrganizationServiceAPI implements ServiceImpl<typeof OrganizationSe
165166

166167
async deleteOrganizationMember(
167168
req: DeleteOrganizationMemberRequest,
168-
context: HandlerContext,
169+
_: HandlerContext,
169170
): Promise<DeleteOrganizationMemberResponse> {
170-
await this.orgService.removeOrganizationMember(context.user.id, req.organizationId, req.userId);
171+
await this.orgService.removeOrganizationMember(userId(), req.organizationId, req.userId);
171172
return new DeleteOrganizationMemberResponse();
172173
}
173174

174175
async getOrganizationSettings(
175176
req: GetOrganizationSettingsRequest,
176-
context: HandlerContext,
177+
_: HandlerContext,
177178
): Promise<GetOrganizationSettingsResponse> {
178-
const settings = await this.orgService.getSettings(context.user.id, req.organizationId);
179+
const settings = await this.orgService.getSettings(userId(), req.organizationId);
179180
const response = new GetOrganizationSettingsResponse();
180181
response.settings = this.apiConverter.toOrganizationSettings(settings);
181182
return response;
182183
}
183184

184185
async updateOrganizationSettings(
185186
req: UpdateOrganizationSettingsRequest,
186-
context: HandlerContext,
187+
_: HandlerContext,
187188
): Promise<UpdateOrganizationSettingsResponse> {
188-
const settings = await this.orgService.updateSettings(context.user.id, req.organizationId, {
189+
const settings = await this.orgService.updateSettings(userId(), req.organizationId, {
189190
workspaceSharingDisabled: req.settings?.workspaceSharingDisabled,
190191
defaultWorkspaceImage: req.settings?.defaultWorkspaceImage,
191192
});

components/server/src/api/server.ts

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,20 @@ import { Config } from "../config";
3030
import { grpcServerHandled, grpcServerHandling, grpcServerStarted } from "../prometheus-metrics";
3131
import { SessionHandler } from "../session-handler";
3232
import { UserService } from "../user/user-service";
33-
import { LogContextOptions, runWithLogContext } from "../util/log-context";
34-
import { wrapAsyncGenerator } from "../util/request-context";
33+
import {
34+
RequestContext,
35+
runWithChildContext,
36+
runWithRequestContext,
37+
wrapAsyncGenerator,
38+
} from "../util/request-context";
3539
import { HelloServiceAPI } from "./hello-service-api";
3640
import { OrganizationServiceAPI } from "./organization-service-api";
3741
import { RateLimited } from "./rate-limited";
3842
import { APIStatsService as StatsServiceAPI } from "./stats";
3943
import { APITeamsService as TeamsServiceAPI } from "./teams";
4044
import { APIUserService as UserServiceAPI } from "./user";
4145
import { WorkspaceServiceAPI } from "./workspace-service-api";
46+
import { SubjectId } from "../auth/subject-id";
4247

4348
decorate(injectable(), PublicAPIConverter);
4449

@@ -127,17 +132,16 @@ export class API {
127132
return {
128133
get(target, prop) {
129134
return (...args: any[]) => {
130-
const logContext: LogContextOptions & {
131-
requestId?: string;
132-
contextTimeMs: number;
133-
grpc_service: string;
134-
grpc_method: string;
135-
} = {
136-
contextTimeMs: performance.now(),
137-
grpc_service,
138-
grpc_method: prop as string,
135+
const connectContext = args[1] as HandlerContext;
136+
const requestContext: RequestContext = {
137+
requestId: v4(),
138+
requestKind: "public-api",
139+
requestMethod: `${grpc_service}/${prop as string}`,
140+
startTime: performance.now(),
141+
signal: connectContext.signal,
139142
};
140-
const withRequestContext = <T>(fn: () => T): T => runWithLogContext("public-api", logContext, fn);
143+
144+
const withRequestContext = <T>(fn: () => T): T => runWithRequestContext(requestContext, fn);
141145

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

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

189-
const context = args[1] as HandlerContext;
190-
191191
const rateLimit = async (subjectId: string) => {
192192
const key = `${grpc_service}/${grpc_method}`;
193193
const options = self.config.rateLimits?.[key] || RateLimited.getOptions(target, prop);
@@ -208,28 +208,39 @@ export class API {
208208
}
209209
};
210210

211-
const apply = async <T>(): Promise<T> => {
212-
const subjectId = await self.verify(context);
213-
await rateLimit(subjectId);
214-
context.user = await self.ensureFgaMigration(subjectId);
211+
const auth = async () => {
212+
const userId = await self.verify(connectContext);
213+
await rateLimit(userId);
214+
await self.ensureFgaMigration(userId);
215+
216+
return SubjectId.fromUserId(userId);
217+
};
215218

219+
const apply = async <T>(): Promise<T> => {
216220
return Reflect.apply(target[prop as any], target, args);
217221
};
218222
if (grpc_type === "unary" || grpc_type === "client_stream") {
219223
return withRequestContext(async () => {
220224
try {
221-
const promise = await apply<Promise<any>>();
222-
const result = await promise;
225+
const subjectId = await auth();
226+
const result = await runWithChildContext({ subjectId }, async () => {
227+
const promise = await apply<Promise<any>>();
228+
return await promise;
229+
});
223230
done();
224231
return result;
225232
} catch (e) {
226233
handleError(e);
227234
}
228235
});
229236
}
237+
238+
let subjectId: SubjectId | undefined = undefined;
230239
return wrapAsyncGenerator(
231240
(async function* () {
232241
try {
242+
subjectId = await auth();
243+
233244
const generator = await apply<AsyncGenerator<any>>();
234245
for await (const item of generator) {
235246
yield item;
@@ -239,7 +250,7 @@ export class API {
239250
handleError(e);
240251
}
241252
})(),
242-
withRequestContext,
253+
(f) => runWithChildContext({ subjectId }, f),
243254
);
244255
};
245256
},

components/server/src/api/workspace-service-api.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { GetWorkspaceRequest, GetWorkspaceResponse } from "@gitpod/public-api/li
1010
import { inject, injectable } from "inversify";
1111
import { WorkspaceService } from "../workspace/workspace-service";
1212
import { PublicAPIConverter } from "@gitpod/gitpod-protocol/lib/public-api-converter";
13+
import { userId } from "../util/request-context";
1314

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

22-
async getWorkspace(req: GetWorkspaceRequest, context: HandlerContext): Promise<GetWorkspaceResponse> {
23-
const info = await this.workspaceService.getWorkspace(context.user.id, req.id);
23+
async getWorkspace(req: GetWorkspaceRequest, _: HandlerContext): Promise<GetWorkspaceResponse> {
24+
const info = await this.workspaceService.getWorkspace(userId(), req.id);
2425
const response = new GetWorkspaceResponse();
2526
response.item = this.apiConverter.toWorkspace(info);
2627
return response;

0 commit comments

Comments
 (0)