Skip to content

Commit

Permalink
[server] allow team members access to prebuilds
Browse files Browse the repository at this point in the history
  • Loading branch information
svenefftinge committed Aug 31, 2021
1 parent 38502cf commit 5b6fec8
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 47 deletions.
13 changes: 8 additions & 5 deletions components/server/ee/src/workspace/gitpod-server-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl<GitpodClient, GitpodSer
if (!runningInstance) {
throw new ResponseError(ErrorCodes.NOT_FOUND, "Can only set keep-alive for running workspaces");
}
await this.guardAccess({ kind: "workspaceInstance", subject: runningInstance, workspaceOwnerID: workspace.ownerId, workspaceIsShared: workspace.shareable || false }, "update");
await this.guardAccess({ kind: "workspaceInstance", subject: runningInstance, workspace: workspace }, "update");

// if any other running instance has a custom timeout other than the user's default, we'll reset that timeout
const client = await this.workspaceManagerClientProvider.get(runningInstance.region);
Expand Down Expand Up @@ -236,7 +236,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl<GitpodClient, GitpodSer
log.warn({ userId: user.id, workspaceId }, 'Can only get keep-alive for running workspaces');
return { duration: "30m", canChange };
}
await this.guardAccess({ kind: "workspaceInstance", subject: runningInstance, workspaceOwnerID: workspace.ownerId, workspaceIsShared: workspace.shareable || false }, "get");
await this.guardAccess({ kind: "workspaceInstance", subject: runningInstance, workspace: workspace }, "get");

const req = new DescribeWorkspaceRequest();
req.setId(runningInstance.id);
Expand Down Expand Up @@ -306,7 +306,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl<GitpodClient, GitpodSer

const instance = await this.workspaceDb.trace({ span }).findRunningInstance(id);
if (instance) {
await this.guardAccess({ kind: "workspaceInstance", subject: instance, workspaceOwnerID: workspace.ownerId, workspaceIsShared: workspace.shareable || false }, "update");
await this.guardAccess({ kind: "workspaceInstance", subject: instance, workspace: workspace }, "update");

const req = new ControlAdmissionRequest();
req.setId(instance.id);
Expand Down Expand Up @@ -349,7 +349,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl<GitpodClient, GitpodSer
throw new ResponseError(ErrorCodes.NOT_FOUND, `Workspace ${workspaceId} has no running instance`);
}

await this.guardAccess({ kind: "workspaceInstance", subject: instance, workspaceOwnerID: workspace.ownerId, workspaceIsShared: workspace.shareable || false }, "get");
await this.guardAccess({ kind: "workspaceInstance", subject: instance, workspace}, "get");
await this.guardAccess({ kind: "snapshot", subject: undefined, workspaceOwnerID: workspace.ownerId, workspaceID: workspace.id }, "create");

const client = await this.workspaceManagerClientProvider.get(instance.region);
Expand Down Expand Up @@ -611,7 +611,10 @@ export class GitpodServerEEImpl extends GitpodServerImpl<GitpodClient, GitpodSer
await this.guardAdminAccess("adminForceStopWorkspace", { id }, Permission.ADMIN_WORKSPACES);

const span = opentracing.globalTracer().startSpan("adminForceStopWorkspace");
await this.internalStopWorkspace({ span }, id, undefined, StopWorkspacePolicy.IMMEDIATELY);
const workspace = await this.workspaceDb.trace({ span }).findById(id);
if (workspace) {
await this.internalStopWorkspace({ span }, workspace, StopWorkspacePolicy.IMMEDIATELY);
}
}

async adminRestoreSoftDeletedWorkspace(id: string): Promise<void> {
Expand Down
2 changes: 1 addition & 1 deletion components/server/src/auth/bearer-authenticator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ interface BearerAuthError extends Error {
code: typeof bearerAuthCode
}
export function isBearerAuthError(error: Error): error is BearerAuthError {
return 'code' in error && error['code'] === bearerAuthCode;
return 'code' in error && (error as any)['code'] === bearerAuthCode;
}
function createBearerAuthError(message: string): BearerAuthError {
return Object.assign(new Error(message), { code: bearerAuthCode } as { code: typeof bearerAuthCode });
Expand Down
71 changes: 70 additions & 1 deletion components/server/src/auth/resource-access.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import { suite, test } from "mocha-typescript";
import * as chai from 'chai';
const expect = chai.expect;
import { TokenResourceGuard, ScopedResourceGuard, GuardedResource, ResourceAccessOp, GuardEnvVar, WorkspaceEnvVarAccessGuard } from "./resource-access";
import { TokenResourceGuard, ScopedResourceGuard, GuardedResource, ResourceAccessOp, GuardEnvVar, WorkspaceEnvVarAccessGuard, TeamMemberResourceGuard, GuardedWorkspace } from "./resource-access";
import { UserEnvVar } from "@gitpod/gitpod-protocol/lib/protocol";

@suite class TestResourceAccess {
Expand Down Expand Up @@ -58,6 +58,75 @@ import { UserEnvVar } from "@gitpod/gitpod-protocol/lib/protocol";
});
}

@test public async teamMemberResourceGuard() {
const tests: {
name: string
userId: string
resource: GuardedWorkspace
isAllowed: boolean
}[] = [
{
name: "member of team - no prebuild",
userId: "foo",
isAllowed: false,
resource: {
kind: "workspace",
subject: {
id: "foobar",
ownerId: "foo",
type: "regular",
} as any,
teamMembers: [{
userId:"foo",
memberSince: "2021-08-31",
role: "member"
}]
},
},
{
name: "member of team - prebuild",
userId: "foo",
isAllowed: true,
resource: {
kind: "workspace",
subject: {
id: "foobar",
ownerId: "foo",
type: "prebuild",
} as any,
teamMembers: [{
userId:"foo",
memberSince: "2021-08-31",
role: "member"
}]
},
},
{
name: "not a member of team - prebuild",
userId: "bar",
isAllowed: false,
resource: {
kind: "workspace",
subject: {
id: "foobar",
ownerId: "foo",
type: "prebuild",
} as any,
teamMembers: [{
userId:"foo",
memberSince: "2021-08-31",
role: "member"
}]
},
},
];

for (const t of tests) {
const res = new TeamMemberResourceGuard(t.userId);
expect(await res.canAccess(t.resource, "get")).to.be.eq(t.isAllowed, `"${t.name}" expected canAccess(resource, "get") === ${t.isAllowed}, but was ${res}`);
}
}

@test public async tokenResourceGuardCanAccess() {
const workspaceResource: GuardedResource = { kind: "workspace", subject: { id: "wsid", ownerId: "foo" } as any };
const tests: {
Expand Down
35 changes: 31 additions & 4 deletions components/server/src/auth/resource-access.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,14 @@ export function isGuardedResourceKind(kind: any): kind is GuardedResourceKind {
export interface GuardedWorkspace {
kind: "workspace";
subject: Workspace;
teamMembers?: TeamMemberInfo[];
}

export interface GuardedWorkspaceInstance {
kind: "workspaceInstance";
subject: WorkspaceInstance | undefined;
workspaceOwnerID: string;
workspaceIsShared: boolean;
workspace: Workspace;
teamMembers?: TeamMemberInfo[];
}

export interface GuardedUser {
Expand Down Expand Up @@ -102,6 +103,7 @@ export interface GuardedToken {
export interface GuardedWorkspaceLog {
kind: "workspaceLog";
subject: Workspace;
teamMembers?: TeamMemberInfo[];
}

export type ResourceAccessOp =
Expand Down Expand Up @@ -135,6 +137,31 @@ export class CompositeResourceAccessGuard implements ResourceAccessGuard {

}

export class TeamMemberResourceGuard implements ResourceAccessGuard {

constructor(readonly userId: string) { }

async canAccess(resource: GuardedResource, operation: ResourceAccessOp): Promise<boolean> {
switch (resource.kind) {
case "workspace":
return await this.hasAccessToWorkspace(resource.subject, resource.teamMembers);
case "workspaceInstance":
return await this.hasAccessToWorkspace(resource.workspace, resource.teamMembers);
case "workspaceLog":
return await this.hasAccessToWorkspace(resource.subject, resource.teamMembers);
}
return false;
}

protected async hasAccessToWorkspace(workspace: Workspace, teamMembers?: TeamMemberInfo[]): Promise<boolean> {
// prebuilds are accessible by team members.
if (workspace.type === 'prebuild' && !!teamMembers) {
return teamMembers.some(m => m.userId === this.userId);
}
return false;
}
}

/**
* OwnerResourceGuard grants access to resources if the user asking for access is the owner of that
* resource.
Expand All @@ -160,7 +187,7 @@ export class OwnerResourceGuard implements ResourceAccessGuard {
case "workspace":
return resource.subject.ownerId === this.userId;
case "workspaceInstance":
return resource.workspaceOwnerID === this.userId;
return resource.workspace.ownerId === this.userId;
case "envVar":
return resource.subject.userId === this.userId;
case "team":
Expand Down Expand Up @@ -190,7 +217,7 @@ export class SharedWorkspaceAccessGuard implements ResourceAccessGuard {
case "workspace":
return resource.subject.shareable === true;
case "workspaceInstance":
return !!resource.workspaceIsShared;
return !!resource.workspace.shareable;
default:
return false;
}
Expand Down
3 changes: 2 additions & 1 deletion components/server/src/websocket-connection-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, WithResourceAccessGuard, WorkspaceLogAccessGuard } from "./auth/resource-access";
import { CompositeResourceAccessGuard, OwnerResourceGuard, ResourceAccessGuard, SharedWorkspaceAccessGuard, TeamMemberResourceGuard, WithResourceAccessGuard, WorkspaceLogAccessGuard } from "./auth/resource-access";
import { increaseApiCallCounter, increaseApiConnectionClosedCounter, increaseApiConnectionCounter, increaseApiCallUserCounter } from "./prometheus-metrics";
import { GitpodServerImpl } from "./workspace/gitpod-server-impl";

Expand Down Expand Up @@ -70,6 +70,7 @@ export class WebsocketConnectionManager<C extends GitpodClient, S extends Gitpod
} else if (!!user) {
resourceGuard = new CompositeResourceAccessGuard([
new OwnerResourceGuard(user.id),
new TeamMemberResourceGuard(user.id),
new SharedWorkspaceAccessGuard(),
new WorkspaceLogAccessGuard(user, this.hostContextProvider),
]);
Expand Down
Loading

0 comments on commit 5b6fec8

Please sign in to comment.