Skip to content

Commit 95d14d3

Browse files
authored
[server] createProject should not query all repositories – EXP-459 (#18532)
* [server] createProject should not query all repositories except for github.com when using the GitHub App. * fixup: circular dependency problem * extract webhook related functions to scm-service.ts * ensure canCreateProject is case-insensitive
1 parent ebc0d20 commit 95d14d3

File tree

4 files changed

+123
-43
lines changed

4 files changed

+123
-43
lines changed

components/server/src/container-module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ import { WorkspaceService } from "./workspace/workspace-service";
131131
import { SSHKeyService } from "./user/sshkey-service";
132132
import { GitpodTokenService } from "./user/gitpod-token-service";
133133
import { EnvVarService } from "./user/env-var-service";
134+
import { ScmService } from "./projects/scm-service";
134135

135136
export const productionContainerModule = new ContainerModule(
136137
(bind, unbind, isBound, rebind, unbindAsync, onActivation, onDeactivation) => {
@@ -258,6 +259,7 @@ export const productionContainerModule = new ContainerModule(
258259

259260
bind(OrganizationService).toSelf().inSingletonScope();
260261
bind(ProjectsService).toSelf().inSingletonScope();
262+
bind(ScmService).toSelf().inSingletonScope();
261263

262264
bind(NewsletterSubscriptionController).toSelf().inSingletonScope();
263265

components/server/src/projects/projects-service.ts

Lines changed: 8 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,23 @@ import { HostContextProvider } from "../auth/host-context-provider";
1919
import { RepoURL } from "../repohost";
2020
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
2121
import { PartialProject } from "@gitpod/gitpod-protocol/lib/teams-projects-protocol";
22-
import { Config } from "../config";
2322
import { IAnalyticsWriter } from "@gitpod/gitpod-protocol/lib/analytics";
2423
import { ErrorCodes, ApplicationError } from "@gitpod/gitpod-protocol/lib/messaging/error";
2524
import { URL } from "url";
2625
import { Authorizer } from "../authorization/authorizer";
2726
import { TransactionalContext } from "@gitpod/gitpod-db/lib/typeorm/transactional-db-impl";
27+
import { ScmService } from "./scm-service";
2828

2929
@injectable()
3030
export class ProjectsService {
3131
constructor(
3232
@inject(ProjectDB) private readonly projectDB: ProjectDB,
3333
@inject(TracedWorkspaceDB) private readonly workspaceDb: DBWithTracing<WorkspaceDB>,
3434
@inject(HostContextProvider) private readonly hostContextProvider: HostContextProvider,
35-
@inject(Config) private readonly config: Config,
3635
@inject(IAnalyticsWriter) private readonly analytics: IAnalyticsWriter,
3736
@inject(WebhookEventDB) private readonly webhookEventDB: WebhookEventDB,
3837
@inject(Authorizer) private readonly auth: Authorizer,
38+
@inject(ScmService) private readonly scmService: ScmService,
3939
) {}
4040

4141
async getProject(userId: string, projectId: string): Promise<Project> {
@@ -244,7 +244,12 @@ export class ProjectsService {
244244
await this.auth.removeProjectFromOrg(installer.id, teamId, project.id);
245245
throw err;
246246
}
247-
await this.onDidCreateProject(project, installer);
247+
await this.scmService.installWebhookForPrebuilds(project, installer);
248+
249+
// Pre-fetch project details in the background -- don't await
250+
this.getProjectOverview(installer, project.id).catch((err) => {
251+
log.error(`Error pre-fetching project details for project ${project.id}: ${err}`);
252+
});
248253

249254
this.analytics.track({
250255
userId: installer.id,
@@ -261,40 +266,6 @@ export class ProjectsService {
261266
return project;
262267
}
263268

264-
private async onDidCreateProject(project: Project, installer: User) {
265-
// Pre-fetch project details in the background -- don't await
266-
this.getProjectOverview(installer, project.id).catch((err) => {
267-
log.error(`Error pre-fetching project details for project ${project.id}: ${err}`);
268-
});
269-
270-
// Install the prebuilds webhook if possible
271-
const { teamId, cloneUrl } = project;
272-
const parsedUrl = RepoURL.parseRepoUrl(project.cloneUrl);
273-
const hostContext = parsedUrl?.host ? this.hostContextProvider.get(parsedUrl?.host) : undefined;
274-
const authProvider = hostContext && hostContext.authProvider.info;
275-
const type = authProvider && authProvider.authProviderType;
276-
if (
277-
type === "GitLab" ||
278-
type === "Bitbucket" ||
279-
type === "BitbucketServer" ||
280-
(type === "GitHub" && (authProvider?.host !== "github.com" || !this.config.githubApp?.enabled))
281-
) {
282-
const repositoryService = hostContext?.services?.repositoryService;
283-
if (repositoryService) {
284-
// Note: For GitLab, we expect .canInstallAutomatedPrebuilds() to always return true, because earlier
285-
// in the project creation flow, we only propose repositories where the user is actually allowed to
286-
// install a webhook.
287-
if (await repositoryService.canInstallAutomatedPrebuilds(installer, cloneUrl)) {
288-
log.info(
289-
{ organizationId: teamId, userId: installer.id },
290-
"Update prebuild installation for project.",
291-
);
292-
await repositoryService.installAutomatedPrebuilds(installer, cloneUrl);
293-
}
294-
}
295-
}
296-
}
297-
298269
async deleteProject(userId: string, projectId: string, transactionCtx?: TransactionalContext): Promise<void> {
299270
await this.auth.checkPermissionOnProject(userId, "delete", projectId);
300271

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/**
2+
* Copyright (c) 2023 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License.AGPL.txt in the project root for license information.
5+
*/
6+
7+
import { Project, User } from "@gitpod/gitpod-protocol";
8+
import { RepoURL } from "../repohost";
9+
import { inject, injectable } from "inversify";
10+
import { HostContextProvider } from "../auth/host-context-provider";
11+
import { Config } from "../config";
12+
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
13+
14+
@injectable()
15+
export class ScmService {
16+
constructor(
17+
@inject(HostContextProvider) private readonly hostContextProvider: HostContextProvider,
18+
@inject(Config) private readonly config: Config,
19+
) {}
20+
21+
async canInstallWebhook(currentUser: User, cloneURL: string) {
22+
try {
23+
const parsedUrl = RepoURL.parseRepoUrl(cloneURL);
24+
const hostContext = parsedUrl?.host ? this.hostContextProvider.get(parsedUrl?.host) : undefined;
25+
const authProvider = hostContext && hostContext.authProvider.info;
26+
const type = authProvider && authProvider.authProviderType;
27+
const host = authProvider?.host;
28+
if (!type || !host) {
29+
throw Error("Unknown host: " + parsedUrl?.host);
30+
}
31+
if (
32+
type === "GitLab" ||
33+
type === "Bitbucket" ||
34+
type === "BitbucketServer" ||
35+
(type === "GitHub" && (host !== "github.com" || !this.config.githubApp?.enabled))
36+
) {
37+
const repositoryService = hostContext?.services?.repositoryService;
38+
if (repositoryService) {
39+
return await repositoryService.canInstallAutomatedPrebuilds(currentUser, cloneURL);
40+
}
41+
}
42+
// The GitHub App case isn't handled here due to a circular dependency problem.
43+
} catch (error) {
44+
log.error("Failed to check precondition for creating a project.");
45+
}
46+
return false;
47+
}
48+
49+
async installWebhookForPrebuilds(project: Project, installer: User) {
50+
// Install the prebuilds webhook if possible
51+
const { teamId, cloneUrl } = project;
52+
const parsedUrl = RepoURL.parseRepoUrl(project.cloneUrl);
53+
const hostContext = parsedUrl?.host ? this.hostContextProvider.get(parsedUrl?.host) : undefined;
54+
const authProvider = hostContext && hostContext.authProvider.info;
55+
const type = authProvider && authProvider.authProviderType;
56+
if (
57+
type === "GitLab" ||
58+
type === "Bitbucket" ||
59+
type === "BitbucketServer" ||
60+
(type === "GitHub" && (authProvider?.host !== "github.com" || !this.config.githubApp?.enabled))
61+
) {
62+
const repositoryService = hostContext?.services?.repositoryService;
63+
if (repositoryService) {
64+
// Note: For GitLab, we expect .canInstallAutomatedPrebuilds() to always return true, because earlier
65+
// in the project creation flow, we only propose repositories where the user is actually allowed to
66+
// install a webhook.
67+
if (await repositoryService.canInstallAutomatedPrebuilds(installer, cloneUrl)) {
68+
log.info(
69+
{ organizationId: teamId, userId: installer.id },
70+
"Update prebuild installation for project.",
71+
);
72+
await repositoryService.installAutomatedPrebuilds(installer, cloneUrl);
73+
}
74+
}
75+
}
76+
}
77+
}

components/server/src/workspace/gitpod-server-impl.ts

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ import { SSHKeyService } from "../user/sshkey-service";
179179
import { StartWorkspaceOptions, WorkspaceService, mapGrpcError } from "./workspace-service";
180180
import { GitpodTokenService } from "../user/gitpod-token-service";
181181
import { EnvVarService } from "../user/env-var-service";
182+
import { ScmService } from "../projects/scm-service";
182183

183184
// shortcut
184185
export const traceWI = (ctx: TraceContext, wi: Omit<LogContext, "userId">) => TraceContext.setOWI(ctx, wi); // userId is already taken care of in WebsocketConnectionManager
@@ -238,6 +239,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
238239
@inject(HeadlessLogService) private readonly headlessLogService: HeadlessLogService,
239240

240241
@inject(ProjectsService) private readonly projectsService: ProjectsService,
242+
@inject(ScmService) private readonly scmService: ScmService,
241243

242244
@inject(IDEService) private readonly ideService: IDEService,
243245

@@ -2580,16 +2582,13 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
25802582
await this.auth.checkPermissionOnOrganization(user.id, "create_project", params.teamId);
25812583

25822584
// Check if provided clone URL is accessible for the current user, and user has admin permissions.
2583-
let url;
25842585
try {
2585-
url = new URL(params.cloneUrl);
2586+
new URL(params.cloneUrl);
25862587
} catch (err) {
25872588
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "Clone URL must be a valid URL.");
25882589
}
2589-
const availableRepositories = await this.getProviderRepositoriesForUser(ctx, { provider: url.host });
2590-
if (!availableRepositories.some((r) => r.cloneUrl === params.cloneUrl)) {
2591-
// The error message is derived from internals of `getProviderRepositoriesForUser` and
2592-
// `getRepositoriesForAutomatedPrebuilds`, which require admin permissions to be present.
2590+
const canCreateProject = await this.canCreateProject(user, params.cloneUrl);
2591+
if (!canCreateProject) {
25932592
throw new ApplicationError(
25942593
ErrorCodes.BAD_REQUEST,
25952594
"Repository URL seems to be inaccessible, or admin permissions are missing.",
@@ -2618,6 +2617,37 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
26182617
return project;
26192618
}
26202619

2620+
/**
2621+
* Checks if a project can be created, i.e. the current user has the required permissions
2622+
* to install webhooks for the given repository.
2623+
*/
2624+
private async canCreateProject(currentUser: User, cloneURL: string) {
2625+
try {
2626+
const parsedUrl = RepoURL.parseRepoUrl(cloneURL);
2627+
const host = parsedUrl?.host;
2628+
if (!host) {
2629+
throw Error("Unknown host: " + parsedUrl?.host);
2630+
}
2631+
if (host === "github.com" && this.config.githubApp?.enabled) {
2632+
const availableRepositories = await this.githubAppSupport.getProviderRepositoriesForUser({
2633+
user: currentUser,
2634+
provider: "github.com",
2635+
});
2636+
return availableRepositories.some(
2637+
(r) => r?.cloneUrl?.toLocaleLowerCase() === cloneURL?.toLocaleLowerCase(),
2638+
);
2639+
} else {
2640+
return await this.scmService.canInstallWebhook(currentUser, cloneURL);
2641+
2642+
// note: the GitHub App based check is not included in the ProjectService due
2643+
// to a circular dependency problem which would otherwise occur.
2644+
}
2645+
} catch (error) {
2646+
log.error("Failed to check precondition for creating a project.");
2647+
}
2648+
return false;
2649+
}
2650+
26212651
public async updateProjectPartial(ctx: TraceContext, partialProject: PartialProject): Promise<void> {
26222652
traceAPIParams(ctx, {
26232653
// censor everything irrelevant

0 commit comments

Comments
 (0)