Skip to content

[server] Add server API to list blocked repositories #11080

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 52 additions & 14 deletions components/gitpod-db/src/blocked-repository-db.spec.db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>(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");

Expand All @@ -44,13 +68,7 @@ class BlockedRepositoryDBSpec {

@test(timeout(10000))
public async checkRepositoryIsNotBlocked() {
const typeorm = testContainer.get<TypeORM>(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",
Expand All @@ -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>(TypeORM);
const manager = await typeorm.getConnection();
Expand Down
14 changes: 14 additions & 0 deletions components/gitpod-db/src/blocked-repository-db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here it might make sense to limit to the fields that actually are indexex, for instance by using orderBy: keyof Pick<BlockedRepository, "id | "blockUser"> or similar.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have no indexes on the table besides the PK. As mentioned yesterday #11036 (comment), mysql can't make use of indexes on urlRegexp when testing with REGEXP.

Do you think we should add indexes to support this admin search?

orderDir: "DESC" | "ASC",
searchTerm?: string,
minCreationDate?: Date,
maxCreationDate?: Date,
): Promise<{ total: number; rows: BlockedRepository[] }>;

findBlockedRepositoryByURL(contextURL: string): Promise<BlockedRepository | undefined>;

createBlockedRepository(urlRegexp: string, blockUser: boolean): Promise<BlockedRepository>;

deleteBlockedRepository(id: number): Promise<boolean>;
}
44 changes: 44 additions & 0 deletions components/gitpod-db/src/typeorm/blocked-repository-db-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,50 @@ export class TypeORMBlockedRepositoryDBImpl implements BlockedRepositoryDB {
return (await this.getEntityManager()).getRepository<DBBlockedRepository>(DBBlockedRepository);
}

public async createBlockedRepository(urlRegexp: string, blockUser: boolean): Promise<BlockedRepository> {
const blockedRepositoryRepo = await this.getBlockedRepositoryRepo();

return await blockedRepositoryRepo.save({ urlRegexp: urlRegexp, blockUser: blockUser, deleted: false });
}

public async deleteBlockedRepository(id: number): Promise<boolean> {
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<BlockedRepository | undefined> {
const blockedRepositoryRepo = await this.getBlockedRepositoryRepo();

Expand Down
7 changes: 7 additions & 0 deletions components/gitpod-protocol/src/admin-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -20,6 +21,12 @@ export interface AdminServer {
adminModifyRoleOrPermission(req: AdminModifyRoleOrPermissionRequest): Promise<User>;
adminModifyPermanentWorkspaceFeatureFlag(req: AdminModifyPermanentWorkspaceFeatureFlagRequest): Promise<User>;

adminCreateBlockedRepository(urlRegexp: string, blockUser: boolean): Promise<BlockedRepository>;
adminDeleteBlockedRepository(id: number): Promise<boolean>;
adminGetBlockedRepositories(
req: AdminGetListRequest<BlockedRepository>,
): Promise<AdminGetListResult<BlockedRepository>>;

adminGetTeamMembers(teamId: string): Promise<TeamMemberInfo[]>;
adminGetTeams(req: AdminGetListRequest<Team>): Promise<AdminGetListResult<Team>>;
adminGetTeamById(id: string): Promise<Team | undefined>;
Expand Down
54 changes: 54 additions & 0 deletions components/server/ee/src/workspace/gitpod-server-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -618,6 +619,59 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
}
}

async adminGetBlockedRepositories(
ctx: TraceContext,
req: AdminGetListRequest<BlockedRepository>,
): Promise<AdminGetListResult<BlockedRepository>> {
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<BlockedRepository> {
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<boolean> {
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<User> {
traceAPIParams(ctx, { req });

Expand Down
3 changes: 3 additions & 0 deletions components/server/src/auth/rate-limiter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
18 changes: 18 additions & 0 deletions components/server/src/workspace/gitpod-server-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<BlockedRepository>,
): Promise<AdminGetListResult<BlockedRepository>> {
throw new ResponseError(ErrorCodes.EE_FEATURE, `Admin support is implemented in Gitpod's Enterprise Edition`);
}

adminCreateBlockedRepository(ctx: TraceContext, urlRegexp: string, blockUser: boolean): Promise<BlockedRepository> {
throw new ResponseError(ErrorCodes.EE_FEATURE, `Admin support is implemented in Gitpod's Enterprise Edition`);
}

adminDeleteBlockedRepository(ctx: TraceContext, id: number): Promise<boolean> {
throw new ResponseError(ErrorCodes.EE_FEATURE, `Admin support is implemented in Gitpod's Enterprise Edition`);
}

async adminGetUser(ctx: TraceContext, id: string): Promise<User> {
throw new ResponseError(ErrorCodes.EE_FEATURE, `Admin support is implemented in Gitpod's Enterprise Edition`);
}
Expand Down