Skip to content

Commit eb1e4cc

Browse files
committed
[server] Introduce WorkspaceService with:
- createWorkspace - getWorkspace - stopWorkspace - deleteWorkspace - hardDeleteWorkspace
1 parent b4f85c3 commit eb1e4cc

File tree

12 files changed

+536
-100
lines changed

12 files changed

+536
-100
lines changed

components/server/src/authorization/authorizer.ts

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
Relation,
1717
ResourceType,
1818
UserPermission,
19+
WorkspacePermission,
1920
rel,
2021
} from "./definitions";
2122
import { SpiceDBAuthorizer } from "./spicedb-authorizer";
@@ -102,11 +103,11 @@ export class Authorizer {
102103
);
103104
}
104105

105-
async hasPermissionOnUser(userId: string, permission: UserPermission, userResourceId: string): Promise<boolean> {
106+
async hasPermissionOnUser(userId: string, permission: UserPermission, resourceUserId: string): Promise<boolean> {
106107
const req = v1.CheckPermissionRequest.create({
107108
subject: subject("user", userId),
108109
permission,
109-
resource: object("user", userResourceId),
110+
resource: object("user", resourceUserId),
110111
consistency,
111112
});
112113

@@ -127,6 +128,35 @@ export class Authorizer {
127128
);
128129
}
129130

131+
async hasPermissionOnWorkspace(
132+
userId: string,
133+
permission: WorkspacePermission,
134+
workspaceId: string,
135+
): Promise<boolean> {
136+
const req = v1.CheckPermissionRequest.create({
137+
subject: subject("user", userId),
138+
permission,
139+
resource: object("workspace", workspaceId),
140+
consistency,
141+
});
142+
143+
return this.authorizer.check(req, { userId });
144+
}
145+
146+
async checkPermissionOnWorkspace(userId: string, permission: WorkspacePermission, workspaceId: string) {
147+
if (await this.hasPermissionOnWorkspace(userId, permission, workspaceId)) {
148+
return;
149+
}
150+
if ("read_info" === permission || !(await this.hasPermissionOnWorkspace(userId, "read_info", workspaceId))) {
151+
throw new ApplicationError(ErrorCodes.NOT_FOUND, `Workspace ${workspaceId} not found.`);
152+
}
153+
154+
throw new ApplicationError(
155+
ErrorCodes.PERMISSION_DENIED,
156+
`You do not have ${permission} on workspace ${workspaceId}`,
157+
);
158+
}
159+
130160
// write operations below
131161
public async removeAllRelationships(userId: string, type: ResourceType, id: string) {
132162
if (await this.isDisabled(userId)) {
@@ -309,6 +339,26 @@ export class Authorizer {
309339
);
310340
}
311341

342+
async createWorkspaceInOrg(orgID: string, userID: string, workspaceID: string): Promise<void> {
343+
if (await this.isDisabled(userID)) {
344+
return;
345+
}
346+
await this.authorizer.writeRelationships(
347+
set(rel.workspace(workspaceID).org.organization(orgID)),
348+
set(rel.workspace(workspaceID).owner.user(userID)),
349+
);
350+
}
351+
352+
async deleteWorkspaceFromOrg(orgID: string, userID: string, workspaceID: string): Promise<void> {
353+
if (await this.isDisabled(userID)) {
354+
return;
355+
}
356+
await this.authorizer.writeRelationships(
357+
remove(rel.workspace(workspaceID).org.organization(orgID)),
358+
remove(rel.workspace(workspaceID).owner.user(userID)),
359+
);
360+
}
361+
312362
public async find(relation: v1.Relationship): Promise<v1.Relationship | undefined> {
313363
const relationships = await this.authorizer.readRelationships({
314364
consistency: v1.Consistency.create({

components/server/src/authorization/definitions.ts

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,23 @@ import { v1 } from "@authzed/authzed-node";
1010

1111
const InstallationID = "1";
1212

13-
export type ResourceType = UserResourceType | InstallationResourceType | OrganizationResourceType | ProjectResourceType;
13+
export type ResourceType =
14+
| UserResourceType
15+
| InstallationResourceType
16+
| OrganizationResourceType
17+
| ProjectResourceType
18+
| WorkspaceResourceType;
1419

15-
export type Relation = UserRelation | InstallationRelation | OrganizationRelation | ProjectRelation;
20+
export const AllResourceTypes: ResourceType[] = ["user", "installation", "organization", "project", "workspace"];
1621

17-
export type Permission = UserPermission | InstallationPermission | OrganizationPermission | ProjectPermission;
22+
export type Relation = UserRelation | InstallationRelation | OrganizationRelation | ProjectRelation | WorkspaceRelation;
23+
24+
export type Permission =
25+
| UserPermission
26+
| InstallationPermission
27+
| OrganizationPermission
28+
| ProjectPermission
29+
| WorkspacePermission;
1830

1931
export type UserResourceType = "user";
2032

@@ -48,6 +60,7 @@ export type OrganizationPermission =
4860
| "write_git_provider"
4961
| "read_billing"
5062
| "write_billing"
63+
| "create_workspace"
5164
| "write_billing_admin";
5265

5366
export type ProjectResourceType = "project";
@@ -56,6 +69,12 @@ export type ProjectRelation = "org" | "editor" | "viewer";
5669

5770
export type ProjectPermission = "read_info" | "write_info" | "delete";
5871

72+
export type WorkspaceResourceType = "workspace";
73+
74+
export type WorkspaceRelation = "org" | "owner";
75+
76+
export type WorkspacePermission = "access" | "stop" | "delete" | "read_info";
77+
5978
export const rel = {
6079
user(id: string) {
6180
const result: Partial<v1.Relationship> = {
@@ -327,4 +346,54 @@ export const rel = {
327346
},
328347
};
329348
},
349+
350+
workspace(id: string) {
351+
const result: Partial<v1.Relationship> = {
352+
resource: {
353+
objectType: "workspace",
354+
objectId: id,
355+
},
356+
};
357+
return {
358+
get org() {
359+
const result2 = {
360+
...result,
361+
relation: "org",
362+
};
363+
return {
364+
organization(objectId: string) {
365+
return {
366+
...result2,
367+
subject: {
368+
object: {
369+
objectType: "organization",
370+
objectId: objectId,
371+
},
372+
},
373+
} as v1.Relationship;
374+
},
375+
};
376+
},
377+
378+
get owner() {
379+
const result2 = {
380+
...result,
381+
relation: "owner",
382+
};
383+
return {
384+
user(objectId: string) {
385+
return {
386+
...result2,
387+
subject: {
388+
object: {
389+
objectType: "user",
390+
objectId: objectId,
391+
},
392+
},
393+
} as v1.Relationship;
394+
},
395+
};
396+
},
397+
};
398+
},
330399
};

components/server/src/container-module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ import { Redis } from "ioredis";
129129
import { RedisPublisher, newRedisClient } from "@gitpod/gitpod-db/lib";
130130
import { UserService } from "./user/user-service";
131131
import { RelationshipUpdater } from "./authorization/relationship-updater";
132+
import { WorkspaceService } from "./workspace/workspace-service";
132133

133134
export const productionContainerModule = new ContainerModule(
134135
(bind, unbind, isBound, rebind, unbindAsync, onActivation, onDeactivation) => {
@@ -159,6 +160,7 @@ export const productionContainerModule = new ContainerModule(
159160
bind(ConfigurationService).toSelf().inSingletonScope();
160161

161162
bind(SnapshotService).toSelf().inSingletonScope();
163+
bind(WorkspaceService).toSelf().inSingletonScope();
162164
bind(WorkspaceFactory).toSelf().inSingletonScope();
163165
bind(WorkspaceDeletionService).toSelf().inSingletonScope();
164166
bind(WorkspaceStarter).toSelf().inSingletonScope();

components/server/src/jobs/workspace-gc.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { TracedWorkspaceDB, DBWithTracing, WorkspaceDB } from "@gitpod/gitpod-db
1212
import { TraceContext } from "@gitpod/gitpod-protocol/lib/util/tracing";
1313
import { Config } from "../config";
1414
import { Job } from "./runner";
15+
import { WorkspaceService } from "../workspace/workspace-service";
1516

1617
/**
1718
* The WorkspaceGarbageCollector has two tasks:
@@ -20,6 +21,7 @@ import { Job } from "./runner";
2021
*/
2122
@injectable()
2223
export class WorkspaceGarbageCollector implements Job {
24+
@inject(WorkspaceService) protected readonly workspaceService: WorkspaceService;
2325
@inject(WorkspaceDeletionService) protected readonly deletionService: WorkspaceDeletionService;
2426
@inject(TracedWorkspaceDB) protected readonly workspaceDB: DBWithTracing<WorkspaceDB>;
2527
@inject(Config) protected readonly config: Config;
@@ -70,7 +72,7 @@ export class WorkspaceGarbageCollector implements Job {
7072
);
7173
const afterSelect = new Date();
7274
const deletes = await Promise.all(
73-
workspaces.map((ws) => this.deletionService.softDeleteWorkspace({ span }, ws, "gc")),
75+
workspaces.map((ws) => this.workspaceService.deleteWorkspace(ws.ownerId, ws.id, "gc")), // TODO(gpl) This should be a system user/service account instead of ws owner
7476
);
7577
const afterDelete = new Date();
7678

@@ -125,7 +127,11 @@ export class WorkspaceGarbageCollector implements Job {
125127
now,
126128
);
127129
const deletes = await Promise.all(
128-
workspaces.map((ws) => this.deletionService.hardDeleteWorkspace({ span }, ws.id)),
130+
workspaces.map((ws) =>
131+
this.workspaceService
132+
.hardDeleteWorkspace(ws.ownerId, ws.id)
133+
.catch((err) => log.error("failed to hard-delete workspace", err)),
134+
), // TODO(gpl) This should be a system user/service account instead of ws owner
129135
);
130136

131137
log.info(`workspace-gc: successfully purged ${deletes.length} workspaces`);

components/server/src/prebuilds/prebuild-manager.ts

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import {
2121
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
2222
import { TraceContext } from "@gitpod/gitpod-protocol/lib/util/tracing";
2323
import { getCommitInfo, HostContextProvider } from "../auth/host-context-provider";
24-
import { WorkspaceFactory } from "../workspace/workspace-factory";
2524
import { ConfigProvider } from "../workspace/config-provider";
2625
import { WorkspaceStarter } from "../workspace/workspace-starter";
2726
import { Config } from "../config";
@@ -38,6 +37,7 @@ import { ErrorCodes, ApplicationError } from "@gitpod/gitpod-protocol/lib/messag
3837
import { UserAuthentication } from "../user/user-authentication";
3938
import { EntitlementService, MayStartWorkspaceResult } from "../billing/entitlement-service";
4039
import { EnvVarService } from "../workspace/env-var-service";
40+
import { WorkspaceService } from "../workspace/workspace-service";
4141

4242
export class WorkspaceRunningError extends Error {
4343
constructor(msg: string, public instance: WorkspaceInstance) {
@@ -56,7 +56,7 @@ export interface StartPrebuildParams {
5656
@injectable()
5757
export class PrebuildManager {
5858
@inject(TracedWorkspaceDB) protected readonly workspaceDB: DBWithTracing<WorkspaceDB>;
59-
@inject(WorkspaceFactory) protected readonly workspaceFactory: WorkspaceFactory;
59+
@inject(WorkspaceService) protected readonly workspaceService: WorkspaceService;
6060
@inject(WorkspaceStarter) protected readonly workspaceStarter: WorkspaceStarter;
6161
@inject(HostContextProvider) protected readonly hostContextProvider: HostContextProvider;
6262
@inject(ConfigProvider) protected readonly configProvider: ConfigProvider;
@@ -77,21 +77,18 @@ export class PrebuildManager {
7777
const results: Promise<any>[] = [];
7878
for (const prebuild of prebuilds) {
7979
try {
80-
for (const instance of prebuild.instances) {
81-
log.info(
82-
{ userId: user.id, instanceId: instance.id, workspaceId: instance.workspaceId },
83-
"Cancelling Prebuild workspace because a newer commit was pushed to the same branch.",
84-
);
85-
results.push(
86-
this.workspaceStarter.stopWorkspaceInstance(
87-
{ span },
88-
instance.id,
89-
instance.region,
90-
"prebuild cancelled because a newer commit was pushed to the same branch",
91-
StopWorkspacePolicy.ABORT,
92-
),
93-
);
94-
}
80+
log.info(
81+
{ userId: user.id, workspaceId: prebuild.workspace.id },
82+
"Cancelling Prebuild workspace because a newer commit was pushed to the same branch.",
83+
);
84+
results.push(
85+
this.workspaceService.stopWorkspace(
86+
user.id,
87+
prebuild.workspace.id,
88+
"prebuild cancelled because a newer commit was pushed to the same branch",
89+
StopWorkspacePolicy.ABORT,
90+
),
91+
);
9592
prebuild.prebuild.state = "aborted";
9693
prebuild.prebuild.error = "A newer commit was pushed to the same branch.";
9794
results.push(this.workspaceDB.trace({ span }).storePrebuiltWorkspace(prebuild.prebuild));
@@ -224,7 +221,7 @@ export class PrebuildManager {
224221
}
225222
}
226223

227-
const workspace = await this.workspaceFactory.createForContext(
224+
const workspace = await this.workspaceService.createWorkspace(
228225
{ span },
229226
user,
230227
project.teamId,

components/server/src/test/expect-utils.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@
77
import { ApplicationError, ErrorCode } from "@gitpod/gitpod-protocol/lib/messaging/error";
88
import { expect } from "chai";
99

10-
export async function expectError(errorCode: ErrorCode, code: Promise<any> | (() => Promise<any>)) {
10+
export async function expectError(errorCode: ErrorCode, code: Promise<any> | (() => Promise<any>), message?: string) {
11+
const msg = "expected error: " + errorCode + (message ? " - " + message : "");
1112
try {
1213
await (code instanceof Function ? code() : code);
13-
expect.fail("expected error: " + errorCode);
14+
expect.fail(msg);
1415
} catch (err) {
1516
if (!ApplicationError.hasErrorCode(err)) {
1617
throw err;
1718
}
18-
expect(err && ApplicationError.hasErrorCode(err) && err.code).to.equal(errorCode);
19+
expect(err && ApplicationError.hasErrorCode(err) && err.code, msg).to.equal(errorCode);
1920
}
2021
}

0 commit comments

Comments
 (0)