Skip to content

Commit 453392b

Browse files
authored
[server] Introduce RequestContext (#19023)
* [server] Introduce RequestContext * [server] Improve (repeated) Docker build times * fix AuthProviderService.createAuthProvider
1 parent feb9fa2 commit 453392b

29 files changed

+619
-256
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/messaging/error.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ export const ErrorCodes = {
6565
// 404 Not Found
6666
NOT_FOUND: 404 as const,
6767

68+
// 408 Request Timeout
69+
REQUEST_TIMEOUT: 408 as const,
70+
6871
// 409 Conflict (e.g. already existing)
6972
CONFLICT: 409 as const,
7073

components/gitpod-protocol/src/public-api-converter.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,9 @@ export class PublicAPIConverter {
201201
if (reason.code === ErrorCodes.INTERNAL_SERVER_ERROR) {
202202
return new ConnectError(reason.message, Code.Internal, metadata, undefined, reason);
203203
}
204+
if (reason.code === ErrorCodes.REQUEST_TIMEOUT) {
205+
return new ConnectError(reason.message, Code.Canceled, metadata, undefined, reason);
206+
}
204207
return new ConnectError(reason.message, Code.InvalidArgument, metadata, undefined, reason);
205208
}
206209
return ConnectError.from(reason, Code.Internal);

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/leeway.Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33
# See License.AGPL.txt in the project root for license information.
44

55
FROM node:18.17.1-slim as builder
6-
COPY components-server--app /installer/
76

87
# Install Python, make, gcc and g++ for node-gyp
98
RUN apt-get update && \
109
apt-get install -y python3 make gcc g++ && \
1110
apt-get clean && \
1211
rm -rf /var/lib/apt/lists/*
1312

13+
COPY components-server--app /installer/
14+
1415
WORKDIR /app
1516
RUN /installer/install.sh
1617

components/server/src/analytics.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
*/
66
import { User } from "@gitpod/gitpod-protocol";
77
import { Request } from "express";
8-
import { IAnalyticsWriter } from "@gitpod/gitpod-protocol/lib/analytics";
8+
import { IAnalyticsWriter, IdentifyMessage, PageMessage, TrackMessage } from "@gitpod/gitpod-protocol/lib/analytics";
99
import * as crypto from "crypto";
1010
import { clientIp } from "./express-util";
11+
import { ctxTrySubjectId } from "./util/request-context";
1112

1213
export async function trackLogin(user: User, request: Request, authHost: string, analytics: IAnalyticsWriter) {
1314
// make new complete identify call for each login
@@ -129,3 +130,35 @@ function stripCookie(cookie: string) {
129130
return cookie;
130131
}
131132
}
133+
134+
export class ContextAwareAnalyticsWriter implements IAnalyticsWriter {
135+
constructor(readonly writer: IAnalyticsWriter) {}
136+
137+
identify(msg: IdentifyMessage): void {
138+
this.writer.identify(msg);
139+
}
140+
141+
track(msg: TrackMessage): void {
142+
this.writer.track(msg);
143+
}
144+
145+
page(msg: PageMessage): void {
146+
const traceIds = this.getAnalyticsIds();
147+
this.writer.page({
148+
...msg,
149+
userId: msg.userId || traceIds.userId,
150+
subjectId: msg.subjectId || traceIds.subjectId,
151+
});
152+
}
153+
154+
private getAnalyticsIds(): { userId?: string; subjectId?: string } {
155+
const subjectId = ctxTrySubjectId();
156+
if (!subjectId) {
157+
return {};
158+
}
159+
return {
160+
userId: subjectId.userId(),
161+
subjectId: subjectId.toString(),
162+
};
163+
}
164+
}

components/server/src/api/auth-provider-service-api.ts

Lines changed: 33 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -23,44 +23,46 @@ import {
2323
DeleteAuthProviderResponse,
2424
} from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb";
2525
import { AuthProviderService } from "../auth/auth-provider-service";
26-
import { AuthProviderEntry, AuthProviderInfo } from "@gitpod/gitpod-protocol";
26+
import { AuthProviderEntry, AuthProviderInfo, User } from "@gitpod/gitpod-protocol";
2727
import { Unauthenticated } from "./unauthenticated";
2828
import { validate as uuidValidate } from "uuid";
2929
import { selectPage } from "./pagination";
30+
import { ctxUserId } from "../util/request-context";
31+
import { UserService } from "../user/user-service";
3032

3133
@injectable()
3234
export class AuthProviderServiceAPI implements ServiceImpl<typeof AuthProviderServiceInterface> {
3335
constructor(
3436
@inject(PublicAPIConverter) private readonly apiConverter: PublicAPIConverter,
3537
@inject(AuthProviderService) private readonly authProviderService: AuthProviderService,
38+
@inject(UserService) private readonly userService: UserService,
3639
) {}
3740

3841
async createAuthProvider(
3942
request: CreateAuthProviderRequest,
40-
context: HandlerContext,
43+
_: HandlerContext,
4144
): Promise<CreateAuthProviderResponse> {
42-
const ownerId = request.owner.case === "ownerId" ? request.owner.value : "";
4345
const organizationId = request.owner.case === "organizationId" ? request.owner.value : "";
4446

45-
if (!uuidValidate(organizationId) && !uuidValidate(ownerId)) {
46-
throw new ConnectError("organizationId or ownerId is required", Code.InvalidArgument);
47-
}
48-
4947
if (organizationId) {
50-
const result = await this.authProviderService.createOrgAuthProvider(context.user.id, {
48+
if (!uuidValidate(organizationId)) {
49+
throw new ConnectError("organizationId is required", Code.InvalidArgument);
50+
}
51+
52+
const result = await this.authProviderService.createOrgAuthProvider(ctxUserId(), {
5153
organizationId,
5254
host: request.host,
53-
ownerId: context.user.id,
55+
ownerId: ctxUserId(),
5456
type: this.apiConverter.fromAuthProviderType(request.type),
5557
clientId: request.oauth2Config?.clientId,
5658
clientSecret: request.oauth2Config?.clientSecret,
5759
});
5860

5961
return new CreateAuthProviderResponse({ authProvider: this.apiConverter.toAuthProvider(result) });
6062
} else {
61-
const result = await this.authProviderService.createAuthProviderOfUser(context.user.id, {
63+
const result = await this.authProviderService.createAuthProviderOfUser(ctxUserId(), {
6264
host: request.host,
63-
ownerId: context.user.id,
65+
ownerId: ctxUserId(),
6466
type: this.apiConverter.fromAuthProviderType(request.type),
6567
clientId: request.oauth2Config?.clientId,
6668
clientSecret: request.oauth2Config?.clientSecret,
@@ -69,12 +71,12 @@ export class AuthProviderServiceAPI implements ServiceImpl<typeof AuthProviderSe
6971
return new CreateAuthProviderResponse({ authProvider: this.apiConverter.toAuthProvider(result) });
7072
}
7173
}
72-
async getAuthProvider(request: GetAuthProviderRequest, context: HandlerContext): Promise<GetAuthProviderResponse> {
74+
async getAuthProvider(request: GetAuthProviderRequest, _: HandlerContext): Promise<GetAuthProviderResponse> {
7375
if (!request.authProviderId) {
7476
throw new ConnectError("authProviderId is required", Code.InvalidArgument);
7577
}
7678

77-
const authProvider = await this.authProviderService.getAuthProvider(context.user.id, request.authProviderId);
79+
const authProvider = await this.authProviderService.getAuthProvider(ctxUserId(), request.authProviderId);
7880
if (!authProvider) {
7981
throw new ConnectError("Provider not found.", Code.NotFound);
8082
}
@@ -84,10 +86,7 @@ export class AuthProviderServiceAPI implements ServiceImpl<typeof AuthProviderSe
8486
});
8587
}
8688

87-
async listAuthProviders(
88-
request: ListAuthProvidersRequest,
89-
context: HandlerContext,
90-
): Promise<ListAuthProvidersResponse> {
89+
async listAuthProviders(request: ListAuthProvidersRequest, _: HandlerContext): Promise<ListAuthProvidersResponse> {
9190
const target = request.id;
9291
const ownerId = target.case === "userId" ? target.value : "";
9392
const organizationId = target.case === "organizationId" ? target.value : "";
@@ -97,8 +96,8 @@ export class AuthProviderServiceAPI implements ServiceImpl<typeof AuthProviderSe
9796
}
9897

9998
const authProviders = organizationId
100-
? await this.authProviderService.getAuthProvidersOfOrg(context.user.id, organizationId)
101-
: await this.authProviderService.getAuthProvidersOfUser(context.user.id);
99+
? await this.authProviderService.getAuthProvidersOfOrg(ctxUserId(), organizationId)
100+
: await this.authProviderService.getAuthProvidersOfUser(ctxUserId());
102101

103102
const selectedProviders = selectPage(authProviders, request.pagination);
104103
const redacted = selectedProviders.map(AuthProviderEntry.redact.bind(AuthProviderEntry));
@@ -118,9 +117,13 @@ export class AuthProviderServiceAPI implements ServiceImpl<typeof AuthProviderSe
118117
@Unauthenticated()
119118
async listAuthProviderDescriptions(
120119
request: ListAuthProviderDescriptionsRequest,
121-
context: HandlerContext,
120+
_: HandlerContext,
122121
): Promise<ListAuthProviderDescriptionsResponse> {
123-
const user = context.user;
122+
const userId = ctxUserId();
123+
let user: User | undefined = undefined;
124+
if (userId) {
125+
user = await this.userService.findUserById(userId, userId);
126+
}
124127
const aps = user
125128
? await this.authProviderService.getAuthProviderDescriptions(user)
126129
: await this.authProviderService.getAuthProviderDescriptionsUnauthenticated();
@@ -135,7 +138,7 @@ export class AuthProviderServiceAPI implements ServiceImpl<typeof AuthProviderSe
135138

136139
async updateAuthProvider(
137140
request: UpdateAuthProviderRequest,
138-
context: HandlerContext,
141+
_: HandlerContext,
139142
): Promise<UpdateAuthProviderResponse> {
140143
if (!request.authProviderId) {
141144
throw new ConnectError("authProviderId is required", Code.InvalidArgument);
@@ -146,23 +149,23 @@ export class AuthProviderServiceAPI implements ServiceImpl<typeof AuthProviderSe
146149
throw new ConnectError("clientId or clientSecret are required", Code.InvalidArgument);
147150
}
148151

149-
const authProvider = await this.authProviderService.getAuthProvider(context.user.id, request.authProviderId);
152+
const authProvider = await this.authProviderService.getAuthProvider(ctxUserId(), request.authProviderId);
150153
if (!authProvider) {
151154
throw new ConnectError("Provider not found.", Code.NotFound);
152155
}
153156

154157
let entry: AuthProviderEntry;
155158
if (authProvider.organizationId) {
156-
entry = await this.authProviderService.updateOrgAuthProvider(context.user.id, {
159+
entry = await this.authProviderService.updateOrgAuthProvider(ctxUserId(), {
157160
id: request.authProviderId,
158161
organizationId: authProvider.organizationId,
159162
clientId: clientId,
160163
clientSecret: clientSecret,
161164
});
162165
} else {
163-
entry = await this.authProviderService.updateAuthProviderOfUser(context.user.id, {
166+
entry = await this.authProviderService.updateAuthProviderOfUser(ctxUserId(), {
164167
id: request.authProviderId,
165-
ownerId: context.user.id,
168+
ownerId: ctxUserId(),
166169
clientId: clientId,
167170
clientSecret: clientSecret,
168171
});
@@ -175,25 +178,25 @@ export class AuthProviderServiceAPI implements ServiceImpl<typeof AuthProviderSe
175178

176179
async deleteAuthProvider(
177180
request: DeleteAuthProviderRequest,
178-
context: HandlerContext,
181+
_: HandlerContext,
179182
): Promise<DeleteAuthProviderResponse> {
180183
if (!request.authProviderId) {
181184
throw new ConnectError("authProviderId is required", Code.InvalidArgument);
182185
}
183186

184-
const authProvider = await this.authProviderService.getAuthProvider(context.user.id, request.authProviderId);
187+
const authProvider = await this.authProviderService.getAuthProvider(ctxUserId(), request.authProviderId);
185188
if (!authProvider) {
186189
throw new ConnectError("Provider not found.", Code.NotFound);
187190
}
188191

189192
if (authProvider.organizationId) {
190193
await this.authProviderService.deleteAuthProviderOfOrg(
191-
context.user.id,
194+
ctxUserId(),
192195
authProvider.organizationId,
193196
request.authProviderId,
194197
);
195198
} else {
196-
await this.authProviderService.deleteAuthProviderOfUser(context.user.id, request.authProviderId);
199+
await this.authProviderService.deleteAuthProviderOfUser(ctxUserId(), request.authProviderId);
197200
}
198201

199202
return new DeleteAuthProviderResponse();

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

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import {
2020
} from "@gitpod/public-api/lib/gitpod/v1/configuration_pb";
2121
import { PaginationResponse } from "@gitpod/public-api/lib/gitpod/v1/pagination_pb";
2222
import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
23+
import { ctxUserId } from "../util/request-context";
24+
import { UserService } from "../user/user-service";
2325

2426
@injectable()
2527
export class ConfigurationServiceAPI implements ServiceImpl<typeof ConfigurationServiceInterface> {
@@ -28,11 +30,13 @@ export class ConfigurationServiceAPI implements ServiceImpl<typeof Configuration
2830
private readonly projectService: ProjectsService,
2931
@inject(PublicAPIConverter)
3032
private readonly apiConverter: PublicAPIConverter,
33+
@inject(UserService)
34+
private readonly userService: UserService,
3135
) {}
3236

3337
async createConfiguration(
3438
req: CreateConfigurationRequest,
35-
context: HandlerContext,
39+
_: HandlerContext,
3640
): Promise<CreateConfigurationResponse> {
3741
if (!req.organizationId) {
3842
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "organization_id is required");
@@ -41,6 +45,11 @@ export class ConfigurationServiceAPI implements ServiceImpl<typeof Configuration
4145
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "clone_url is required");
4246
}
4347

48+
const installer = await this.userService.findUserById(ctxUserId(), ctxUserId());
49+
if (!installer) {
50+
throw new ApplicationError(ErrorCodes.NOT_FOUND, "user not found");
51+
}
52+
4453
const project = await this.projectService.createProject(
4554
{
4655
teamId: req.organizationId,
@@ -49,27 +58,27 @@ export class ConfigurationServiceAPI implements ServiceImpl<typeof Configuration
4958
appInstallationId: "",
5059
slug: "",
5160
},
52-
context.user,
61+
installer,
5362
);
5463

5564
return new CreateConfigurationResponse({
5665
configuration: this.apiConverter.toConfiguration(project),
5766
});
5867
}
5968

60-
async getConfiguration(req: GetConfigurationRequest, context: HandlerContext) {
69+
async getConfiguration(req: GetConfigurationRequest, _: HandlerContext) {
6170
if (!req.configurationId) {
6271
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "configuration_id is required");
6372
}
6473

65-
const project = await this.projectService.getProject(context.user.id, req.configurationId);
74+
const project = await this.projectService.getProject(ctxUserId(), req.configurationId);
6675

6776
return {
6877
configuration: this.apiConverter.toConfiguration(project),
6978
};
7079
}
7180

72-
async listConfigurations(req: ListConfigurationsRequest, context: HandlerContext) {
81+
async listConfigurations(req: ListConfigurationsRequest, _: HandlerContext) {
7382
if (!req.organizationId) {
7483
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "organization_id is required");
7584
}
@@ -78,7 +87,7 @@ export class ConfigurationServiceAPI implements ServiceImpl<typeof Configuration
7887
const currentPage = req.pagination?.page ?? 1;
7988
const offset = currentPage > 1 ? (currentPage - 1) * limit : 0;
8089

81-
const { rows, total } = await this.projectService.findProjects(context.user.id, {
90+
const { rows, total } = await this.projectService.findProjects(ctxUserId(), {
8291
organizationId: req.organizationId,
8392
searchTerm: req.searchTerm,
8493
orderBy: "name",
@@ -96,12 +105,12 @@ export class ConfigurationServiceAPI implements ServiceImpl<typeof Configuration
96105
});
97106
}
98107

99-
async deleteConfiguration(req: DeleteConfigurationRequest, handler: HandlerContext) {
108+
async deleteConfiguration(req: DeleteConfigurationRequest, _: HandlerContext) {
100109
if (!req.configurationId) {
101110
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "configuration_id is required");
102111
}
103112

104-
await this.projectService.deleteProject(handler.user.id, req.configurationId);
113+
await this.projectService.deleteProject(ctxUserId(), req.configurationId);
105114

106115
return new DeleteConfigurationResponse();
107116
}

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

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

0 commit comments

Comments
 (0)