From 79083ce2b73712b9041094f7b6aa6c2d9e6e8dd1 Mon Sep 17 00:00:00 2001 From: Alex Tugarev Date: Wed, 4 May 2022 15:18:21 +0000 Subject: [PATCH] [server] blocklist repositories --- .../ee/src/workspace/gitpod-server-impl.ts | 16 +++++---- components/server/src/config.ts | 6 ++++ components/server/src/user/user-service.ts | 10 ++++++ .../server/src/workspace/workspace-starter.ts | 33 +++++++++++++++++++ 4 files changed, 58 insertions(+), 7 deletions(-) diff --git a/components/server/ee/src/workspace/gitpod-server-impl.ts b/components/server/ee/src/workspace/gitpod-server-impl.ts index 9ac2d9fe574f39..b9f57f19d0ca44 100644 --- a/components/server/ee/src/workspace/gitpod-server-impl.ts +++ b/components/server/ee/src/workspace/gitpod-server-impl.ts @@ -232,7 +232,10 @@ export class GitpodServerEEImpl extends GitpodServerImpl { const result = await this.eligibilityService.mayStartWorkspace(user, new Date(), runningInstances); if (!result.enoughCredits) { - throw new ResponseError(ErrorCodes.NOT_ENOUGH_CREDIT, `Not enough monthly workspace hours. Please upgrade your account to get more hours for your workspaces.`); + throw new ResponseError( + ErrorCodes.NOT_ENOUGH_CREDIT, + `Not enough monthly workspace hours. Please upgrade your account to get more hours for your workspaces.`, + ); } if (!!result.hitParallelWorkspaceLimit) { throw new ResponseError( @@ -565,14 +568,13 @@ export class GitpodServerEEImpl extends GitpodServerImpl { await this.guardAdminAccess("adminBlockUser", { req }, Permission.ADMIN_USERS); - const target = await this.userDB.findUserById(req.id); - if (!target) { + let targetUser; + try { + targetUser = await this.userService.blockUser(req.id, req.blocked); + } catch (error) { throw new ResponseError(ErrorCodes.NOT_FOUND, "not found"); } - target.blocked = !!req.blocked; - await this.userDB.storeUser(target); - const workspaceDb = this.workspaceDb.trace(ctx); const workspaces = await workspaceDb.findWorkspacesByUser(req.id); const isDefined = (x: T | undefined): x is T => x !== undefined; @@ -584,7 +586,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl { // For some reason, returning the result of `this.userDB.storeUser(target)` does not work. The response never arrives the caller. // Returning `target` instead (which should be equivalent). - return this.censorUser(target); + return this.censorUser(targetUser); } async adminDeleteUser(ctx: TraceContext, userId: string): Promise { diff --git a/components/server/src/config.ts b/components/server/src/config.ts index d936304276f3b9..2f9c7c9d68005b 100644 --- a/components/server/src/config.ts +++ b/components/server/src/config.ts @@ -152,6 +152,12 @@ export interface ConfigSerialized { * Key '*' specifies the default rate limit for a cloneURL, unless overriden by a specific cloneURL. */ prebuildLimiter: { [cloneURL: string]: number } & { "*": number }; + + /** + * List of repositories not allowed to be used for workspace starts. + * `blockUser` attribute to control handling of the user's account. + */ + blockedRepositories?: { urlRegExp: string; blockUser: boolean }[]; } export namespace ConfigFile { diff --git a/components/server/src/user/user-service.ts b/components/server/src/user/user-service.ts index 23929112f82aed..e35a97cc5792bd 100644 --- a/components/server/src/user/user-service.ts +++ b/components/server/src/user/user-service.ts @@ -269,6 +269,16 @@ export class UserService { return false; } + async blockUser(targetUserId: string, block: boolean): Promise { + const target = await this.userDb.findUserById(targetUserId); + if (!target) { + throw new Error("Not found."); + } + + target.blocked = !!block; + return await this.userDb.storeUser(target); + } + async findUserForLogin(params: { candidate: Identity }) { let user = await this.userDb.findUserByIdentity(params.candidate); return user; diff --git a/components/server/src/workspace/workspace-starter.ts b/components/server/src/workspace/workspace-starter.ts index a953d9f55ab052..76ffeb47ee8375 100644 --- a/components/server/src/workspace/workspace-starter.ts +++ b/components/server/src/workspace/workspace-starter.ts @@ -219,6 +219,8 @@ export class WorkspaceStarter { options = options || {}; try { + await this.checkBlockedRepository(user, workspace.contextURL); + // Some workspaces do not have an image source. // Workspaces without image source are not only legacy, but also happened due to what looks like a bug. // Whenever a such a workspace is re-started we'll give it an image source now. This is in line with how this thing used to work. @@ -332,6 +334,37 @@ export class WorkspaceStarter { } } + protected blockedRepositories: { urlRegExp: RegExp; blockUser: boolean }[] | undefined; + protected getBlockedRepositories() { + if (!this.blockedRepositories) { + this.blockedRepositories = []; + if (this.config.blockedRepositories) { + for (const { blockUser, urlRegExp } of this.config.blockedRepositories) { + this.blockedRepositories.push({ + blockUser, + urlRegExp: new RegExp(urlRegExp), + }); + } + } + } + return this.blockedRepositories; + } + + protected async checkBlockedRepository(user: User, contextURL: string) { + const hit = this.getBlockedRepositories().find((r) => !!contextURL && r.urlRegExp.test(contextURL)); + if (!hit) { + return; + } + if (hit.blockUser) { + try { + await this.userService.blockUser(user.id, true); + } catch (error) { + log.error("Failed to block user.", error); + } + } + throw new Error(`${contextURL} is blocklisted on Gitpod.`); + } + // Note: this function does not expect to be awaited for by its caller. This means that it takes care of error handling itself. protected async actuallyStartWorkspace( ctx: TraceContext,