-

-
No Recent Projects
-
Add projects to enable and manage Prebuilds.
Learn more about Prebuilds
-
-
-
+ {projects.length < 1 && (
+
+

+
No Recent Projects
+
Add projects to enable and manage Prebuilds.
Learn more about Prebuilds
+
+
+
+
+
+
+ )}
+ {projects.length > 0 && (
+
+
+
+
+
onSearchProjects(e.target.value)} />
+
+
+
+ { /* TODO */ }
+ }, {
+ title: 'All',
+ onClick: () => { /* TODO */ }
+ }]} />
+
+
+
+
+ {projects.map(p => (
+
+
+
{p.name}
+
{toRemoteURL(p.cloneUrl)}
+
+
+
+
viewAllPrebuilds(p)}>
+ {p.lastPrebuild
+ ? (
+
+
+
+
{p.lastPrebuild.branch}
+
{moment(p.lastPrebuild.startedAt, "YYYYMMDD").fromNow()}
+
View All ⟶
+
)
+ : (
)}
+
+
))}
+
+
-
+ )}
>;
}
\ No newline at end of file
diff --git a/components/dashboard/src/projects/Settings.tsx b/components/dashboard/src/projects/Settings.tsx
new file mode 100644
index 00000000000000..6eaa43d0f4322f
--- /dev/null
+++ b/components/dashboard/src/projects/Settings.tsx
@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2021 Gitpod GmbH. All rights reserved.
+ * Licensed under the GNU Affero General Public License (AGPL).
+ * See License-AGPL.txt in the project root for license information.
+ */
+
+import { useContext, useEffect } from "react";
+import { useLocation, useRouteMatch } from "react-router";
+import Header from "../components/Header";
+import { getCurrentTeam, TeamsContext } from "../teams/teams-context";
+
+export default function () {
+ const { teams } = useContext(TeamsContext);
+ const location = useLocation();
+ const match = useRouteMatch<{ team: string, resource: string }>("/:team/:resource");
+ const projectName = match?.params?.resource;
+ const team = getCurrentTeam(location, teams);
+
+
+ useEffect(() => {
+ if (!team) {
+ return;
+ }
+ (async () => {
+ })();
+ }, [team]);
+
+ return <>
+
+
+
+
+ >;
+}
\ No newline at end of file
diff --git a/components/dashboard/src/provider-utils.tsx b/components/dashboard/src/provider-utils.tsx
index ff61c470a79f75..f6a95e32149448 100644
--- a/components/dashboard/src/provider-utils.tsx
+++ b/components/dashboard/src/provider-utils.tsx
@@ -64,8 +64,13 @@ async function openAuthorizeWindow(params: OpenAuthorizeWindowParams) {
search: `returnTo=${encodeURIComponent(returnTo)}&host=${host}${overrideScopes ? "&override=true" : ""}&scopes=${requestedScopes.join(',')}`
}).toString();
+ const width = 800;
+ const height = 800;
+ const left = (window.screen.width / 2) - (width / 2);
+ const top = (window.screen.height / 2) - (height / 2);
+
// Optimistically assume that the new window was opened.
- window.open(url, "gitpod-auth-window");
+ window.open(url, "gitpod-auth-window", `width=${width},height=${height},top=${top},left=${left}status=yes,scrollbars=yes,resizable=yes`);
const eventListener = (event: MessageEvent) => {
// todo: check event.origin
diff --git a/components/gitpod-db/src/project-db.ts b/components/gitpod-db/src/project-db.ts
index c84b380f46e1bc..0c052b718484a9 100644
--- a/components/gitpod-db/src/project-db.ts
+++ b/components/gitpod-db/src/project-db.ts
@@ -9,4 +9,5 @@ import { Project } from "@gitpod/gitpod-protocol";
export const ProjectDB = Symbol('ProjectDB');
export interface ProjectDB {
findProjectsByTeam(teamId: string): Promise
;
+ createProject(name: string, cloneUrl: string, teamId: string, appInstallationId: string): Promise;
}
\ No newline at end of file
diff --git a/components/gitpod-db/src/typeorm/entity/db-project.ts b/components/gitpod-db/src/typeorm/entity/db-project.ts
index dc1db7fac96015..ec7768eb44ac61 100644
--- a/components/gitpod-db/src/typeorm/entity/db-project.ts
+++ b/components/gitpod-db/src/typeorm/entity/db-project.ts
@@ -13,6 +13,9 @@ export class DBProject {
@PrimaryColumn(TypeORM.UUID_COLUMN_TYPE)
id: string;
+ @Column()
+ name: string;
+
@Column()
cloneUrl: string;
diff --git a/components/gitpod-db/src/typeorm/migration/1622468446118-TeamsAndProjects.ts b/components/gitpod-db/src/typeorm/migration/1622468446118-TeamsAndProjects.ts
index cc4d1eb423caa1..bb00ecd7e90ed8 100644
--- a/components/gitpod-db/src/typeorm/migration/1622468446118-TeamsAndProjects.ts
+++ b/components/gitpod-db/src/typeorm/migration/1622468446118-TeamsAndProjects.ts
@@ -11,7 +11,7 @@ export class TeamsAndProjects1622468446118 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise {
await queryRunner.query("CREATE TABLE IF NOT EXISTS `d_b_team` (`id` char(36) NOT NULL, `name` varchar(255) NOT NULL, `slug` varchar(255) NOT NULL, `creationTime` varchar(255) NOT NULL, `deleted` tinyint(4) NOT NULL DEFAULT '0', `_lastModified` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (`id`), KEY `ind_dbsync` (`_lastModified`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
await queryRunner.query("CREATE TABLE IF NOT EXISTS `d_b_team_membership` (`id` char(36) NOT NULL, `teamId` char(36) NOT NULL, `userId` char(36) NOT NULL, `role` varchar(255) NOT NULL, `creationTime` varchar(255) NOT NULL, `deleted` tinyint(4) NOT NULL DEFAULT '0', `_lastModified` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (`id`), KEY `ind_teamId` (`teamId`), KEY `ind_userId` (`userId`), KEY `ind_dbsync` (`_lastModified`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
- await queryRunner.query("CREATE TABLE IF NOT EXISTS `d_b_project` (`id` char(36) NOT NULL, `cloneUrl` varchar(255) NOT NULL, `teamId` char(36) NOT NULL, `appInstallationId` varchar(255) NOT NULL, `creationTime` varchar(255) NOT NULL, `deleted` tinyint(4) NOT NULL DEFAULT '0', `_lastModified` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (`id`), KEY `ind_teamId` (`teamId`), KEY `ind_dbsync` (`_lastModified`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
+ await queryRunner.query("CREATE TABLE IF NOT EXISTS `d_b_project` (`id` char(36) NOT NULL, `name` varchar(255) NOT NULL, `cloneUrl` varchar(255) NOT NULL, `teamId` char(36) NOT NULL, `appInstallationId` varchar(255) NOT NULL, `creationTime` varchar(255) NOT NULL, `deleted` tinyint(4) NOT NULL DEFAULT '0', `_lastModified` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (`id`), KEY `ind_teamId` (`teamId`), KEY `ind_dbsync` (`_lastModified`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
}
public async down(queryRunner: QueryRunner): Promise {
diff --git a/components/gitpod-db/src/typeorm/project-db-impl.ts b/components/gitpod-db/src/typeorm/project-db-impl.ts
index d13e48357260e9..4ef79cd79941c8 100644
--- a/components/gitpod-db/src/typeorm/project-db-impl.ts
+++ b/components/gitpod-db/src/typeorm/project-db-impl.ts
@@ -10,6 +10,7 @@ import { Repository } from "typeorm";
import { ProjectDB } from "../project-db";
import { DBProject } from "./entity/db-project";
import { Project } from "@gitpod/gitpod-protocol";
+import * as uuidv4 from 'uuid/v4';
@injectable()
export class ProjectDBImpl implements ProjectDB {
@@ -27,4 +28,19 @@ export class ProjectDBImpl implements ProjectDB {
const repo = await this.getRepo();
return repo.find({ teamId });
}
+
+ public async createProject(name: string, cloneUrl: string, teamId: string, appInstallationId: string): Promise {
+ const repo = await this.getRepo();
+
+ const project: Project = {
+ id: uuidv4(),
+ name,
+ teamId,
+ cloneUrl,
+ appInstallationId,
+ creationTime: new Date().toISOString(),
+ }
+ await repo.save(project);
+ return project;
+ }
}
diff --git a/components/gitpod-protocol/src/gitpod-service.ts b/components/gitpod-protocol/src/gitpod-service.ts
index dd456080d81bd1..85f84309cfd68b 100644
--- a/components/gitpod-protocol/src/gitpod-service.ts
+++ b/components/gitpod-protocol/src/gitpod-service.ts
@@ -9,7 +9,8 @@ import {
WhitelistedRepository, WorkspaceImageBuild, AuthProviderInfo, Branding, CreateWorkspaceMode,
Token, UserEnvVarValue, ResolvePluginsParams, PreparePluginUploadParams, Terms,
ResolvedPlugins, Configuration, InstallPluginsParams, UninstallPluginParams, UserInfo, GitpodTokenType,
- GitpodToken, AuthProviderEntry, GuessGitTokenScopesParams, GuessedGitTokenScopes, Team, TeamMemberInfo, TeamMembershipInvite
+ GitpodToken, AuthProviderEntry, GuessGitTokenScopesParams, GuessedGitTokenScopes, Team, TeamMemberInfo,
+ TeamMembershipInvite, Project, ProjectInfo, PrebuildInfo
} from './protocol';
import { JsonRpcProxy, JsonRpcServer } from './messaging/proxy-factory';
import { Disposable, CancellationTokenSource } from 'vscode-jsonrpc';
@@ -202,6 +203,35 @@ export interface GitpodServer extends JsonRpcServer, AdminServer,
getGithubUpgradeUrls(): Promise;
+ /**
+ * projects
+ */
+ getProviderRepositoriesForUser(params: GetProviderRepositoriesParams): Promise;
+ createProject(params: CreateProjectParams): Promise;
+ getProjects(teamId: string): Promise;
+ getPrebuilds(teamId: string, project: string): Promise;
+}
+
+export interface CreateProjectParams {
+ name: string;
+ account: string;
+ provider: string;
+ cloneUrl: string;
+ teamId: string;
+ appInstallationId: string;
+}
+export interface GetProviderRepositoriesParams {
+ provider: string;
+ hints?: { installationId: string } | object;
+}
+export interface ProviderRepository {
+ name: string;
+ account: string;
+ accountAvatarUrl: string;
+ cloneUrl: string;
+ updatedAt: string;
+ installationId?: number;
+ installationUpdatedAt?: string;
}
export const WorkspaceTimeoutValues = ["30m", "60m", "180m"] as const;
@@ -214,6 +244,7 @@ export const createServerMock = function { });
methods.dispose = methods.dispose || (() => { });
return new Proxy>(methods as any as JsonRpcProxy, {
+ // @ts-ignore
get: (target: S, property: keyof S) => {
const result = target[property];
if (!result) {
diff --git a/components/gitpod-protocol/src/protocol.ts b/components/gitpod-protocol/src/protocol.ts
index 48bf60a9649405..1d5d00cdf2ba60 100644
--- a/components/gitpod-protocol/src/protocol.ts
+++ b/components/gitpod-protocol/src/protocol.ts
@@ -103,6 +103,8 @@ export interface AdditionalUserData {
// key is the name of the OAuth client i.e. local app, string the iso date when it was approved
// TODO(rl): provide a management UX to allow rescinding of approval
oauthClientsApproved?: { [key: string]: string }
+ // to remember GH Orgs the user installed/updated the GH App for
+ knownGitHubOrgs?: string[];
}
export interface EmailNotificationSettings {
@@ -1198,6 +1200,7 @@ export interface Terms {
export interface Project {
id: string;
+ name: string;
cloneUrl: string;
teamId: string;
appInstallationId: string;
@@ -1206,6 +1209,21 @@ export interface Project {
deleted?: boolean;
}
+export interface ProjectInfo extends Project {
+ lastPrebuild?: PrebuildInfo;
+}
+
+export interface PrebuildInfo {
+ id: string;
+ teamId: string;
+ project: string;
+ cloneUrl: string;
+ branch: string;
+ startedAt: string;
+ startedBy: string;
+ status: PrebuiltWorkspaceState;
+}
+
export interface Team {
id: string;
name: string;
diff --git a/components/server/ee/src/container-module.ts b/components/server/ee/src/container-module.ts
index a431bcc344ebec..ecc239aaf8bad6 100644
--- a/components/server/ee/src/container-module.ts
+++ b/components/server/ee/src/container-module.ts
@@ -52,6 +52,7 @@ import { UserDeletionService } from "../../src/user/user-deletion-service";
import { BlockedUserFilter } from "../../src/auth/blocked-user-filter";
import { EMailDomainService, EMailDomainServiceImpl } from "./auth/email-domain-service";
import { UserDeletionServiceEE } from "./user/user-deletion-service";
+import { GitHubAppSupport } from "./github/github-app-support";
export const productionEEContainerModule = new ContainerModule((bind, unbind, isBound, rebind) => {
rebind(Server).to(ServerEE).inSingletonScope();
@@ -66,6 +67,7 @@ export const productionEEContainerModule = new ContainerModule((bind, unbind, is
bind(IPrefixContextParser).to(StartPrebuildContextParser).inSingletonScope();
bind(IPrefixContextParser).to(StartIncrementalPrebuildContextParser).inSingletonScope();
bind(GithubApp).toSelf().inSingletonScope();
+ bind(GitHubAppSupport).toSelf().inSingletonScope();
bind(GithubAppRules).toSelf().inSingletonScope();
bind(PrebuildStatusMaintainer).toSelf().inSingletonScope();
bind(GitLabApp).toSelf().inSingletonScope();
diff --git a/components/server/ee/src/github/github-app-support.ts b/components/server/ee/src/github/github-app-support.ts
new file mode 100644
index 00000000000000..7ccdc8fd84229d
--- /dev/null
+++ b/components/server/ee/src/github/github-app-support.ts
@@ -0,0 +1,151 @@
+/**
+ * Copyright (c) 2020 Gitpod GmbH. All rights reserved.
+ * Licensed under the Gitpod Enterprise Source Code License,
+ * See License.enterprise.txt in the project root folder.
+ */
+
+import { ProviderRepository, User } from "@gitpod/gitpod-protocol";
+import { inject, injectable } from "inversify";
+import { GithubApp } from "../prebuilds/github-app";
+import { RequestError } from "@octokit/request-error";
+import { TokenProvider } from "../../../src/user/token-provider";
+import { UserDB } from "@gitpod/gitpod-db/lib";
+import { Octokit, RestEndpointMethodTypes } from "@octokit/rest";
+import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
+
+@injectable()
+export class GitHubAppSupport {
+
+ @inject(GithubApp) protected readonly githubApp: GithubApp;
+ @inject(UserDB) protected readonly userDB: UserDB;
+ @inject(TokenProvider) protected readonly tokenProvider: TokenProvider;
+
+ async getProviderRepositoriesForUser(params: { user: User, provider: string, hints?: object }): Promise {
+ const { user, provider, hints } = params;
+ const result: ProviderRepository[] = [];
+ const probot = this.githubApp.server?.probotApp;
+ if (!probot) {
+ return result;
+ }
+ if (params.provider !== "github.com") {
+ return result; // Just GitHub.com for now
+ }
+
+ const identity = user.identities.find(i => i.authProviderId === "Public-GitHub");
+ if (!identity) {
+ return result;
+ }
+ const usersGitHubAccount = identity.authName;
+
+ const appApi = await probot.auth();
+
+ const findInstallationForAccount = async (account: string) => {
+ try {
+ return await appApi.apps.getUserInstallation({ username: account })
+ } catch (error) {
+ if (error instanceof RequestError) {
+ // ignore 404 - not found
+ } else {
+ log.debug(error);
+ }
+ }
+ }
+ const listReposForInstallation = async (installation: RestEndpointMethodTypes["apps"]["getUserInstallation"]["response"]) => {
+ const sub = await probot.auth(installation.data.id);
+ try {
+ const accessibleRepos = await sub.apps.listReposAccessibleToInstallation();
+ return accessibleRepos.data.repositories.map(r => {
+ return {
+ name: r.name,
+ cloneUrl: r.clone_url,
+ account: r.owner.login,
+ accountAvatarUrl: r.owner.avatar_url,
+ updatedAt: r.updated_at,
+ installationId: installation.data.id,
+ installationUpdatedAt: installation.data.updated_at
+ };
+ });
+ } catch (error) {
+ if (error instanceof RequestError) {
+ // ignore 404 - not found
+ } else {
+ log.debug(error);
+ }
+ }
+ }
+
+ const listReposAccessibleToInstallation = async (account: string) => {
+ const installation = await findInstallationForAccount(account);
+ if (installation) {
+ return await listReposForInstallation(installation);
+ }
+ }
+
+ const ownRepos = await listReposAccessibleToInstallation(usersGitHubAccount);
+ if (ownRepos) {
+ result.push(...ownRepos);
+ }
+
+ const organizations: string[] = [];
+ try {
+ const token = await this.tokenProvider.getTokenForHost(user, provider);
+ if (token.scopes.includes("read:org")) {
+ const api = new Octokit({
+ auth: token.value
+ });
+ const { data } = await api.orgs.listMembershipsForAuthenticatedUser();
+ organizations.push(...data.map(o => o.organization.login));
+ }
+ } catch { }
+
+ // Add Orgs we learned about from previous installations
+ for (const org of (user.additionalData?.knownGitHubOrgs || [])) {
+ if (!organizations.includes(org)) {
+ organizations.unshift(org);
+ }
+ }
+ for (const org of organizations) {
+ const orgRepos = await listReposAccessibleToInstallation(org);
+ if (orgRepos) {
+ result.push(...orgRepos);
+ }
+ }
+
+ // If hints contain an additional installationId, let's try to add the repos
+ //
+ const installationId = parseInt((hints as any)?.installationId, 10);
+ if (!isNaN(installationId)) {
+ if (!result.some(r => r.installationId === installationId)) {
+ const installation = await appApi.apps.getInstallation({installation_id: installationId});
+ if (installation) {
+ const additional = await listReposForInstallation(installation);
+ if (additional) {
+ for (const repo of additional) {
+ if (result.some(r => r.account === repo.account && r.name === repo.name)) {
+ continue; // avoid duplicates when switching between "selected repos" and "all repos"
+ }
+
+ // add at the beginning
+ result.unshift(repo);
+
+ // optionally store newly identified organization of a user,
+ // just because the `listMembershipsForAuthenticatedUser` operation of the GH API
+ // requires an extra permission of the org's maintainer.
+ user.additionalData = user.additionalData || {}
+ user.additionalData.knownGitHubOrgs = user.additionalData.knownGitHubOrgs || [ ];
+ if (!user.additionalData.knownGitHubOrgs.includes(repo.account)) {
+ user.additionalData.knownGitHubOrgs.push(repo.account);
+ await this.userDB.updateUserPartial(user);
+ }
+ }
+ }
+ } else {
+ log.debug(`Provided installationId appears to be invalid.`, { installationId });
+ }
+
+ }
+ }
+
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/components/server/ee/src/prebuilds/github-app.ts b/components/server/ee/src/prebuilds/github-app.ts
index 3d1eba945637d3..c84816566636ee 100644
--- a/components/server/ee/src/prebuilds/github-app.ts
+++ b/components/server/ee/src/prebuilds/github-app.ts
@@ -7,7 +7,7 @@
import { Server, Probot, Context } from 'probot';
import { getPrivateKey } from '@probot/get-private-key';
import * as fs from 'fs-extra';
-import { injectable, inject, decorate } from 'inversify';
+import { injectable, inject } from 'inversify';
import { Env } from '../../../src/env';
import { AppInstallationDB, TracedWorkspaceDB, DBWithTracing, UserDB, WorkspaceDB } from '@gitpod/gitpod-db/lib';
import * as express from 'express';
@@ -32,8 +32,6 @@ import { Options, ApplicationFunctionOptions } from 'probot/lib/types';
* look at those values to begin with.
*/
-decorate(injectable(), Probot)
-
@injectable()
export class GithubApp {
@inject(AppInstallationDB) protected readonly appInstallationDB: AppInstallationDB;
@@ -103,6 +101,31 @@ export class GithubApp {
app.on(['pull_request.opened', 'pull_request.synchronize', 'pull_request.reopened'], async ctx => {
await this.handlePullRequest(ctx);
});
+
+ options.getRouter && options.getRouter('/reconfigure').get('/', async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ const gh = await app.auth();
+ const data = await gh.apps.getAuthenticated();
+ const slug = data.data.slug;
+
+ const state = req.query.state;
+ res.redirect(`https://github.com/apps/${slug}/installations/new?state=${state}`)
+ });
+ options.getRouter && options.getRouter('/setup').get('/', async (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ const state = req.query.state;
+ const installationId = req.query.installation_id;
+ const setupAction = req.query.setup_action;
+ const payload = { installationId, setupAction };
+ req.query
+
+ if (state) {
+ const url = this.env.hostUrl.with({ pathname: '/complete-auth', search: "message=payload:" + Buffer.from(JSON.stringify(payload), "utf-8").toString('base64') }).toString();
+ res.redirect(url);
+ } else {
+ const url = this.env.hostUrl.with({ pathname: 'install-github-app', search: `installation_id=${installationId}` }).toString();
+ res.redirect(url);
+ }
+
+ });
}
protected async handlePushEvent(ctx: Context): Promise {
diff --git a/components/server/ee/src/workspace/gitpod-server-impl.ts b/components/server/ee/src/workspace/gitpod-server-impl.ts
index 02d84b47193d0c..0dbd474819d26c 100644
--- a/components/server/ee/src/workspace/gitpod-server-impl.ts
+++ b/components/server/ee/src/workspace/gitpod-server-impl.ts
@@ -7,7 +7,7 @@
import { injectable, inject } from "inversify";
import { GitpodServerImpl } from "../../../src/workspace/gitpod-server-impl";
import { TraceContext } from "@gitpod/gitpod-protocol/lib/util/tracing";
-import { GitpodServer, GitpodClient, AdminGetListRequest, User, AdminGetListResult, Permission, AdminBlockUserRequest, AdminModifyRoleOrPermissionRequest, RoleOrPermission, AdminModifyPermanentWorkspaceFeatureFlagRequest, UserFeatureSettings, AdminGetWorkspacesRequest, WorkspaceAndInstance, GetWorkspaceTimeoutResult, WorkspaceTimeoutDuration, WorkspaceTimeoutValues, SetWorkspaceTimeoutResult, WorkspaceContext, CreateWorkspaceMode, WorkspaceCreationResult, PrebuiltWorkspaceContext, CommitContext, PrebuiltWorkspace, PermissionName, WorkspaceInstance, EduEmailDomain } from "@gitpod/gitpod-protocol";
+import { GitpodServer, GitpodClient, AdminGetListRequest, User, AdminGetListResult, Permission, AdminBlockUserRequest, AdminModifyRoleOrPermissionRequest, RoleOrPermission, AdminModifyPermanentWorkspaceFeatureFlagRequest, UserFeatureSettings, AdminGetWorkspacesRequest, WorkspaceAndInstance, GetWorkspaceTimeoutResult, WorkspaceTimeoutDuration, WorkspaceTimeoutValues, SetWorkspaceTimeoutResult, WorkspaceContext, CreateWorkspaceMode, WorkspaceCreationResult, PrebuiltWorkspaceContext, CommitContext, PrebuiltWorkspace, PermissionName, WorkspaceInstance, EduEmailDomain, ProviderRepository, PrebuildInfo } from "@gitpod/gitpod-protocol";
import { ResponseError } from "vscode-jsonrpc";
import { TakeSnapshotRequest, AdmissionLevel, ControlAdmissionRequest, StopWorkspacePolicy, DescribeWorkspaceRequest, SetTimeoutRequest } from "@gitpod/ws-manager/lib";
import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
@@ -39,6 +39,8 @@ import { ChargebeeService } from "../user/chargebee-service";
import { Chargebee as chargebee } from '@gitpod/gitpod-payment-endpoint/lib/chargebee';
import { EnvEE } from "../env";
+import { GitHubAppSupport } from "../github/github-app-support";
+
@injectable()
export class GitpodServerEEImpl extends GitpodServerImpl {
@inject(LicenseEvaluator) protected readonly licenseEvaluator: LicenseEvaluator;
@@ -65,6 +67,8 @@ export class GitpodServerEEImpl extends GitpodServerImpl this.guardAccess({kind: "snapshot", subject: s, workspaceOwnerID: workspace.ownerId}, "get")));
+ await Promise.all(snapshots.map(s => this.guardAccess({ kind: "snapshot", subject: s, workspaceOwnerID: workspace.ownerId }, "get")));
return snapshots.map(s => s.id);
} catch (e) {
@@ -401,7 +405,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl): Promise> {
this.requireEELicense(Feature.FeatureAdminDashboard);
- await this.guardAdminAccess("adminGetUsers", {req}, Permission.ADMIN_USERS);
+ await this.guardAdminAccess("adminGetUsers", { req }, Permission.ADMIN_USERS);
const span = opentracing.globalTracer().startSpan("adminGetUsers");
try {
@@ -419,7 +423,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
this.requireEELicense(Feature.FeatureAdminDashboard);
- await this.guardAdminAccess("adminGetUser", {id}, Permission.ADMIN_USERS);
+ await this.guardAdminAccess("adminGetUser", { id }, Permission.ADMIN_USERS);
let result: User | undefined;
const span = opentracing.globalTracer().startSpan("adminGetUser");
@@ -441,7 +445,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
this.requireEELicense(Feature.FeatureAdminDashboard);
- await this.guardAdminAccess("adminBlockUser", {req}, Permission.ADMIN_USERS);
+ await this.guardAdminAccess("adminBlockUser", { req }, Permission.ADMIN_USERS);
const span = opentracing.globalTracer().startSpan("adminBlockUser");
try {
@@ -474,7 +478,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
this.requireEELicense(Feature.FeatureAdminDashboard);
- await this.guardAdminAccess("adminDeleteUser", {id}, Permission.ADMIN_USERS);
+ await this.guardAdminAccess("adminDeleteUser", { id }, Permission.ADMIN_USERS);
const span = opentracing.globalTracer().startSpan("adminDeleteUser");
try {
@@ -490,7 +494,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
this.requireEELicense(Feature.FeatureAdminDashboard);
- await this.guardAdminAccess("adminModifyRoleOrPermission", {req}, Permission.ADMIN_USERS);
+ await this.guardAdminAccess("adminModifyRoleOrPermission", { req }, Permission.ADMIN_USERS);
const span = opentracing.globalTracer().startSpan("adminModifyRoleOrPermission");
span.log(req);
@@ -526,7 +530,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
this.requireEELicense(Feature.FeatureAdminDashboard);
- await this.guardAdminAccess("adminModifyPermanentWorkspaceFeatureFlag", {req}, Permission.ADMIN_USERS);
+ await this.guardAdminAccess("adminModifyPermanentWorkspaceFeatureFlag", { req }, Permission.ADMIN_USERS);
const span = opentracing.globalTracer().startSpan("adminModifyPermanentWorkspaceFeatureFlag");
span.log(req);
@@ -564,7 +568,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl> {
this.requireEELicense(Feature.FeatureAdminDashboard);
- await this.guardAdminAccess("adminGetWorkspaces", {req}, Permission.ADMIN_WORKSPACES);
+ await this.guardAdminAccess("adminGetWorkspaces", { req }, Permission.ADMIN_WORKSPACES);
const span = opentracing.globalTracer().startSpan("adminGetWorkspaces");
try {
@@ -580,7 +584,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
this.requireEELicense(Feature.FeatureAdminDashboard);
- await this.guardAdminAccess("adminGetWorkspace", {id}, Permission.ADMIN_WORKSPACES);
+ await this.guardAdminAccess("adminGetWorkspace", { id }, Permission.ADMIN_WORKSPACES);
let result: WorkspaceAndInstance | undefined;
const span = opentracing.globalTracer().startSpan("adminGetWorkspace");
@@ -602,16 +606,16 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
this.requireEELicense(Feature.FeatureAdminDashboard);
- await this.guardAdminAccess("adminForceStopWorkspace", {id}, Permission.ADMIN_WORKSPACES);
+ await this.guardAdminAccess("adminForceStopWorkspace", { id }, Permission.ADMIN_WORKSPACES);
const span = opentracing.globalTracer().startSpan("adminForceStopWorkspace");
- await this.internalStopWorkspace({ span }, id, undefined, StopWorkspacePolicy.IMMEDIATELY);
+ await this.internalStopWorkspace({ span }, id, undefined, StopWorkspacePolicy.IMMEDIATELY);
}
async adminRestoreSoftDeletedWorkspace(id: string): Promise {
this.requireEELicense(Feature.FeatureAdminDashboard);
- await this.guardAdminAccess("adminRestoreSoftDeletedWorkspace", {id}, Permission.ADMIN_WORKSPACES);
+ await this.guardAdminAccess("adminRestoreSoftDeletedWorkspace", { id }, Permission.ADMIN_WORKSPACES);
const span = opentracing.globalTracer().startSpan("adminRestoreSoftDeletedWorkspace");
await this.workspaceDb.trace({ span }).transaction(async db => {
@@ -633,10 +637,10 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
@@ -751,7 +755,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
- await this.guardAdminAccess("adminGetWorkspaces", {key}, Permission.ADMIN_API);
+ await this.guardAdminAccess("adminGetWorkspaces", { key }, Permission.ADMIN_API);
await this.licenseDB.store(uuidv4(), key);
await this.licenseEvaluator.reloadLicense();
@@ -1036,7 +1040,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
const user = this.checkUser('subscriptionCancel');
const chargebeeSubscriptionId = await this.doGetUserPaidSubscription(user.id, subscriptionId);
- await this.chargebeeService.cancelSubscription(chargebeeSubscriptionId , { userId: user.id }, { subscriptionId, chargebeeSubscriptionId });
+ await this.chargebeeService.cancelSubscription(chargebeeSubscriptionId, { userId: user.id }, { subscriptionId, chargebeeSubscriptionId });
}
async subscriptionCancelDowngrade(subscriptionId: string): Promise {
@@ -1426,4 +1430,38 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
+ const user = this.checkAndBlockUser("getProviderRepositoriesForUser");
+
+ const repositories = await this.githubAppSupport.getProviderRepositoriesForUser({ user, ...params });
+ return repositories;
+ }
+
+ public async getPrebuilds(teamId: string, projectName: string): Promise {
+ this.checkAndBlockUser("getPrebuilds");
+
+ const project = (await this.projectDB.findProjectsByTeam(teamId)).find(p => p.name === projectName);
+ if (project) {
+ const pws = (await this.workspaceDb.trace({}).findPrebuildsWithWorkpace(project.cloneUrl))[0];
+ if (pws) {
+ return [{
+ id: pws.prebuild.id,
+ startedAt: pws.prebuild.creationTime,
+ startedBy: "Owner",
+ teamId,
+ project: projectName,
+ branch: "main",
+ cloneUrl: pws.prebuild.cloneURL,
+ status: pws.prebuild.state
+ }]
+ }
+ }
+
+ return [];
+ }
+ //
+ //#endregion
}
diff --git a/components/server/src/auth/rate-limiter.ts b/components/server/src/auth/rate-limiter.ts
index 96c507cc391caf..3d75e1b61c82d3 100644
--- a/components/server/src/auth/rate-limiter.ts
+++ b/components/server/src/auth/rate-limiter.ts
@@ -156,6 +156,11 @@ function readConfig(): RateLimiterConfig {
"tsGetUnassignedSlot": { group: "default", points: 1 },
"tsReactivateSlot": { group: "default", points: 1 },
"tsReassignSlot": { group: "default", points: 1 },
+
+ "getProviderRepositoriesForUser": { group: "default", points: 1 },
+ "createProject": { group: "default", points: 1 },
+ "getProjects": { group: "default", points: 1 },
+ "getPrebuilds": { group: "default", points: 1 },
};
const fromEnv = JSON.parse(process.env.RATE_LIMITER_CONFIG || "{}")
diff --git a/components/server/src/workspace/gitpod-server-impl.ts b/components/server/src/workspace/gitpod-server-impl.ts
index 1f78466884aa1e..733c29faccd150 100644
--- a/components/server/src/workspace/gitpod-server-impl.ts
+++ b/components/server/src/workspace/gitpod-server-impl.ts
@@ -7,7 +7,7 @@
import { BlobServiceClient } from "@gitpod/content-service/lib/blobs_grpc_pb";
import { DownloadUrlRequest, DownloadUrlResponse, UploadUrlRequest, UploadUrlResponse } from '@gitpod/content-service/lib/blobs_pb';
import { AppInstallationDB, UserDB, UserMessageViewsDB, WorkspaceDB, DBWithTracing, TracedWorkspaceDB, DBGitpodToken, DBUser, UserStorageResourcesDB, ProjectDB, TeamDB } from '@gitpod/gitpod-db/lib';
-import { AuthProviderEntry, AuthProviderInfo, Branding, CommitContext, Configuration, CreateWorkspaceMode, DisposableCollection, GetWorkspaceTimeoutResult, GitpodClient, GitpodServer, GitpodToken, GitpodTokenType, InstallPluginsParams, PermissionName, PortVisibility, PrebuiltWorkspace, PrebuiltWorkspaceContext, PreparePluginUploadParams, ResolvedPlugins, ResolvePluginsParams, SetWorkspaceTimeoutResult, StartPrebuildContext, StartWorkspaceResult, Terms, Token, UninstallPluginParams, User, UserEnvVar, UserEnvVarValue, UserInfo, WhitelistedRepository, Workspace, WorkspaceContext, WorkspaceCreationResult, WorkspaceImageBuild, WorkspaceInfo, WorkspaceInstance, WorkspaceInstancePort, WorkspaceInstanceUser, WorkspaceTimeoutDuration, GuessGitTokenScopesParams, GuessedGitTokenScopes, Team, TeamMemberInfo, TeamMembershipInvite } from '@gitpod/gitpod-protocol';
+import { AuthProviderEntry, AuthProviderInfo, Branding, CommitContext, Configuration, CreateWorkspaceMode, DisposableCollection, GetWorkspaceTimeoutResult, GitpodClient, GitpodServer, GitpodToken, GitpodTokenType, InstallPluginsParams, PermissionName, PortVisibility, PrebuiltWorkspace, PrebuiltWorkspaceContext, PreparePluginUploadParams, ResolvedPlugins, ResolvePluginsParams, SetWorkspaceTimeoutResult, StartPrebuildContext, StartWorkspaceResult, Terms, Token, UninstallPluginParams, User, UserEnvVar, UserEnvVarValue, UserInfo, WhitelistedRepository, Workspace, WorkspaceContext, WorkspaceCreationResult, WorkspaceImageBuild, WorkspaceInfo, WorkspaceInstance, WorkspaceInstancePort, WorkspaceInstanceUser, WorkspaceTimeoutDuration, GuessGitTokenScopesParams, GuessedGitTokenScopes, Team, TeamMemberInfo, TeamMembershipInvite, CreateProjectParams, ProjectInfo, Project, ProviderRepository, PrebuildInfo } from '@gitpod/gitpod-protocol';
import { AccountStatement } from "@gitpod/gitpod-protocol/lib/accounting-protocol";
import { AdminBlockUserRequest, AdminGetListRequest, AdminGetListResult, AdminGetWorkspacesRequest, AdminModifyPermanentWorkspaceFeatureFlagRequest, AdminModifyRoleOrPermissionRequest, WorkspaceAndInstance } from '@gitpod/gitpod-protocol/lib/admin-protocol';
import { GetLicenseInfoResult, LicenseFeature, LicenseValidationResult } from '@gitpod/gitpod-protocol/lib/license-protocol';
@@ -49,7 +49,6 @@ import { WorkspaceStarter } from './workspace-starter';
import { HeadlessLogSources } from "@gitpod/gitpod-protocol/lib/headless-workspace-log";
import { WorkspaceLogService } from "./workspace-log-service";
-
@injectable()
export class GitpodServerImpl implements GitpodServer, Disposable {
@@ -1863,4 +1862,31 @@ export class GitpodServerImpl {
+ this.checkAndBlockUser("getProviderRepositoriesForUser");
+ return [];
+ }
+ public async createProject(params: CreateProjectParams): Promise {
+ this.checkUser("createProject");
+ const { name, cloneUrl, teamId, appInstallationId } = params;
+ return this.projectDB.createProject(name, cloneUrl, teamId, appInstallationId);
+ }
+ public async getProjects(teamId: string): Promise {
+ this.checkUser("getProjects");
+ const result: Project[] = [];
+ const toProjectInfo = (p: Project) => ({ ...p });
+ result.push(...(await this.projectDB.findProjectsByTeam(teamId)).map(toProjectInfo));
+ return result;
+ }
+ public async getPrebuilds(teamId: string, project: string): Promise {
+ this.checkAndBlockUser("getPrebuilds");
+ return [];
+ }
+ //
+ //#endregion
}