Skip to content

Commit 0c0178b

Browse files
svenefftingeroboquat
authored andcommitted
[server] pass organizationId to create ws
1 parent e62c773 commit 0c0178b

File tree

13 files changed

+89
-51
lines changed

13 files changed

+89
-51
lines changed

components/gitpod-protocol/src/attribution.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ export interface TeamAttributionId {
2222
export namespace AttributionId {
2323
const SEPARATOR = ":";
2424

25+
export function createFromOrganizationId(organizationId?: string): AttributionId | undefined {
26+
return organizationId ? { kind: "team", teamId: organizationId } : undefined;
27+
}
28+
2529
export function create(userOrTeam: User | Team): AttributionId {
2630
if (User.is(userOrTeam)) {
2731
return { kind: "user", userId: userOrTeam.id };

components/gitpod-protocol/src/gitpod-service.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,7 @@ export namespace GitpodServer {
439439
}
440440
export interface CreateWorkspaceOptions extends StartWorkspaceOptions {
441441
contextUrl: string;
442+
organizationId?: string;
442443

443444
// whether running workspaces on the same context should be ignored. If false (default) users will be asked.
444445
ignoreRunningWorkspaceOnSameCommit?: boolean;

components/gitpod-protocol/src/protocol.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { WorkspaceInstance, PortVisibility } from "./workspace-instance";
88
import { RoleOrPermission } from "./permission";
99
import { Project } from "./teams-projects-protocol";
1010
import { createHash } from "crypto";
11+
import { AttributionId } from "./attribution";
1112

1213
export interface UserInfo {
1314
name?: string;
@@ -178,6 +179,17 @@ export namespace User {
178179
return user;
179180
}
180181

182+
export function getDefaultAttributionId(user: User): AttributionId {
183+
if (user.usageAttributionId) {
184+
const result = AttributionId.parse(user.usageAttributionId);
185+
if (!result) {
186+
throw new Error("Invalid attribution ID: " + user.usageAttributionId);
187+
}
188+
return result;
189+
}
190+
return AttributionId.create(user);
191+
}
192+
181193
// The actual Profile of a User
182194
export interface Profile {
183195
name: string;

components/server/ee/src/billing/entitlement-service-chargebee.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { Accounting, SubscriptionService } from "@gitpod/gitpod-payment-endpoint
99
import {
1010
BillingTier,
1111
User,
12-
Workspace,
1312
WorkspaceInstance,
1413
WorkspaceTimeoutDuration,
1514
WORKSPACE_TIMEOUT_DEFAULT_LONG,
@@ -38,7 +37,7 @@ export class EntitlementServiceChargebee implements EntitlementService {
3837

3938
async mayStartWorkspace(
4039
user: User,
41-
workspace: Workspace,
40+
organizationId: string | undefined,
4241
date: Date,
4342
runningInstances: Promise<WorkspaceInstance[]>,
4443
): Promise<MayStartWorkspaceResult> {

components/server/ee/src/billing/entitlement-service-license.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { UserDB } from "@gitpod/gitpod-db/lib";
88
import {
99
BillingTier,
1010
User,
11-
Workspace,
1211
WorkspaceInstance,
1312
WorkspaceTimeoutDuration,
1413
WORKSPACE_TIMEOUT_DEFAULT_LONG,
@@ -28,7 +27,7 @@ export class EntitlementServiceLicense implements EntitlementService {
2827

2928
async mayStartWorkspace(
3029
user: User,
31-
workspace: Pick<Workspace, "projectId">,
30+
organizationId: string | undefined,
3231
date: Date,
3332
runningInstances: Promise<WorkspaceInstance[]>,
3433
): Promise<MayStartWorkspaceResult> {

components/server/ee/src/billing/entitlement-service-ubp.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
BillingTier,
1010
Team,
1111
User,
12-
Workspace,
1312
WorkspaceInstance,
1413
WorkspaceTimeoutDuration,
1514
WORKSPACE_TIMEOUT_DEFAULT_LONG,
@@ -47,7 +46,7 @@ export class EntitlementServiceUBP implements EntitlementService {
4746

4847
async mayStartWorkspace(
4948
user: User,
50-
workspace: Pick<Workspace, "projectId">,
49+
organizationId: string | undefined,
5150
date: Date,
5251
runningInstances: Promise<WorkspaceInstance[]>,
5352
): Promise<MayStartWorkspaceResult> {
@@ -64,7 +63,7 @@ export class EntitlementServiceUBP implements EntitlementService {
6463
}
6564
};
6665
const [usageLimitReachedOnCostCenter, hitParallelWorkspaceLimit] = await Promise.all([
67-
this.checkUsageLimitReached(user, workspace, date),
66+
this.checkUsageLimitReached(user, organizationId, date),
6867
hasHitParallelWorkspaceLimit(),
6968
]);
7069
return {
@@ -75,10 +74,10 @@ export class EntitlementServiceUBP implements EntitlementService {
7574

7675
protected async checkUsageLimitReached(
7776
user: User,
78-
workspace: Pick<Workspace, "projectId">,
77+
organizationId: string | undefined,
7978
date: Date,
8079
): Promise<AttributionId | undefined> {
81-
const result = await this.userService.checkUsageLimitReached(user, workspace);
80+
const result = await this.userService.checkUsageLimitReached(user, organizationId);
8281
if (result.reached) {
8382
return result.attributionId;
8483
}

components/server/ee/src/billing/entitlement-service.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import {
88
BillingTier,
99
User,
10-
Workspace,
1110
WorkspaceInstance,
1211
WorkspaceTimeoutDuration,
1312
WORKSPACE_TIMEOUT_DEFAULT_LONG,
@@ -38,7 +37,7 @@ export class EntitlementServiceImpl implements EntitlementService {
3837

3938
async mayStartWorkspace(
4039
user: User,
41-
workspace: Workspace,
40+
organizationId: string | undefined,
4241
date: Date = new Date(),
4342
runningInstances: Promise<WorkspaceInstance[]>,
4443
): Promise<MayStartWorkspaceResult> {
@@ -52,11 +51,11 @@ export class EntitlementServiceImpl implements EntitlementService {
5251
const billingMode = await this.billingModes.getBillingModeForUser(user, date);
5352
switch (billingMode.mode) {
5453
case "none":
55-
return this.license.mayStartWorkspace(user, workspace, date, runningInstances);
54+
return this.license.mayStartWorkspace(user, organizationId, date, runningInstances);
5655
case "chargebee":
57-
return this.chargebee.mayStartWorkspace(user, workspace, date, runningInstances);
56+
return this.chargebee.mayStartWorkspace(user, organizationId, date, runningInstances);
5857
case "usage-based":
59-
return this.ubp.mayStartWorkspace(user, workspace, date, runningInstances);
58+
return this.ubp.mayStartWorkspace(user, organizationId, date, runningInstances);
6059
default:
6160
throw new Error("Unsupported billing mode: " + (billingMode as any).mode); // safety net
6261
}

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ export class PrebuildManager {
139139
`Running prebuilds without a project is no longer supported. Please add '${cloneURL}' as a project in a team.`,
140140
);
141141
}
142-
await this.checkUsageLimitReached(user, project); // throws if true
142+
await this.checkUsageLimitReached(user, project.teamId); // throws if out of credits
143143

144144
const config = await this.fetchConfig({ span }, user, context);
145145

@@ -296,17 +296,17 @@ export class PrebuildManager {
296296
}
297297
}
298298

299-
protected async checkUsageLimitReached(user: User, project: Project): Promise<void> {
299+
protected async checkUsageLimitReached(user: User, organizationId?: string): Promise<void> {
300300
let result: MayStartWorkspaceResult = {};
301301
try {
302302
result = await this.entitlementService.mayStartWorkspace(
303303
user,
304-
{ projectId: project.id },
304+
organizationId,
305305
new Date(),
306306
Promise.resolve([]),
307307
);
308308
} catch (err) {
309-
log.error({ userId: user.id }, "EntitlementSerivce.mayStartWorkspace error", err);
309+
log.error({ userId: user.id }, "EntitlementService.mayStartWorkspace error", err);
310310
return; // we don't want to block workspace starts because of internal errors
311311
}
312312
if (!!result.usageLimitReachedOnCostCenter) {

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

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -291,14 +291,19 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
291291
protected async mayStartWorkspace(
292292
ctx: TraceContext,
293293
user: User,
294-
workspace: Workspace,
294+
organizationId: string | undefined,
295295
runningInstances: Promise<WorkspaceInstance[]>,
296296
): Promise<void> {
297-
await super.mayStartWorkspace(ctx, user, workspace, runningInstances);
297+
await super.mayStartWorkspace(ctx, user, organizationId, runningInstances);
298298

299299
let result: MayStartWorkspaceResult = {};
300300
try {
301-
result = await this.entitlementService.mayStartWorkspace(user, workspace, new Date(), runningInstances);
301+
result = await this.entitlementService.mayStartWorkspace(
302+
user,
303+
organizationId,
304+
new Date(),
305+
runningInstances,
306+
);
302307
TraceContext.addNestedTags(ctx, { mayStartWorkspace: { result } });
303308
} catch (err) {
304309
log.error({ userId: user.id }, "EntitlementSerivce.mayStartWorkspace error", err);
@@ -2381,7 +2386,11 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
23812386
try {
23822387
const billingMode = await this.billingModes.getBillingModeForUser(user, new Date());
23832388
if (billingMode.mode === "usage-based") {
2384-
const limit = await this.userService.checkUsageLimitReached(user);
2389+
const attributionId: AttributionId = User.getDefaultAttributionId(user);
2390+
const limit = await this.userService.checkUsageLimitReached(
2391+
user,
2392+
attributionId.kind === "team" ? attributionId.teamId : undefined,
2393+
);
23852394
await this.guardCostCenterAccess(ctx, user.id, limit.attributionId, "get");
23862395

23872396
switch (limit.attributionId.kind) {

components/server/src/billing/entitlement-service.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
import {
88
User,
9-
Workspace,
109
WorkspaceInstance,
1110
WorkspaceTimeoutDuration,
1211
WORKSPACE_TIMEOUT_DEFAULT_SHORT,
@@ -43,7 +42,7 @@ export interface EntitlementService {
4342
*/
4443
mayStartWorkspace(
4544
user: User,
46-
workspace: Pick<Workspace, "projectId">,
45+
organizationId: string | undefined,
4746
date: Date,
4847
runningInstances: Promise<WorkspaceInstance[]>,
4948
): Promise<MayStartWorkspaceResult>;
@@ -89,7 +88,7 @@ export interface EntitlementService {
8988
export class CommunityEntitlementService implements EntitlementService {
9089
async mayStartWorkspace(
9190
user: User,
92-
workspace: Workspace,
91+
organizationId: string | undefined,
9392
date: Date,
9493
runningInstances: Promise<WorkspaceInstance[]>,
9594
): Promise<MayStartWorkspaceResult> {

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

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66

77
import { injectable, inject } from "inversify";
8-
import { User, Identity, UserEnvVarValue, Token, Workspace } from "@gitpod/gitpod-protocol";
8+
import { User, Identity, UserEnvVarValue, Token } from "@gitpod/gitpod-protocol";
99
import { ProjectDB, TeamDB, TermsAcceptanceDB, UserDB } from "@gitpod/gitpod-db/lib";
1010
import { HostContextProvider } from "../auth/host-context-provider";
1111
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
@@ -162,37 +162,37 @@ export class UserService {
162162
protected async validateUsageAttributionId(user: User, usageAttributionId: string): Promise<AttributionId> {
163163
const attribution = AttributionId.parse(usageAttributionId);
164164
if (!attribution) {
165-
throw new ResponseError(ErrorCodes.INVALID_COST_CENTER, "The billing team id configured is invalid.");
165+
throw new ResponseError(ErrorCodes.INVALID_COST_CENTER, "The provided attributionId is invalid.", {
166+
id: usageAttributionId,
167+
});
166168
}
167169
if (attribution.kind === "team") {
168170
const team = await this.teamDB.findTeamById(attribution.teamId);
169171
if (!team) {
170172
throw new ResponseError(
171173
ErrorCodes.INVALID_COST_CENTER,
172-
"The billing team you've selected no longer exists.",
174+
"Organization not found. Please contact support if you believe this is an error.",
173175
);
174176
}
175177
const members = await this.teamDB.findMembersByTeam(team.id);
176178
if (!members.find((m) => m.userId === user.id)) {
179+
// if the user's not a member of an org, they can't see it
177180
throw new ResponseError(
178181
ErrorCodes.INVALID_COST_CENTER,
179-
"You're no longer a member of the selected billing team.",
182+
"Organization not found. Please contact support if you believe this is an error.",
180183
);
181184
}
182185
}
183186
if (attribution.kind === "user") {
184187
if (user.id !== attribution.userId) {
185-
throw new ResponseError(
186-
ErrorCodes.INVALID_COST_CENTER,
187-
"You can select either yourself or a team you are a member of",
188-
);
188+
throw new ResponseError(ErrorCodes.INVALID_COST_CENTER, "Invalid organizationId.");
189189
}
190190
}
191191
const billedAttributionIds = await this.listAvailableUsageAttributionIds(user);
192192
if (billedAttributionIds.find((id) => AttributionId.equals(id, attribution)) === undefined) {
193193
throw new ResponseError(
194194
ErrorCodes.INVALID_COST_CENTER,
195-
"You can select either yourself or a billed team you are a member of",
195+
"Organization not found. Please contact support if you believe this is an error.",
196196
);
197197
}
198198
return attribution;
@@ -202,13 +202,17 @@ export class UserService {
202202
* Identifies the team or user to which a workspace instance's running time should be attributed to
203203
* (e.g. for usage analytics or billing purposes).
204204
*
205+
* This is the legacy logic for determining a cost center. It's only used for workspaces that are started by users ibefore they have been migrated to org-only mode.
205206
*
206207
* @param user
207208
* @param projectId
208209
* @returns The validated AttributionId
209210
*/
210211
async getWorkspaceUsageAttributionId(user: User, projectId?: string): Promise<AttributionId> {
211-
// if it's a workspace for a project the user has access to and the costcenter has credits use that
212+
if (user.additionalData?.isMigratedToTeamOnlyAttribution) {
213+
throw new Error("getWorkspaceUsageAttributionId should not be called for users in org-only mode.");
214+
}
215+
// if it's a workspace for a project the user has access to and the org has credits use that
212216
if (projectId) {
213217
let attributionId: AttributionId | undefined;
214218
const project = await this.projectDb.findProjectById(projectId);
@@ -234,7 +238,7 @@ export class UserService {
234238
if (teams.length > 0) {
235239
return AttributionId.create(teams[0]);
236240
}
237-
throw new ResponseError(ErrorCodes.INVALID_COST_CENTER, "No team found for user");
241+
throw new ResponseError(ErrorCodes.INVALID_COST_CENTER, "No organization found for user");
238242
}
239243
return AttributionId.create(user);
240244
}
@@ -244,11 +248,11 @@ export class UserService {
244248
* @param workspace - optional, in which case the default billing account will be checked
245249
* @returns
246250
*/
247-
async checkUsageLimitReached(
248-
user: User,
249-
workspace?: Pick<Workspace, "projectId">,
250-
): Promise<UsageLimitReachedResult> {
251-
const attributionId = await this.getWorkspaceUsageAttributionId(user, workspace?.projectId);
251+
async checkUsageLimitReached(user: User, organizationId?: string): Promise<UsageLimitReachedResult> {
252+
if (!organizationId && user.additionalData?.isMigratedToTeamOnlyAttribution) {
253+
throw new Error("organizationId must be provided for org-only users");
254+
}
255+
const attributionId = AttributionId.createFromOrganizationId(organizationId) || AttributionId.create(user);
252256
const creditBalance = await this.usageService.getCurrentBalance(attributionId);
253257
const currentInvoiceCredits = creditBalance.usedCredits;
254258
const usageLimit = creditBalance.usageLimit;

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

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -707,7 +707,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
707707
const mayStartPromise = this.mayStartWorkspace(
708708
ctx,
709709
user,
710-
workspace,
710+
workspace.organizationId,
711711
this.workspaceDb.trace(ctx).findRegularRunningInstances(user.id),
712712
);
713713
await this.guardAccess({ kind: "workspace", subject: workspace }, "get");
@@ -1189,10 +1189,18 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
11891189
? await this.projectDB.findProjectByCloneUrl(context.repository.cloneUrl)
11901190
: undefined;
11911191

1192-
//TODO(se) relying on the attribution mechanism is a temporary work around. We will go to explicit passing of organization IDs soon.
1193-
const attributionId = await this.userService.getWorkspaceUsageAttributionId(user, project?.id);
1194-
const organizationId = attributionId.kind === "team" ? attributionId.teamId : undefined;
1192+
let organizationId = options.organizationId;
1193+
if (!organizationId) {
1194+
if (!user.additionalData?.isMigratedToTeamOnlyAttribution) {
1195+
const attributionId = await this.userService.getWorkspaceUsageAttributionId(user, project?.id);
1196+
organizationId = attributionId.kind === "team" ? attributionId.teamId : undefined;
1197+
} else {
1198+
throw new ResponseError(ErrorCodes.BAD_REQUEST, "No organizationId provided.");
1199+
}
1200+
}
1201+
const mayStartWorkspacePromise = this.mayStartWorkspace(ctx, user, organizationId, runningInstancesPromise);
11951202

1203+
// TODO (se) findPrebuiltWorkspace also needs the organizationId once we limit prebuild reuse to the same org
11961204
const prebuiltWorkspace = await this.findPrebuiltWorkspace(
11971205
ctx,
11981206
user,
@@ -1217,7 +1225,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
12171225
context,
12181226
normalizedContextUrl,
12191227
);
1220-
await this.mayStartWorkspace(ctx, user, workspace, runningInstancesPromise);
1228+
await mayStartWorkspacePromise;
12211229
try {
12221230
await this.guardAccess({ kind: "workspace", subject: workspace }, "create");
12231231
} catch (err) {
@@ -1332,7 +1340,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
13321340
protected async mayStartWorkspace(
13331341
ctx: TraceContext,
13341342
user: User,
1335-
workspace: Workspace,
1343+
organizationId: string | undefined,
13361344
runningInstances: Promise<WorkspaceInstance[]>,
13371345
): Promise<void> {}
13381346

0 commit comments

Comments
 (0)