From d05129e0c43805f9bf3bb9fd759c4edd56b37a11 Mon Sep 17 00:00:00 2001 From: Andrew Farries Date: Fri, 1 Jul 2022 10:33:49 +0000 Subject: [PATCH] [server] Add APIs for working with blocked repos * Create, list, delete. --- .../src/blocked-repository-db.spec.db.ts | 66 +++++++++++++++---- .../gitpod-db/src/blocked-repository-db.ts | 14 ++++ .../src/typeorm/blocked-repository-db-impl.ts | 44 +++++++++++++ .../gitpod-protocol/src/admin-protocol.ts | 7 ++ .../ee/src/workspace/gitpod-server-impl.ts | 54 +++++++++++++++ components/server/src/auth/rate-limiter.ts | 3 + .../src/workspace/gitpod-server-impl.ts | 18 +++++ 7 files changed, 192 insertions(+), 14 deletions(-) diff --git a/components/gitpod-db/src/blocked-repository-db.spec.db.ts b/components/gitpod-db/src/blocked-repository-db.spec.db.ts index aa6e075b86683c..8cb35d2b15c8c1 100644 --- a/components/gitpod-db/src/blocked-repository-db.spec.db.ts +++ b/components/gitpod-db/src/blocked-repository-db.spec.db.ts @@ -25,15 +25,39 @@ class BlockedRepositoryDBSpec { await this.wipeRepo(); } + @test(timeout(10000)) + public async canCreateABlockedRepository() { + const blockedRepository = await this.blockedRepositoryDb.createBlockedRepository( + "github.com/bob/some-repo", + true, + ); + expect(blockedRepository.urlRegexp).eq("github.com/bob/some-repo"); + expect(blockedRepository.blockUser).eq(true); + } + + @test(timeout(10000)) + public async canDeleteABlockedRepository() { + const blockedRepository = await this.blockedRepositoryDb.createBlockedRepository("github.com/bob/*/", true); + + const result = await this.blockedRepositoryDb.deleteBlockedRepository(blockedRepository.id); + + expect(result).eq(true); + expect(await this.blockedRepositoryDb.findBlockedRepositoryByURL("github.com/bob/some-repo")).undefined; + } + + @test(timeout(10000)) + public async canNotDeleteABlockedRepositoryWithAnIdThatDoesNotExist() { + await this.blockedRepositoryDb.createBlockedRepository("github.com/bob/*/", true); + + const result = await this.blockedRepositoryDb.deleteBlockedRepository(9999); + + expect(result).eq(false); + expect(await this.blockedRepositoryDb.findBlockedRepositoryByURL("github.com/bob/some-repo")).not.undefined; + } + @test(timeout(10000)) public async checkRepositoryIsBlocked() { - const typeorm = testContainer.get(TypeORM); - const manager = await typeorm.getConnection(); - manager.getRepository(DBBlockedRepository).insert({ - urlRegexp: "github.com/bob/.*", - blockUser: true, - deleted: false, - }); + await this.blockedRepositoryDb.createBlockedRepository("github.com/bob/.*", true); const blockedRepository = await this.blockedRepositoryDb.findBlockedRepositoryByURL("github.com/bob/some-repo"); @@ -44,13 +68,7 @@ class BlockedRepositoryDBSpec { @test(timeout(10000)) public async checkRepositoryIsNotBlocked() { - const typeorm = testContainer.get(TypeORM); - const manager = await typeorm.getConnection(); - manager.getRepository(DBBlockedRepository).insert({ - urlRegexp: "github.com/bob/.*", - blockUser: true, - deleted: false, - }); + await this.blockedRepositoryDb.createBlockedRepository("github.com/bob/.*", true); const blockedRepository = await this.blockedRepositoryDb.findBlockedRepositoryByURL( "github.com/alice/some-repo", @@ -59,6 +77,26 @@ class BlockedRepositoryDBSpec { expect(blockedRepository).undefined; } + @test(timeout(10000)) + public async canFindAllRepositoriesWithoutSearchTerm() { + await this.blockedRepositoryDb.createBlockedRepository("github.com/bob/.*", true); + await this.blockedRepositoryDb.createBlockedRepository("github.com/alice/.*", true); + + const blockedRepositories = await this.blockedRepositoryDb.findAllBlockedRepositories(0, 1, "id", "ASC"); + + expect(blockedRepositories.total).eq(2); + } + + @test(timeout(10000)) + public async canFindAllRepositoriesWithSearchTerm() { + await this.blockedRepositoryDb.createBlockedRepository("github.com/bob/.*", true); + await this.blockedRepositoryDb.createBlockedRepository("github.com/alice/.*", true); + + const blockedRepositories = await this.blockedRepositoryDb.findAllBlockedRepositories(0, 1, "id", "ASC", "bob"); + + expect(blockedRepositories.total).eq(1); + } + async wipeRepo() { const typeorm = testContainer.get(TypeORM); const manager = await typeorm.getConnection(); diff --git a/components/gitpod-db/src/blocked-repository-db.ts b/components/gitpod-db/src/blocked-repository-db.ts index 363f9837a45cc2..6327559fbd3201 100644 --- a/components/gitpod-db/src/blocked-repository-db.ts +++ b/components/gitpod-db/src/blocked-repository-db.ts @@ -9,5 +9,19 @@ import { BlockedRepository } from "@gitpod/gitpod-protocol/src/blocked-repositor export const BlockedRepositoryDB = Symbol("BlockedRepositoryDB"); export interface BlockedRepositoryDB { + findAllBlockedRepositories( + offset: number, + limit: number, + orderBy: keyof BlockedRepository, + orderDir: "DESC" | "ASC", + searchTerm?: string, + minCreationDate?: Date, + maxCreationDate?: Date, + ): Promise<{ total: number; rows: BlockedRepository[] }>; + findBlockedRepositoryByURL(contextURL: string): Promise; + + createBlockedRepository(urlRegexp: string, blockUser: boolean): Promise; + + deleteBlockedRepository(id: number): Promise; } diff --git a/components/gitpod-db/src/typeorm/blocked-repository-db-impl.ts b/components/gitpod-db/src/typeorm/blocked-repository-db-impl.ts index 58f13ba78693f6..e7814ffb736436 100644 --- a/components/gitpod-db/src/typeorm/blocked-repository-db-impl.ts +++ b/components/gitpod-db/src/typeorm/blocked-repository-db-impl.ts @@ -23,6 +23,50 @@ export class TypeORMBlockedRepositoryDBImpl implements BlockedRepositoryDB { return (await this.getEntityManager()).getRepository(DBBlockedRepository); } + public async createBlockedRepository(urlRegexp: string, blockUser: boolean): Promise { + const blockedRepositoryRepo = await this.getBlockedRepositoryRepo(); + + return await blockedRepositoryRepo.save({ urlRegexp: urlRegexp, blockUser: blockUser, deleted: false }); + } + + public async deleteBlockedRepository(id: number): Promise { + const blockedRepositoryRepo = await this.getBlockedRepositoryRepo(); + + const result = await blockedRepositoryRepo.delete(id); + return !!result.affected; + } + + public async findAllBlockedRepositories( + offset: number, + limit: number, + orderBy: keyof BlockedRepository, + orderDir: "DESC" | "ASC", + searchTerm?: string, + minCreationDate?: Date, + maxCreationDate?: Date, + ): Promise<{ total: number; rows: BlockedRepository[] }> { + const blockedRepositoryRepo = await this.getBlockedRepositoryRepo(); + + const qBuilder = blockedRepositoryRepo.createQueryBuilder("br").where(`br.deleted = 0`); + if (searchTerm) { + qBuilder.andWhere(`br.urlRegexp LIKE :searchTerm`, { searchTerm: "%" + searchTerm + "%" }); + } + if (minCreationDate) { + qBuilder.andWhere("br.createdAt >= :minCreationDate", { + minCreationDate: minCreationDate.toISOString(), + }); + } + if (maxCreationDate) { + qBuilder.andWhere("br.createdAt < :maxCreationDate", { + maxCreationDate: maxCreationDate.toISOString(), + }); + } + qBuilder.orderBy("br." + orderBy, orderDir); + qBuilder.skip(offset).take(limit).select(); + const [rows, total] = await qBuilder.getManyAndCount(); + return { total, rows }; + } + public async findBlockedRepositoryByURL(contextURL: string): Promise { const blockedRepositoryRepo = await this.getBlockedRepositoryRepo(); diff --git a/components/gitpod-protocol/src/admin-protocol.ts b/components/gitpod-protocol/src/admin-protocol.ts index 25153c995a7eaa..d4582dda743a31 100644 --- a/components/gitpod-protocol/src/admin-protocol.ts +++ b/components/gitpod-protocol/src/admin-protocol.ts @@ -5,6 +5,7 @@ */ import { User, Workspace, NamedWorkspaceFeatureFlag } from "./protocol"; +import { BlockedRepository } from "./blocked-repositories-protocol"; import { FindPrebuildsParams } from "./gitpod-service"; import { Project, Team, PrebuildWithStatus, TeamMemberInfo, TeamMemberRole } from "./teams-projects-protocol"; import { WorkspaceInstance, WorkspaceInstancePhase } from "./workspace-instance"; @@ -20,6 +21,12 @@ export interface AdminServer { adminModifyRoleOrPermission(req: AdminModifyRoleOrPermissionRequest): Promise; adminModifyPermanentWorkspaceFeatureFlag(req: AdminModifyPermanentWorkspaceFeatureFlagRequest): Promise; + adminCreateBlockedRepository(urlRegexp: string, blockUser: boolean): Promise; + adminDeleteBlockedRepository(id: number): Promise; + adminGetBlockedRepositories( + req: AdminGetListRequest, + ): Promise>; + adminGetTeamMembers(teamId: string): Promise; adminGetTeams(req: AdminGetListRequest): Promise>; adminGetTeamById(id: string): Promise; diff --git a/components/server/ee/src/workspace/gitpod-server-impl.ts b/components/server/ee/src/workspace/gitpod-server-impl.ts index f599b00f129c5d..ed539f25caf0d1 100644 --- a/components/server/ee/src/workspace/gitpod-server-impl.ts +++ b/components/server/ee/src/workspace/gitpod-server-impl.ts @@ -66,6 +66,7 @@ import { PrebuildManager } from "../prebuilds/prebuild-manager"; import { LicenseDB } from "@gitpod/gitpod-db/lib"; import { ResourceAccessGuard } from "../../../src/auth/resource-access"; import { AccountStatement, CreditAlert, Subscription } from "@gitpod/gitpod-protocol/lib/accounting-protocol"; +import { BlockedRepository } from "@gitpod/gitpod-protocol/lib/blocked-repositories-protocol"; import { EligibilityService } from "../user/eligibility-service"; import { AccountStatementProvider } from "../user/account-statement-provider"; import { GithubUpgradeURL, PlanCoupon } from "@gitpod/gitpod-protocol/lib/payment-protocol"; @@ -618,6 +619,59 @@ export class GitpodServerEEImpl extends GitpodServerImpl { } } + async adminGetBlockedRepositories( + ctx: TraceContext, + req: AdminGetListRequest, + ): Promise> { + traceAPIParams(ctx, { req: censor(req, "searchTerm") }); // searchTerm may contain PII + await this.requireEELicense(Feature.FeatureAdminDashboard); + + await this.guardAdminAccess("adminGetBlockedRepositories", { req }, Permission.ADMIN_USERS); + + try { + const res = await this.blockedRepostoryDB.findAllBlockedRepositories( + req.offset, + req.limit, + req.orderBy, + req.orderDir === "asc" ? "ASC" : "DESC", + req.searchTerm, + ); + return res; + } catch (e) { + throw new ResponseError(ErrorCodes.INTERNAL_SERVER_ERROR, e.toString()); + } + } + + async adminCreateBlockedRepository( + ctx: TraceContext, + urlRegexp: string, + blockUser: boolean, + ): Promise { + traceAPIParams(ctx, { urlRegexp, blockUser }); + await this.requireEELicense(Feature.FeatureAdminDashboard); + + await this.guardAdminAccess("adminCreateBlockedRepository", { urlRegexp, blockUser }, Permission.ADMIN_USERS); + + try { + return await this.blockedRepostoryDB.createBlockedRepository(urlRegexp, blockUser); + } catch (e) { + throw new ResponseError(ErrorCodes.INTERNAL_SERVER_ERROR, e.toString()); + } + } + + async adminDeleteBlockedRepository(ctx: TraceContext, id: number): Promise { + traceAPIParams(ctx, { id }); + await this.requireEELicense(Feature.FeatureAdminDashboard); + + await this.guardAdminAccess("adminDeleteBlockedRepository", { id }, Permission.ADMIN_USERS); + + try { + return await this.blockedRepostoryDB.deleteBlockedRepository(id); + } catch (e) { + throw new ResponseError(ErrorCodes.INTERNAL_SERVER_ERROR, e.toString()); + } + } + async adminModifyRoleOrPermission(ctx: TraceContext, req: AdminModifyRoleOrPermissionRequest): Promise { traceAPIParams(ctx, { req }); diff --git a/components/server/src/auth/rate-limiter.ts b/components/server/src/auth/rate-limiter.ts index 37df085b09e532..05061ebf27a60f 100644 --- a/components/server/src/auth/rate-limiter.ts +++ b/components/server/src/auth/rate-limiter.ts @@ -162,6 +162,9 @@ function getConfig(config: RateLimiterConfig): RateLimiterConfig { adminGetSettings: { group: "default", points: 1 }, adminUpdateSettings: { group: "default", points: 1 }, adminGetTelemetryData: { group: "default", points: 1 }, + adminGetBlockedRepositories: { group: "default", points: 1 }, + adminCreateBlockedRepository: { group: "default", points: 1 }, + adminDeleteBlockedRepository: { group: "default", points: 1 }, validateLicense: { group: "default", points: 1 }, getLicenseInfo: { group: "default", points: 1 }, diff --git a/components/server/src/workspace/gitpod-server-impl.ts b/components/server/src/workspace/gitpod-server-impl.ts index 90bdd6a1a901b9..33858f36248d37 100644 --- a/components/server/src/workspace/gitpod-server-impl.ts +++ b/components/server/src/workspace/gitpod-server-impl.ts @@ -24,6 +24,7 @@ import { InstallationAdminDB, ProjectDB, } from "@gitpod/gitpod-db/lib"; +import { BlockedRepositoryDB } from "@gitpod/gitpod-db/lib/blocked-repository-db"; import { AuthProviderEntry, AuthProviderInfo, @@ -79,6 +80,7 @@ import { UserSSHPublicKeyValue, } from "@gitpod/gitpod-protocol"; import { AccountStatement } from "@gitpod/gitpod-protocol/lib/accounting-protocol"; +import { BlockedRepository } from "@gitpod/gitpod-protocol/lib/blocked-repositories-protocol"; import { AdminBlockUserRequest, AdminGetListRequest, @@ -199,6 +201,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { @inject(ImageBuilderClientProvider) protected imageBuilderClientProvider: ImageBuilderClientProvider; @inject(UserDB) protected readonly userDB: UserDB; + @inject(BlockedRepositoryDB) protected readonly blockedRepostoryDB: BlockedRepositoryDB; @inject(TokenProvider) protected readonly tokenProvider: TokenProvider; @inject(UserService) protected readonly userService: UserService; @inject(UserMessageViewsDB) protected readonly userMessageViewsDB: UserMessageViewsDB; @@ -2631,6 +2634,21 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { throw new ResponseError(ErrorCodes.EE_FEATURE, `Admin support is implemented in Gitpod's Enterprise Edition`); } + adminGetBlockedRepositories( + ctx: TraceContext, + req: AdminGetListRequest, + ): Promise> { + throw new ResponseError(ErrorCodes.EE_FEATURE, `Admin support is implemented in Gitpod's Enterprise Edition`); + } + + adminCreateBlockedRepository(ctx: TraceContext, urlRegexp: string, blockUser: boolean): Promise { + throw new ResponseError(ErrorCodes.EE_FEATURE, `Admin support is implemented in Gitpod's Enterprise Edition`); + } + + adminDeleteBlockedRepository(ctx: TraceContext, id: number): Promise { + throw new ResponseError(ErrorCodes.EE_FEATURE, `Admin support is implemented in Gitpod's Enterprise Edition`); + } + async adminGetUser(ctx: TraceContext, id: string): Promise { throw new ResponseError(ErrorCodes.EE_FEATURE, `Admin support is implemented in Gitpod's Enterprise Edition`); }