From 0e09d0aaaca1a323ad930c3fd4d25be84592011e Mon Sep 17 00:00:00 2001 From: Jan Keromnes Date: Thu, 17 Feb 2022 16:05:47 +0000 Subject: [PATCH] [server] Restrict snapshot access based on repository access MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also refactor: - Simplify GuardedSnapshot.workspace - WorkspaceLogAccessGuard → RepositoryResourceGuard - RepositoryService.canAccessHeadlessLogs → RepositoryProvider.hasReadAccess --- components/gitpod-protocol/src/protocol.ts | 5 +-- .../ee/src/auth/host-container-mapping.ts | 3 -- .../server/ee/src/github/container-module.ts | 13 ------ .../server/ee/src/prebuilds/github-service.ts | 36 ---------------- .../server/ee/src/prebuilds/gitlab-service.ts | 21 +--------- .../ee/src/workspace/gitpod-server-impl.ts | 4 +- .../server/src/auth/resource-access.spec.ts | 15 +++++-- components/server/src/auth/resource-access.ts | 41 +++++++++++-------- .../bitbucket-server-repository-provider.ts | 13 ++++-- .../bitbucket-repository-provider.ts | 5 +++ .../src/github/github-repository-provider.ts | 17 ++++++++ .../src/gitlab/gitlab-repository-provider.ts | 14 +++++++ .../server/src/repohost/repo-service.ts | 5 +-- .../src/repohost/repository-provider.ts | 1 + .../websocket/websocket-connection-manager.ts | 4 +- .../src/workspace/gitpod-server-impl.ts | 14 ++++++- .../src/workspace/headless-log-controller.ts | 4 +- 17 files changed, 104 insertions(+), 111 deletions(-) delete mode 100644 components/server/ee/src/github/container-module.ts delete mode 100644 components/server/ee/src/prebuilds/github-service.ts diff --git a/components/gitpod-protocol/src/protocol.ts b/components/gitpod-protocol/src/protocol.ts index 2768b2955f2029..faa782e5c11006 100644 --- a/components/gitpod-protocol/src/protocol.ts +++ b/components/gitpod-protocol/src/protocol.ts @@ -815,15 +815,14 @@ export namespace WithSnapshot { } } -export interface WithPrebuild { - snapshotBucketId: string; +export interface WithPrebuild extends WithSnapshot { prebuildWorkspaceId: string; wasPrebuilt: true; } export namespace WithPrebuild { export function is(context: any): context is WithPrebuild { return context - && 'snapshotBucketId' in context + && WithSnapshot.is(context) && 'prebuildWorkspaceId' in context && 'wasPrebuilt' in context; } diff --git a/components/server/ee/src/auth/host-container-mapping.ts b/components/server/ee/src/auth/host-container-mapping.ts index 539a8131ddba76..90590270d29164 100644 --- a/components/server/ee/src/auth/host-container-mapping.ts +++ b/components/server/ee/src/auth/host-container-mapping.ts @@ -8,7 +8,6 @@ import { injectable, interfaces } from "inversify"; import { HostContainerMapping } from "../../../src/auth/host-container-mapping"; import { gitlabContainerModuleEE } from "../gitlab/container-module"; import { bitbucketContainerModuleEE } from "../bitbucket/container-module"; -import { gitHubContainerModuleEE } from "../github/container-module"; @injectable() export class HostContainerMappingEE extends HostContainerMapping { @@ -24,8 +23,6 @@ export class HostContainerMappingEE extends HostContainerMapping { // case "BitbucketServer": // FIXME // return (modules || []).concat([bitbucketContainerModuleEE]); - case "GitHub": - return (modules || []).concat([gitHubContainerModuleEE]); default: return modules; } diff --git a/components/server/ee/src/github/container-module.ts b/components/server/ee/src/github/container-module.ts deleted file mode 100644 index b21b8a6e0a9e66..00000000000000 --- a/components/server/ee/src/github/container-module.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * 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 { ContainerModule } from "inversify"; -import { RepositoryService } from "../../../src/repohost/repo-service"; -import { GitHubService } from "../prebuilds/github-service"; - -export const gitHubContainerModuleEE = new ContainerModule((_bind, _unbind, _isBound, rebind) => { - rebind(RepositoryService).to(GitHubService).inSingletonScope(); -}); diff --git a/components/server/ee/src/prebuilds/github-service.ts b/components/server/ee/src/prebuilds/github-service.ts deleted file mode 100644 index d57c3aa8f2500d..00000000000000 --- a/components/server/ee/src/prebuilds/github-service.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * 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 { RepositoryService } from "../../../src/repohost/repo-service"; -import { inject, injectable } from "inversify"; -import { CommitContext, User, WorkspaceContext } from "@gitpod/gitpod-protocol"; -import { GitHubGraphQlEndpoint } from "../../../src/github/api"; - -@injectable() -export class GitHubService extends RepositoryService { - @inject(GitHubGraphQlEndpoint) protected readonly githubQueryApi: GitHubGraphQlEndpoint; - - async canAccessHeadlessLogs(user: User, context: WorkspaceContext): Promise { - if (!CommitContext.is(context)) { - return false; - } - - try { - // If you have no "viewerPermission" on a repository you may not access it's headless logs - // Ref: https://docs.github.com/en/graphql/reference/enums#repositorypermission - const result: any = await this.githubQueryApi.runQuery(user, ` - query { - repository(name: "${context.repository.name}", owner: "${context.repository.owner}") { - viewerPermission - } - } - `); - return result.data.repository !== null; - } catch (err) { - return false; - } - } -} \ No newline at end of file diff --git a/components/server/ee/src/prebuilds/gitlab-service.ts b/components/server/ee/src/prebuilds/gitlab-service.ts index 21ef393f03b4ab..fcaa4e247daa25 100644 --- a/components/server/ee/src/prebuilds/gitlab-service.ts +++ b/components/server/ee/src/prebuilds/gitlab-service.ts @@ -5,7 +5,7 @@ */ import { RepositoryService } from "../../../src/repohost/repo-service"; -import { CommitContext, User, WorkspaceContext } from "@gitpod/gitpod-protocol"; +import { User } from "@gitpod/gitpod-protocol"; import { inject, injectable } from "inversify"; import { GitLabApi, GitLab } from "../../../src/gitlab/api"; import { AuthProviderParams } from "../../../src/auth/auth-provider"; @@ -66,25 +66,6 @@ export class GitlabService extends RepositoryService { log.info('Installed Webhook for ' + cloneUrl, { cloneUrl, userId: user.id }); } - async canAccessHeadlessLogs(user: User, context: WorkspaceContext): Promise { - if (!CommitContext.is(context)) { - return false; - } - const { owner, name: repoName } = context.repository; - - try { - // If we can "see" a project we are allowed to access it's headless logs - const api = await this.api.create(user); - const response = await api.Projects.show(`${owner}/${repoName}`); - if (GitLab.ApiError.is(response)) { - return false; - } - return true; - } catch (err) { - return false; - } - } - protected getHookUrl() { return this.config.hostUrl.with({ pathname: GitLabApp.path diff --git a/components/server/ee/src/workspace/gitpod-server-impl.ts b/components/server/ee/src/workspace/gitpod-server-impl.ts index c016da403f6060..89855d9ef36a23 100644 --- a/components/server/ee/src/workspace/gitpod-server-impl.ts +++ b/components/server/ee/src/workspace/gitpod-server-impl.ts @@ -359,7 +359,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl { if (!workspace || workspace.ownerId !== userId) { throw new ResponseError(ErrorCodes.NOT_FOUND, `Workspace ${workspaceId} does not exist.`); } - await this.guardAccess({ kind: "snapshot", subject: undefined, workspaceOwnerID: workspace.ownerId, workspaceID: workspace.id }, "create"); + await this.guardAccess({ kind: "snapshot", subject: undefined, workspace }, "create"); return workspace; } @@ -406,7 +406,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl { } const snapshots = await this.workspaceDb.trace(ctx).findSnapshotsByWorkspaceId(workspaceId); - await Promise.all(snapshots.map(s => this.guardAccess({ kind: "snapshot", subject: s, workspaceOwnerID: workspace.ownerId }, "get"))); + await Promise.all(snapshots.map(s => this.guardAccess({ kind: "snapshot", subject: s, workspace }, "get"))); return snapshots.map(s => s.id); } diff --git a/components/server/src/auth/resource-access.spec.ts b/components/server/src/auth/resource-access.spec.ts index 7c96b2bb4d4523..118f29614fc3e0 100644 --- a/components/server/src/auth/resource-access.spec.ts +++ b/components/server/src/auth/resource-access.spec.ts @@ -178,7 +178,7 @@ import { UserEnvVar } from "@gitpod/gitpod-protocol/lib/protocol"; guard: new TokenResourceGuard(workspaceResource.subject.ownerId, [ "resource:" + ScopedResourceGuard.marshalResourceScope({ kind: "snapshot", subjectID: ScopedResourceGuard.SNAPSHOT_WORKSPACE_SUBJECT_ID_PREFIX + workspaceResource.subject.id, operations: ["create"] }), ]), - resource: { kind: "snapshot", subject: undefined, workspaceID: workspaceResource.subject.id, workspaceOwnerID: workspaceResource.subject.ownerId }, + resource: { kind: "snapshot", subject: undefined, workspace: workspaceResource.subject }, operation: "create", expectation: true, }, @@ -187,7 +187,7 @@ import { UserEnvVar } from "@gitpod/gitpod-protocol/lib/protocol"; guard: new TokenResourceGuard(workspaceResource.subject.ownerId, [ "resource:" + ScopedResourceGuard.marshalResourceScope({ kind: "snapshot", subjectID: workspaceResource.subject.id, operations: ["create"] }), ]), - resource: { kind: "snapshot", subject: undefined, workspaceID: workspaceResource.subject.id, workspaceOwnerID: workspaceResource.subject.ownerId }, + resource: { kind: "snapshot", subject: undefined, workspace: workspaceResource.subject }, operation: "create", expectation: false, }, @@ -196,10 +196,19 @@ import { UserEnvVar } from "@gitpod/gitpod-protocol/lib/protocol"; guard: new TokenResourceGuard(workspaceResource.subject.ownerId, [ "resource:" + ScopedResourceGuard.marshalResourceScope({ kind: "snapshot", subjectID: workspaceResource.subject.id, operations: ["create"] }), ]), - resource: { kind: "snapshot", subject: undefined, workspaceID: workspaceResource.subject.id, workspaceOwnerID: "other_owner" }, + resource: { kind: "snapshot", subject: undefined, workspace: { ...workspaceResource.subject, ownerId: "other_owner" } }, operation: "create", expectation: false, }, + { + name: "snaphshot get", + guard: new TokenResourceGuard(workspaceResource.subject.ownerId, [ + "resource:" + ScopedResourceGuard.marshalResourceScope({ kind: "snapshot", subjectID: ScopedResourceGuard.SNAPSHOT_WORKSPACE_SUBJECT_ID_PREFIX + workspaceResource.subject.id, operations: ["get"] }), + ]), + resource: { kind: "snapshot", subject: undefined, workspace: workspaceResource.subject }, + operation: "get", + expectation: true, + }, ] await Promise.all(tests.map(async t => { diff --git a/components/server/src/auth/resource-access.ts b/components/server/src/auth/resource-access.ts index f90a76be59593f..59625822534287 100644 --- a/components/server/src/auth/resource-access.ts +++ b/components/server/src/auth/resource-access.ts @@ -4,7 +4,8 @@ * See License-AGPL.txt in the project root for license information. */ -import { ContextURL, GitpodToken, Snapshot, Team, TeamMemberInfo, Token, User, UserEnvVar, Workspace, WorkspaceInstance } from "@gitpod/gitpod-protocol"; +import { CommitContext, ContextURL, GitpodToken, Snapshot, Team, TeamMemberInfo, Token, User, UserEnvVar, Workspace, WorkspaceInstance } from "@gitpod/gitpod-protocol"; +import { UnauthorizedError } from "../errors"; import { HostContextProvider } from "./host-context-provider"; declare var resourceInstance: GuardedResource; @@ -61,9 +62,8 @@ export interface GuardedUser { export interface GuardedSnapshot { kind: "snapshot"; - subject: Snapshot | undefined; - workspaceOwnerID: string; - workspaceID?: string; + subject?: Snapshot; + workspace: Workspace; } export interface GuardedUserStorage { @@ -177,7 +177,7 @@ export class OwnerResourceGuard implements ResourceAccessGuard { case "gitpodToken": return resource.subject.user.id === this.userId; case "snapshot": - return resource.workspaceOwnerID === this.userId; + return resource.workspace.ownerId === this.userId; case "token": return resource.tokenOwnerID === this.userId; case "user": @@ -360,10 +360,7 @@ export namespace ScopedResourceGuard { if (resource.subject) { return resource.subject.id; } - if (resource.workspaceID) { - return SNAPSHOT_WORKSPACE_SUBJECT_ID_PREFIX + resource.workspaceID; - } - return undefined; + return SNAPSHOT_WORKSPACE_SUBJECT_ID_PREFIX + resource.workspace.id; case "token": return resource.subject.value; case "user": @@ -464,13 +461,13 @@ export namespace TokenResourceGuard { } -export class WorkspaceLogAccessGuard implements ResourceAccessGuard { +export class RepositoryResourceGuard implements ResourceAccessGuard { constructor( protected readonly user: User, protected readonly hostContextProvider: HostContextProvider) {} async canAccess(resource: GuardedResource, operation: ResourceAccessOp): Promise { - if (resource.kind !== 'workspaceLog') { + if (resource.kind !== 'workspaceLog' && resource.kind !== 'snapshot') { return false; } // only get operations are supported @@ -478,9 +475,9 @@ export class WorkspaceLogAccessGuard implements ResourceAccessGuard { return false; } - // Check if user can access repositories headless logs - const ws = resource.subject; - const contextURL = ContextURL.getNormalizedURL(ws); + // Check if user has at least read access to the repository + const workspace = resource.kind === 'snapshot' ? resource.workspace : resource.subject; + const contextURL = ContextURL.getNormalizedURL(workspace); if (!contextURL) { throw new Error(`unable to parse ContextURL: ${contextURL}`); } @@ -488,11 +485,19 @@ export class WorkspaceLogAccessGuard implements ResourceAccessGuard { if (!hostContext) { throw new Error(`no HostContext found for hostname: ${contextURL.hostname}`); } - - const svcs = hostContext.services; - if (!svcs) { + const { authProvider } = hostContext; + const identity = User.getIdentity(this.user, authProvider.authProviderId); + if (!identity) { + throw UnauthorizedError.create(contextURL.hostname, authProvider.info.requirements?.default || [], "missing-identity"); + } + const { services } = hostContext; + if (!services) { throw new Error(`no services found in HostContext for hostname: ${contextURL.hostname}`); } - return svcs.repositoryService.canAccessHeadlessLogs(this.user, ws.context); + if (!CommitContext.is(workspace.context)) { + return false; + } + const { owner, name: repo } = workspace.context.repository; + return services.repositoryProvider.hasReadAccess(this.user, owner, repo); } } \ No newline at end of file diff --git a/components/server/src/bitbucket-server/bitbucket-server-repository-provider.ts b/components/server/src/bitbucket-server/bitbucket-server-repository-provider.ts index 443852724e382c..138aca98f25ccc 100644 --- a/components/server/src/bitbucket-server/bitbucket-server-repository-provider.ts +++ b/components/server/src/bitbucket-server/bitbucket-server-repository-provider.ts @@ -11,10 +11,6 @@ import { RepositoryProvider } from '../repohost/repository-provider'; @injectable() export class BitbucketServerRepositoryProvider implements RepositoryProvider { - getUserRepos(user: User): Promise { - throw new Error("getUserRepos not implemented."); - } - async getRepo(user: User, owner: string, name: string): Promise { // const api = await this.apiFactory.create(user); // const repo = (await api.repositories.get({ workspace: owner, repo_slug: name })).data; @@ -100,4 +96,13 @@ export class BitbucketServerRepositoryProvider implements RepositoryProvider { // authorAvatarUrl: commit.author?.user?.links?.avatar?.href, // }; } + + getUserRepos(user: User): Promise { + throw new Error("getUserRepos not implemented."); + } + + hasReadAccess(user: User, owner: string, repo: string): Promise { + throw new Error("Method not implemented."); + } + } diff --git a/components/server/src/bitbucket/bitbucket-repository-provider.ts b/components/server/src/bitbucket/bitbucket-repository-provider.ts index e6a7ded2f974bc..8f0f5ff7c4ee42 100644 --- a/components/server/src/bitbucket/bitbucket-repository-provider.ts +++ b/components/server/src/bitbucket/bitbucket-repository-provider.ts @@ -103,4 +103,9 @@ export class BitbucketRepositoryProvider implements RepositoryProvider { // FIXME(janx): Not implemented yet return []; } + + async hasReadAccess(user: User, owner: string, repo: string): Promise { + // FIXME(janx): Not implemented yet + return false; + } } diff --git a/components/server/src/github/github-repository-provider.ts b/components/server/src/github/github-repository-provider.ts index 52707ed244bd0e..f0a11e0ce24c54 100644 --- a/components/server/src/github/github-repository-provider.ts +++ b/components/server/src/github/github-repository-provider.ts @@ -128,4 +128,21 @@ export class GithubRepositoryProvider implements RepositoryProvider { }`); return (result.data.viewer?.repositoriesContributedTo?.edges || []).map((edge: any) => edge.node.url) } + + async hasReadAccess(user: User, owner: string, repo: string): Promise { + try { + // If you have no "viewerPermission" on a repository you may not read it + // Ref: https://docs.github.com/en/graphql/reference/enums#repositorypermission + const result: any = await this.githubQueryApi.runQuery(user, ` + query { + repository(name: "${repo}", owner: "${owner}") { + viewerPermission + } + } + `); + return result.data.repository !== null; + } catch (err) { + return false; + } + } } diff --git a/components/server/src/gitlab/gitlab-repository-provider.ts b/components/server/src/gitlab/gitlab-repository-provider.ts index b534e0ca6c0e84..154f1bc82969a7 100644 --- a/components/server/src/gitlab/gitlab-repository-provider.ts +++ b/components/server/src/gitlab/gitlab-repository-provider.ts @@ -99,4 +99,18 @@ export class GitlabRepositoryProvider implements RepositoryProvider { // FIXME(janx): Not implemented yet return []; } + + async hasReadAccess(user: User, owner: string, repo: string): Promise { + try { + // If we can "see" a project we are allowed to read it + const api = await this.gitlab.create(user); + const response = await api.Projects.show(`${owner}/${repo}`); + if (GitLab.ApiError.is(response)) { + return false; + } + return true; + } catch (err) { + return false; + } + } } diff --git a/components/server/src/repohost/repo-service.ts b/components/server/src/repohost/repo-service.ts index b0104794dcc04a..3740e919f8209f 100644 --- a/components/server/src/repohost/repo-service.ts +++ b/components/server/src/repohost/repo-service.ts @@ -4,7 +4,7 @@ * See License-AGPL.txt in the project root for license information. */ -import { User, WorkspaceContext } from "@gitpod/gitpod-protocol"; +import { User } from "@gitpod/gitpod-protocol"; import { injectable } from "inversify"; @injectable() @@ -18,7 +18,4 @@ export class RepositoryService { throw new Error('unsupported'); } - async canAccessHeadlessLogs(user: User, context: WorkspaceContext): Promise { - return false; - } } \ No newline at end of file diff --git a/components/server/src/repohost/repository-provider.ts b/components/server/src/repohost/repository-provider.ts index f5fa0480ad5918..48c056c19eca0b 100644 --- a/components/server/src/repohost/repository-provider.ts +++ b/components/server/src/repohost/repository-provider.ts @@ -14,4 +14,5 @@ export interface RepositoryProvider { getBranches(user: User, owner: string, repo: string): Promise; getCommitInfo(user: User, owner: string, repo: string, ref: string): Promise; getUserRepos(user: User): Promise; + hasReadAccess(user: User, owner: string, repo: string): Promise; } \ No newline at end of file diff --git a/components/server/src/websocket/websocket-connection-manager.ts b/components/server/src/websocket/websocket-connection-manager.ts index 3baee6ef17a3a5..4a0b8cbe191642 100644 --- a/components/server/src/websocket/websocket-connection-manager.ts +++ b/components/server/src/websocket/websocket-connection-manager.ts @@ -15,7 +15,7 @@ import { ErrorCodes as RPCErrorCodes, MessageConnection, ResponseError } from "v import { AllAccessFunctionGuard, FunctionAccessGuard, WithFunctionAccessGuard } from "../auth/function-access"; import { HostContextProvider } from "../auth/host-context-provider"; import { RateLimiter, RateLimiterConfig, UserRateLimiter } from "../auth/rate-limiter"; -import { CompositeResourceAccessGuard, OwnerResourceGuard, ResourceAccessGuard, SharedWorkspaceAccessGuard, TeamMemberResourceGuard, WithResourceAccessGuard, WorkspaceLogAccessGuard } from "../auth/resource-access"; +import { CompositeResourceAccessGuard, OwnerResourceGuard, ResourceAccessGuard, SharedWorkspaceAccessGuard, TeamMemberResourceGuard, WithResourceAccessGuard, RepositoryResourceGuard } from "../auth/resource-access"; import { takeFirst } from "../express-util"; import { increaseApiCallCounter, increaseApiConnectionClosedCounter, increaseApiConnectionCounter, increaseApiCallUserCounter, observeAPICallsDuration, apiCallDurationHistogram } from "../prometheus-metrics"; import { GitpodServerImpl } from "../workspace/gitpod-server-impl"; @@ -191,7 +191,7 @@ export class WebsocketConnectionManager implements ConnectionHandler { new OwnerResourceGuard(user.id), new TeamMemberResourceGuard(user.id), new SharedWorkspaceAccessGuard(), - new WorkspaceLogAccessGuard(user, this.hostContextProvider), + new RepositoryResourceGuard(user, this.hostContextProvider), ]); } else { resourceGuard = { canAccess: async () => false }; diff --git a/components/server/src/workspace/gitpod-server-impl.ts b/components/server/src/workspace/gitpod-server-impl.ts index c49559169983ee..6ae5f3aff371f2 100644 --- a/components/server/src/workspace/gitpod-server-impl.ts +++ b/components/server/src/workspace/gitpod-server-impl.ts @@ -6,7 +6,7 @@ import { DownloadUrlRequest, DownloadUrlResponse, UploadUrlRequest, UploadUrlResponse } from '@gitpod/content-service/lib/blobs_pb'; import { AppInstallationDB, UserDB, UserMessageViewsDB, WorkspaceDB, DBWithTracing, TracedWorkspaceDB, DBGitpodToken, DBUser, UserStorageResourcesDB, TeamDB, InstallationAdminDB, ProjectDB } from '@gitpod/gitpod-db/lib'; -import { AuthProviderEntry, AuthProviderInfo, CommitContext, Configuration, CreateWorkspaceMode, DisposableCollection, GetWorkspaceTimeoutResult, GitpodClient as GitpodApiClient, 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, Project, ProviderRepository, TeamMemberRole, WithDefaultConfig, FindPrebuildsParams, PrebuildWithStatus, StartPrebuildResult, ClientHeaderFields, Permission } from '@gitpod/gitpod-protocol'; +import { AuthProviderEntry, AuthProviderInfo, CommitContext, Configuration, CreateWorkspaceMode, DisposableCollection, GetWorkspaceTimeoutResult, GitpodClient as GitpodApiClient, 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, Project, ProviderRepository, TeamMemberRole, WithDefaultConfig, FindPrebuildsParams, PrebuildWithStatus, StartPrebuildResult, ClientHeaderFields, Permission, SnapshotContext } 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'; @@ -832,6 +832,18 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable { this.mayStartWorkspace(ctx, user, runningInstancesPromise), ]); + if (SnapshotContext.is(context)) { + const snapshot = await this.workspaceDb.trace(ctx).findSnapshotById(context.snapshotId); + if (!snapshot) { + throw new ResponseError(ErrorCodes.NOT_FOUND, "No snapshot with id '" + context.snapshotId + "' found."); + } + const workspace = await this.workspaceDb.trace(ctx).findById(snapshot.originalWorkspaceId); + if (!workspace) { + throw new ResponseError(ErrorCodes.NOT_FOUND, "No workspace with id '" + snapshot.originalWorkspaceId + "' found."); + } + await this.guardAccess({ kind: "snapshot", subject: snapshot, workspace }, "get"); + } + // if we're forced to use the default config, mark the context as such if (!!options.forceDefaultConfig) { context = WithDefaultConfig.mark(context); diff --git a/components/server/src/workspace/headless-log-controller.ts b/components/server/src/workspace/headless-log-controller.ts index bb6b60b61d50fe..6734beff260908 100644 --- a/components/server/src/workspace/headless-log-controller.ts +++ b/components/server/src/workspace/headless-log-controller.ts @@ -8,7 +8,7 @@ import { inject, injectable } from "inversify"; import * as express from 'express'; import { HEADLESS_LOG_STREAM_STATUS_CODE, Queue, TeamMemberInfo, User, Workspace, WorkspaceInstance } from "@gitpod/gitpod-protocol"; import { log } from '@gitpod/gitpod-protocol/lib/util/logging'; -import { CompositeResourceAccessGuard, OwnerResourceGuard, TeamMemberResourceGuard, WorkspaceLogAccessGuard } from "../auth/resource-access"; +import { CompositeResourceAccessGuard, OwnerResourceGuard, TeamMemberResourceGuard, RepositoryResourceGuard } from "../auth/resource-access"; import { DBWithTracing, TracedWorkspaceDB } from "@gitpod/gitpod-db/lib/traced-db"; import { WorkspaceDB } from "@gitpod/gitpod-db/lib/workspace-db"; import { TeamDB } from "@gitpod/gitpod-db/lib/team-db"; @@ -170,7 +170,7 @@ export class HeadlessLogController { const resourceGuard = new CompositeResourceAccessGuard([ new OwnerResourceGuard(user.id), new TeamMemberResourceGuard(user.id), - new WorkspaceLogAccessGuard(user, this.hostContextProvider), + new RepositoryResourceGuard(user, this.hostContextProvider), ]); if (!await resourceGuard.canAccess({ kind: 'workspaceLog', subject: workspace, teamMembers }, 'get')) { res.sendStatus(403);