Skip to content

Commit

Permalink
[server] pass organizationId to create ws
Browse files Browse the repository at this point in the history
  • Loading branch information
svenefftinge committed Feb 13, 2023
1 parent c2f624f commit 97140d6
Show file tree
Hide file tree
Showing 12 changed files with 87 additions and 51 deletions.
1 change: 1 addition & 0 deletions components/gitpod-protocol/src/gitpod-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,7 @@ export namespace GitpodServer {
}
export interface CreateWorkspaceOptions extends StartWorkspaceOptions {
contextUrl: string;
organizationId?: string;

// whether running workspaces on the same context should be ignored. If false (default) users will be asked.
ignoreRunningWorkspaceOnSameCommit?: boolean;
Expand Down
12 changes: 12 additions & 0 deletions components/gitpod-protocol/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { WorkspaceInstance, PortVisibility } from "./workspace-instance";
import { RoleOrPermission } from "./permission";
import { Project } from "./teams-projects-protocol";
import { createHash } from "crypto";
import { AttributionId } from "./attribution";

export interface UserInfo {
name?: string;
Expand Down Expand Up @@ -178,6 +179,17 @@ export namespace User {
return user;
}

export function getDefaultAttributionId(user: User): AttributionId {
if (user.usageAttributionId) {
const result = AttributionId.parse(user.usageAttributionId);
if (!result) {
throw new Error("Invalid attribution ID: " + user.usageAttributionId);
}
return result;
}
return AttributionId.create(user);
}

// The actual Profile of a User
export interface Profile {
name: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { Accounting, SubscriptionService } from "@gitpod/gitpod-payment-endpoint
import {
BillingTier,
User,
Workspace,
WorkspaceInstance,
WorkspaceTimeoutDuration,
WORKSPACE_TIMEOUT_DEFAULT_LONG,
Expand Down Expand Up @@ -38,7 +37,7 @@ export class EntitlementServiceChargebee implements EntitlementService {

async mayStartWorkspace(
user: User,
workspace: Workspace,
organizationId: string | undefined,
date: Date,
runningInstances: Promise<WorkspaceInstance[]>,
): Promise<MayStartWorkspaceResult> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { UserDB } from "@gitpod/gitpod-db/lib";
import {
BillingTier,
User,
Workspace,
WorkspaceInstance,
WorkspaceTimeoutDuration,
WORKSPACE_TIMEOUT_DEFAULT_LONG,
Expand All @@ -28,7 +27,7 @@ export class EntitlementServiceLicense implements EntitlementService {

async mayStartWorkspace(
user: User,
workspace: Pick<Workspace, "projectId">,
organizationId: string | undefined,
date: Date,
runningInstances: Promise<WorkspaceInstance[]>,
): Promise<MayStartWorkspaceResult> {
Expand Down
9 changes: 4 additions & 5 deletions components/server/ee/src/billing/entitlement-service-ubp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
BillingTier,
Team,
User,
Workspace,
WorkspaceInstance,
WorkspaceTimeoutDuration,
WORKSPACE_TIMEOUT_DEFAULT_LONG,
Expand Down Expand Up @@ -47,7 +46,7 @@ export class EntitlementServiceUBP implements EntitlementService {

async mayStartWorkspace(
user: User,
workspace: Pick<Workspace, "projectId">,
organizationId: string | undefined,
date: Date,
runningInstances: Promise<WorkspaceInstance[]>,
): Promise<MayStartWorkspaceResult> {
Expand All @@ -64,7 +63,7 @@ export class EntitlementServiceUBP implements EntitlementService {
}
};
const [usageLimitReachedOnCostCenter, hitParallelWorkspaceLimit] = await Promise.all([
this.checkUsageLimitReached(user, workspace, date),
this.checkUsageLimitReached(user, organizationId, date),
hasHitParallelWorkspaceLimit(),
]);
return {
Expand All @@ -75,10 +74,10 @@ export class EntitlementServiceUBP implements EntitlementService {

protected async checkUsageLimitReached(
user: User,
workspace: Pick<Workspace, "projectId">,
organizationId: string | undefined,
date: Date,
): Promise<AttributionId | undefined> {
const result = await this.userService.checkUsageLimitReached(user, workspace);
const result = await this.userService.checkUsageLimitReached(user, organizationId);
if (result.reached) {
return result.attributionId;
}
Expand Down
9 changes: 4 additions & 5 deletions components/server/ee/src/billing/entitlement-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import {
BillingTier,
User,
Workspace,
WorkspaceInstance,
WorkspaceTimeoutDuration,
WORKSPACE_TIMEOUT_DEFAULT_LONG,
Expand Down Expand Up @@ -38,7 +37,7 @@ export class EntitlementServiceImpl implements EntitlementService {

async mayStartWorkspace(
user: User,
workspace: Workspace,
organizationId: string | undefined,
date: Date = new Date(),
runningInstances: Promise<WorkspaceInstance[]>,
): Promise<MayStartWorkspaceResult> {
Expand All @@ -52,11 +51,11 @@ export class EntitlementServiceImpl implements EntitlementService {
const billingMode = await this.billingModes.getBillingModeForUser(user, date);
switch (billingMode.mode) {
case "none":
return this.license.mayStartWorkspace(user, workspace, date, runningInstances);
return this.license.mayStartWorkspace(user, organizationId, date, runningInstances);
case "chargebee":
return this.chargebee.mayStartWorkspace(user, workspace, date, runningInstances);
return this.chargebee.mayStartWorkspace(user, organizationId, date, runningInstances);
case "usage-based":
return this.ubp.mayStartWorkspace(user, workspace, date, runningInstances);
return this.ubp.mayStartWorkspace(user, organizationId, date, runningInstances);
default:
throw new Error("Unsupported billing mode: " + (billingMode as any).mode); // safety net
}
Expand Down
8 changes: 4 additions & 4 deletions components/server/ee/src/prebuilds/prebuild-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export class PrebuildManager {
`Running prebuilds without a project is no longer supported. Please add '${cloneURL}' as a project in a team.`,
);
}
await this.checkUsageLimitReached(user, project); // throws if true
await this.checkUsageLimitReached(user, project.teamId); // throws if out of credits

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

Expand Down Expand Up @@ -296,17 +296,17 @@ export class PrebuildManager {
}
}

protected async checkUsageLimitReached(user: User, project: Project): Promise<void> {
protected async checkUsageLimitReached(user: User, organizationId?: string): Promise<void> {
let result: MayStartWorkspaceResult = {};
try {
result = await this.entitlementService.mayStartWorkspace(
user,
{ projectId: project.id },
organizationId,
new Date(),
Promise.resolve([]),
);
} catch (err) {
log.error({ userId: user.id }, "EntitlementSerivce.mayStartWorkspace error", err);
log.error({ userId: user.id }, "EntitlementService.mayStartWorkspace error", err);
return; // we don't want to block workspace starts because of internal errors
}
if (!!result.usageLimitReachedOnCostCenter) {
Expand Down
17 changes: 13 additions & 4 deletions components/server/ee/src/workspace/gitpod-server-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,14 +291,19 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
protected async mayStartWorkspace(
ctx: TraceContext,
user: User,
workspace: Workspace,
organizationId: string | undefined,
runningInstances: Promise<WorkspaceInstance[]>,
): Promise<void> {
await super.mayStartWorkspace(ctx, user, workspace, runningInstances);
await super.mayStartWorkspace(ctx, user, organizationId, runningInstances);

let result: MayStartWorkspaceResult = {};
try {
result = await this.entitlementService.mayStartWorkspace(user, workspace, new Date(), runningInstances);
result = await this.entitlementService.mayStartWorkspace(
user,
organizationId,
new Date(),
runningInstances,
);
TraceContext.addNestedTags(ctx, { mayStartWorkspace: { result } });
} catch (err) {
log.error({ userId: user.id }, "EntitlementSerivce.mayStartWorkspace error", err);
Expand Down Expand Up @@ -2381,7 +2386,11 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
try {
const billingMode = await this.billingModes.getBillingModeForUser(user, new Date());
if (billingMode.mode === "usage-based") {
const limit = await this.userService.checkUsageLimitReached(user);
const attributionId: AttributionId = User.getDefaultAttributionId(user);
const limit = await this.userService.checkUsageLimitReached(
user,
attributionId.kind === "team" ? attributionId.teamId : undefined,
);
await this.guardCostCenterAccess(ctx, user.id, limit.attributionId, "get");

switch (limit.attributionId.kind) {
Expand Down
5 changes: 2 additions & 3 deletions components/server/src/billing/entitlement-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

import {
User,
Workspace,
WorkspaceInstance,
WorkspaceTimeoutDuration,
WORKSPACE_TIMEOUT_DEFAULT_SHORT,
Expand Down Expand Up @@ -43,7 +42,7 @@ export interface EntitlementService {
*/
mayStartWorkspace(
user: User,
workspace: Pick<Workspace, "projectId">,
organizationId: string | undefined,
date: Date,
runningInstances: Promise<WorkspaceInstance[]>,
): Promise<MayStartWorkspaceResult>;
Expand Down Expand Up @@ -89,7 +88,7 @@ export interface EntitlementService {
export class CommunityEntitlementService implements EntitlementService {
async mayStartWorkspace(
user: User,
workspace: Workspace,
organizationId: string | undefined,
date: Date,
runningInstances: Promise<WorkspaceInstance[]>,
): Promise<MayStartWorkspaceResult> {
Expand Down
37 changes: 21 additions & 16 deletions components/server/src/user/user-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import { injectable, inject } from "inversify";
import { User, Identity, UserEnvVarValue, Token, Workspace } from "@gitpod/gitpod-protocol";
import { User, Identity, UserEnvVarValue, Token } from "@gitpod/gitpod-protocol";
import { ProjectDB, TeamDB, TermsAcceptanceDB, UserDB } from "@gitpod/gitpod-db/lib";
import { HostContextProvider } from "../auth/host-context-provider";
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
Expand Down Expand Up @@ -162,37 +162,37 @@ export class UserService {
protected async validateUsageAttributionId(user: User, usageAttributionId: string): Promise<AttributionId> {
const attribution = AttributionId.parse(usageAttributionId);
if (!attribution) {
throw new ResponseError(ErrorCodes.INVALID_COST_CENTER, "The billing team id configured is invalid.");
throw new ResponseError(ErrorCodes.INVALID_COST_CENTER, "The provided attributionId is invalid.", {
id: usageAttributionId,
});
}
if (attribution.kind === "team") {
const team = await this.teamDB.findTeamById(attribution.teamId);
if (!team) {
throw new ResponseError(
ErrorCodes.INVALID_COST_CENTER,
"The billing team you've selected no longer exists.",
"Organization not found. Please contact support if you believe this is an error.",
);
}
const members = await this.teamDB.findMembersByTeam(team.id);
if (!members.find((m) => m.userId === user.id)) {
// if the user's not a member of an org, they can't see it
throw new ResponseError(
ErrorCodes.INVALID_COST_CENTER,
"You're no longer a member of the selected billing team.",
"Organization not found. Please contact support if you believe this is an error.",
);
}
}
if (attribution.kind === "user") {
if (user.id !== attribution.userId) {
throw new ResponseError(
ErrorCodes.INVALID_COST_CENTER,
"You can select either yourself or a team you are a member of",
);
throw new ResponseError(ErrorCodes.INVALID_COST_CENTER, "Invalid organizationId.");
}
}
const billedAttributionIds = await this.listAvailableUsageAttributionIds(user);
if (billedAttributionIds.find((id) => AttributionId.equals(id, attribution)) === undefined) {
throw new ResponseError(
ErrorCodes.INVALID_COST_CENTER,
"You can select either yourself or a billed team you are a member of",
"Organization not found. Please contact support if you believe this is an error.",
);
}
return attribution;
Expand All @@ -202,13 +202,17 @@ export class UserService {
* Identifies the team or user to which a workspace instance's running time should be attributed to
* (e.g. for usage analytics or billing purposes).
*
* 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.
*
* @param user
* @param projectId
* @returns The validated AttributionId
*/
async getWorkspaceUsageAttributionId(user: User, projectId?: string): Promise<AttributionId> {
// if it's a workspace for a project the user has access to and the costcenter has credits use that
if (user.additionalData?.isMigratedToTeamOnlyAttribution) {
throw new Error("getWorkspaceUsageAttributionId should not be called for users in org-only mode.");
}
// if it's a workspace for a project the user has access to and the org has credits use that
if (projectId) {
let attributionId: AttributionId | undefined;
const project = await this.projectDb.findProjectById(projectId);
Expand All @@ -234,7 +238,7 @@ export class UserService {
if (teams.length > 0) {
return AttributionId.create(teams[0]);
}
throw new ResponseError(ErrorCodes.INVALID_COST_CENTER, "No team found for user");
throw new ResponseError(ErrorCodes.INVALID_COST_CENTER, "No organization found for user");
}
return AttributionId.create(user);
}
Expand All @@ -244,11 +248,12 @@ export class UserService {
* @param workspace - optional, in which case the default billing account will be checked
* @returns
*/
async checkUsageLimitReached(
user: User,
workspace?: Pick<Workspace, "projectId">,
): Promise<UsageLimitReachedResult> {
const attributionId = await this.getWorkspaceUsageAttributionId(user, workspace?.projectId);
async checkUsageLimitReached(user: User, organizationId?: string): Promise<UsageLimitReachedResult> {
if (!organizationId && user.additionalData?.isMigratedToTeamOnlyAttribution) {
throw new Error("organizationId must be provided for org-only users");
}
const attributionId: AttributionId =
organizationId === undefined ? AttributionId.create(user) : { kind: "team", teamId: organizationId };
const creditBalance = await this.usageService.getCurrentBalance(attributionId);
const currentInvoiceCredits = creditBalance.usedCredits;
const usageLimit = creditBalance.usageLimit;
Expand Down
19 changes: 13 additions & 6 deletions components/server/src/workspace/gitpod-server-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -707,7 +707,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
const mayStartPromise = this.mayStartWorkspace(
ctx,
user,
workspace,
workspace.organizationId,
this.workspaceDb.trace(ctx).findRegularRunningInstances(user.id),
);
await this.guardAccess({ kind: "workspace", subject: workspace }, "get");
Expand Down Expand Up @@ -1189,10 +1189,17 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
? await this.projectDB.findProjectByCloneUrl(context.repository.cloneUrl)
: undefined;

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

// TODO (se) findPrebuiltWorkspace also needs the organizationId once we limit prebuild reuse to the same org
const prebuiltWorkspace = await this.findPrebuiltWorkspace(
ctx,
user,
Expand All @@ -1217,7 +1224,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
context,
normalizedContextUrl,
);
await this.mayStartWorkspace(ctx, user, workspace, runningInstancesPromise);
await mayStartWorkspacePromise;
try {
await this.guardAccess({ kind: "workspace", subject: workspace }, "create");
} catch (err) {
Expand Down Expand Up @@ -1332,7 +1339,7 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
protected async mayStartWorkspace(
ctx: TraceContext,
user: User,
workspace: Workspace,
organizationId: string | undefined,
runningInstances: Promise<WorkspaceInstance[]>,
): Promise<void> {}

Expand Down
Loading

0 comments on commit 97140d6

Please sign in to comment.