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 14, 2023
1 parent a5021da commit 04a6032
Show file tree
Hide file tree
Showing 26 changed files with 569 additions and 225 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
3 changes: 3 additions & 0 deletions components/gitpod-protocol/src/messaging/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ export const ErrorCodes = {
// 404 Not Found
NOT_FOUND: 404 as const,

// 408 Request Timeout
REQUEST_TIMEOUT: 408 as const,

// 409 Conflict (e.g. already existing)
CONFLICT: 409 as const,

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
31 changes: 30 additions & 1 deletion components/server/src/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
*/
import { User } from "@gitpod/gitpod-protocol";
import { Request } from "express";
import { IAnalyticsWriter } from "@gitpod/gitpod-protocol/lib/analytics";
import { IAnalyticsWriter, IdentifyMessage, PageMessage, TrackMessage } from "@gitpod/gitpod-protocol/lib/analytics";
import * as crypto from "crypto";
import { clientIp } from "./express-util";
import { ctxTrySubjectId } from "./util/request-context";

export async function trackLogin(user: User, request: Request, authHost: string, analytics: IAnalyticsWriter) {
// make new complete identify call for each login
Expand Down Expand Up @@ -129,3 +130,31 @@ function stripCookie(cookie: string) {
return cookie;
}
}

export class ContextAwareAnalyticsWriter implements IAnalyticsWriter {
constructor(readonly writer: IAnalyticsWriter) {}

identify(msg: IdentifyMessage): void {}

track(msg: TrackMessage): void {}

page(msg: PageMessage): void {
const traceIds = this.getAnalyticsIds();
this.writer.page({
...msg,
userId: msg.userId || traceIds.userId,
subjectId: msg.subjectId || traceIds.subjectId,
});
}

private getAnalyticsIds(): { userId?: string; subjectId?: string } {
const subjectId = ctxTrySubjectId();
if (!subjectId) {
return {};
}
return {
userId: subjectId.userId(),
subjectId: subjectId.toString(),
};
}
}
51 changes: 29 additions & 22 deletions components/server/src/api/auth-provider-service-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,24 @@ import {
DeleteAuthProviderResponse,
} from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb";
import { AuthProviderService } from "../auth/auth-provider-service";
import { AuthProviderEntry, AuthProviderInfo } from "@gitpod/gitpod-protocol";
import { AuthProviderEntry, AuthProviderInfo, User } from "@gitpod/gitpod-protocol";
import { Unauthenticated } from "./unauthenticated";
import { validate as uuidValidate } from "uuid";
import { selectPage } from "./pagination";
import { ctxUserId } from "../util/request-context";
import { UserService } from "../user/user-service";

@injectable()
export class AuthProviderServiceAPI implements ServiceImpl<typeof AuthProviderServiceInterface> {
constructor(
@inject(PublicAPIConverter) private readonly apiConverter: PublicAPIConverter,
@inject(AuthProviderService) private readonly authProviderService: AuthProviderService,
@inject(UserService) private readonly userService: UserService,
) {}

async createAuthProvider(
request: CreateAuthProviderRequest,
context: HandlerContext,
_: HandlerContext,
): Promise<CreateAuthProviderResponse> {
const ownerId = request.owner.case === "ownerId" ? request.owner.value : "";
const organizationId = request.owner.case === "organizationId" ? request.owner.value : "";
Expand All @@ -47,20 +50,20 @@ export class AuthProviderServiceAPI implements ServiceImpl<typeof AuthProviderSe
}

if (organizationId) {
const result = await this.authProviderService.createOrgAuthProvider(context.user.id, {
const result = await this.authProviderService.createOrgAuthProvider(ctxUserId(), {
organizationId,
host: request.host,
ownerId: context.user.id,
ownerId: ctxUserId(),
type: this.apiConverter.fromAuthProviderType(request.type),
clientId: request.oauth2Config?.clientId,
clientSecret: request.oauth2Config?.clientSecret,
});

return new CreateAuthProviderResponse({ authProvider: this.apiConverter.toAuthProvider(result) });
} else {
const result = await this.authProviderService.createAuthProviderOfUser(context.user.id, {
const result = await this.authProviderService.createAuthProviderOfUser(ctxUserId(), {
host: request.host,
ownerId: context.user.id,
ownerId: ctxUserId(),
type: this.apiConverter.fromAuthProviderType(request.type),
clientId: request.oauth2Config?.clientId,
clientSecret: request.oauth2Config?.clientSecret,
Expand All @@ -69,12 +72,12 @@ export class AuthProviderServiceAPI implements ServiceImpl<typeof AuthProviderSe
return new CreateAuthProviderResponse({ authProvider: this.apiConverter.toAuthProvider(result) });
}
}
async getAuthProvider(request: GetAuthProviderRequest, context: HandlerContext): Promise<GetAuthProviderResponse> {
async getAuthProvider(request: GetAuthProviderRequest, _: HandlerContext): Promise<GetAuthProviderResponse> {
if (!request.authProviderId) {
throw new ConnectError("authProviderId is required", Code.InvalidArgument);
}

const authProvider = await this.authProviderService.getAuthProvider(context.user.id, request.authProviderId);
const authProvider = await this.authProviderService.getAuthProvider(ctxUserId(), request.authProviderId);
if (!authProvider) {
throw new ConnectError("Provider not found.", Code.NotFound);
}
Expand All @@ -86,7 +89,7 @@ export class AuthProviderServiceAPI implements ServiceImpl<typeof AuthProviderSe

async listAuthProviders(
request: ListAuthProvidersRequest,
context: HandlerContext,
_: HandlerContext,
): Promise<ListAuthProvidersResponse> {
const target = request.id;
const ownerId = target.case === "userId" ? target.value : "";
Expand All @@ -97,8 +100,8 @@ export class AuthProviderServiceAPI implements ServiceImpl<typeof AuthProviderSe
}

const authProviders = organizationId
? await this.authProviderService.getAuthProvidersOfOrg(context.user.id, organizationId)
: await this.authProviderService.getAuthProvidersOfUser(context.user.id);
? await this.authProviderService.getAuthProvidersOfOrg(ctxUserId(), organizationId)
: await this.authProviderService.getAuthProvidersOfUser(ctxUserId());

const selectedProviders = selectPage(authProviders, request.pagination);
const redacted = selectedProviders.map(AuthProviderEntry.redact.bind(AuthProviderEntry));
Expand All @@ -118,9 +121,13 @@ export class AuthProviderServiceAPI implements ServiceImpl<typeof AuthProviderSe
@Unauthenticated()
async listAuthProviderDescriptions(
request: ListAuthProviderDescriptionsRequest,
context: HandlerContext,
_: HandlerContext,
): Promise<ListAuthProviderDescriptionsResponse> {
const user = context.user;
const userId = ctxUserId();
let user: User | undefined = undefined;
if (userId) {
user = await this.userService.findUserById(userId, userId);
}
const aps = user
? await this.authProviderService.getAuthProviderDescriptions(user)
: await this.authProviderService.getAuthProviderDescriptionsUnauthenticated();
Expand All @@ -135,7 +142,7 @@ export class AuthProviderServiceAPI implements ServiceImpl<typeof AuthProviderSe

async updateAuthProvider(
request: UpdateAuthProviderRequest,
context: HandlerContext,
_: HandlerContext,
): Promise<UpdateAuthProviderResponse> {
if (!request.authProviderId) {
throw new ConnectError("authProviderId is required", Code.InvalidArgument);
Expand All @@ -146,23 +153,23 @@ export class AuthProviderServiceAPI implements ServiceImpl<typeof AuthProviderSe
throw new ConnectError("clientId or clientSecret are required", Code.InvalidArgument);
}

const authProvider = await this.authProviderService.getAuthProvider(context.user.id, request.authProviderId);
const authProvider = await this.authProviderService.getAuthProvider(ctxUserId(), request.authProviderId);
if (!authProvider) {
throw new ConnectError("Provider not found.", Code.NotFound);
}

let entry: AuthProviderEntry;
if (authProvider.organizationId) {
entry = await this.authProviderService.updateOrgAuthProvider(context.user.id, {
entry = await this.authProviderService.updateOrgAuthProvider(ctxUserId(), {
id: request.authProviderId,
organizationId: authProvider.organizationId,
clientId: clientId,
clientSecret: clientSecret,
});
} else {
entry = await this.authProviderService.updateAuthProviderOfUser(context.user.id, {
entry = await this.authProviderService.updateAuthProviderOfUser(ctxUserId(), {
id: request.authProviderId,
ownerId: context.user.id,
ownerId: ctxUserId(),
clientId: clientId,
clientSecret: clientSecret,
});
Expand All @@ -175,25 +182,25 @@ export class AuthProviderServiceAPI implements ServiceImpl<typeof AuthProviderSe

async deleteAuthProvider(
request: DeleteAuthProviderRequest,
context: HandlerContext,
_: HandlerContext,
): Promise<DeleteAuthProviderResponse> {
if (!request.authProviderId) {
throw new ConnectError("authProviderId is required", Code.InvalidArgument);
}

const authProvider = await this.authProviderService.getAuthProvider(context.user.id, request.authProviderId);
const authProvider = await this.authProviderService.getAuthProvider(ctxUserId(), request.authProviderId);
if (!authProvider) {
throw new ConnectError("Provider not found.", Code.NotFound);
}

if (authProvider.organizationId) {
await this.authProviderService.deleteAuthProviderOfOrg(
context.user.id,
ctxUserId(),
authProvider.organizationId,
request.authProviderId,
);
} else {
await this.authProviderService.deleteAuthProviderOfUser(context.user.id, request.authProviderId);
await this.authProviderService.deleteAuthProviderOfUser(ctxUserId(), request.authProviderId);
}

return new DeleteAuthProviderResponse();
Expand Down
25 changes: 17 additions & 8 deletions components/server/src/api/configuration-service-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import {
} from "@gitpod/public-api/lib/gitpod/v1/configuration_pb";
import { PaginationResponse } from "@gitpod/public-api/lib/gitpod/v1/pagination_pb";
import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
import { ctxUserId } from "../util/request-context";
import { UserService } from "../user/user-service";

@injectable()
export class ConfigurationServiceAPI implements ServiceImpl<typeof ConfigurationServiceInterface> {
Expand All @@ -28,11 +30,13 @@ export class ConfigurationServiceAPI implements ServiceImpl<typeof Configuration
private readonly projectService: ProjectsService,
@inject(PublicAPIConverter)
private readonly apiConverter: PublicAPIConverter,
@inject(UserService)
private readonly userService: UserService,
) {}

async createConfiguration(
req: CreateConfigurationRequest,
context: HandlerContext,
_: HandlerContext,
): Promise<CreateConfigurationResponse> {
if (!req.organizationId) {
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "organization_id is required");
Expand All @@ -44,6 +48,11 @@ export class ConfigurationServiceAPI implements ServiceImpl<typeof Configuration
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "name is required");
}

const installer = await this.userService.findUserById(ctxUserId(), ctxUserId());
if (!installer) {
throw new ApplicationError(ErrorCodes.NOT_FOUND, "user not found");
}

const project = await this.projectService.createProject(
{
teamId: req.organizationId,
Expand All @@ -52,35 +61,35 @@ export class ConfigurationServiceAPI implements ServiceImpl<typeof Configuration
appInstallationId: "",
slug: "",
},
context.user,
installer,
);

return new CreateConfigurationResponse({
configuration: this.apiConverter.toConfiguration(project),
});
}

async getConfiguration(req: GetConfigurationRequest, context: HandlerContext) {
async getConfiguration(req: GetConfigurationRequest, _: HandlerContext) {
if (!req.configurationId) {
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "configuration_id is required");
}

const project = await this.projectService.getProject(context.user.id, req.configurationId);
const project = await this.projectService.getProject(ctxUserId(), req.configurationId);

return {
configuration: this.apiConverter.toConfiguration(project),
};
}

async listConfigurations(req: ListConfigurationsRequest, context: HandlerContext) {
async listConfigurations(req: ListConfigurationsRequest, _: HandlerContext) {
if (!req.organizationId) {
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "organization_id is required");
}

const limit = req.pagination?.pageSize || 25;
const offset = (req.pagination?.page ?? 0) * limit;

const { rows, total } = await this.projectService.findProjects(context.user.id, {
const { rows, total } = await this.projectService.findProjects(ctxUserId(), {
searchTerm: req.searchTerm,
orderBy: "name",
orderDir: "ASC",
Expand All @@ -96,12 +105,12 @@ export class ConfigurationServiceAPI implements ServiceImpl<typeof Configuration
});
}

async deleteConfiguration(req: DeleteConfigurationRequest, handler: HandlerContext) {
async deleteConfiguration(req: DeleteConfigurationRequest, _: HandlerContext) {
if (!req.configurationId) {
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "configuration_id is required");
}

await this.projectService.deleteProject(handler.user.id, req.configurationId);
await this.projectService.deleteProject(ctxUserId(), req.configurationId);

return new DeleteConfigurationResponse();
}
Expand Down
13 changes: 0 additions & 13 deletions components/server/src/api/handler-context-augmentation.d.ts

This file was deleted.

20 changes: 11 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,30 @@ import {
SayHelloResponse,
} from "@gitpod/public-api/lib/gitpod/experimental/v1/dummy_pb";
import { injectable } from "inversify";
import { ctxCheckAborted, ctxTrySubjectId } 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 (true) {
ctxCheckAborted();

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 ctxTrySubjectId()?.toString() || "World";
}
Loading

0 comments on commit 04a6032

Please sign in to comment.