Skip to content

Commit a1b7c3c

Browse files
committed
[server] Use BillingModes for guarding the Stripe API
1 parent 725d26c commit a1b7c3c

File tree

4 files changed

+70
-30
lines changed

4 files changed

+70
-30
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import { IDEServer } from "./ide-protocol";
6262
import { InstallationAdminSettings, TelemetryData } from "./installation-admin-protocol";
6363
import { Currency } from "./plans";
6464
import { BillableSession } from "./usage";
65+
import { BillingMode } from "./billing-modes";
6566

6667
export interface GitpodClient {
6768
onInstanceUpdate(instance: WorkspaceInstance): void;
@@ -296,6 +297,9 @@ export interface GitpodServer extends JsonRpcServer<GitpodClient>, AdminServer,
296297
listBilledUsage(attributionId: string): Promise<BillableSession[]>;
297298
setUsageAttribution(usageAttribution: string): Promise<void>;
298299

300+
getBillingModeForUser(): Promise<BillingMode>;
301+
getBillingModeForTeam(teamId: string): Promise<BillingMode>;
302+
299303
/**
300304
* Analytics
301305
*/

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

Lines changed: 36 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -106,10 +106,10 @@ import { ClientMetadata, traceClientMetadata } from "../../../src/websocket/webs
106106
import { BitbucketAppSupport } from "../bitbucket/bitbucket-app-support";
107107
import { URL } from "url";
108108
import { UserCounter } from "../user/user-counter";
109-
import { getExperimentsClientForBackend } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server";
110109
import { AttributionId } from "@gitpod/gitpod-protocol/lib/attribution";
111110
import { CachingUsageServiceClientProvider } from "@gitpod/usage-api/lib/usage/v1/sugar";
112111
import * as usage from "@gitpod/usage-api/lib/usage/v1/usage_pb";
112+
import { BillingMode } from "@gitpod/gitpod-protocol/lib/billing-modes";
113113

114114
@injectable()
115115
export class GitpodServerEEImpl extends GitpodServerImpl {
@@ -1919,24 +1919,34 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
19191919
return subscription;
19201920
}
19211921

1922-
protected async ensureIsUsageBasedFeatureFlagEnabled(user: User): Promise<void> {
1923-
const teams = await this.teamDB.findTeamsByUser(user.id);
1924-
const isUsageBasedBillingEnabled = await getExperimentsClientForBackend().getValueAsync(
1925-
"isUsageBasedBillingEnabled",
1926-
false,
1927-
{
1928-
user,
1929-
teams: teams,
1930-
},
1922+
protected async ensureStripeApisAllowed(sub: { user?: User; team?: Team }): Promise<void> {
1923+
this.ensureBillingMode(
1924+
sub,
1925+
// Stripe is allowed when you either are on the usage-based side already, or you can switch to)
1926+
(m) => m.mode === "usage-based" || (m.mode === "chargebee" && m.tier === "paid_cancelled_and_ubb"),
19311927
);
1932-
if (!isUsageBasedBillingEnabled) {
1933-
throw new ResponseError(ErrorCodes.PERMISSION_DENIED, "not allowed");
1928+
}
1929+
1930+
protected async ensureBillingMode(
1931+
sub: { user?: User; team?: Team },
1932+
pred: (m: BillingMode) => boolean,
1933+
): Promise<void> {
1934+
let billingMode: BillingMode | undefined = undefined;
1935+
if (sub.user) {
1936+
billingMode = await this.billingModes.getBillingModeForUser(sub.user);
1937+
} else if (sub.team) {
1938+
billingMode = await this.billingModes.getBillingModeForTeam(sub.team);
1939+
}
1940+
1941+
if (billingMode && pred(billingMode)) {
1942+
return;
19341943
}
1944+
throw new ResponseError(ErrorCodes.PERMISSION_DENIED, "not allowed");
19351945
}
19361946

19371947
async getStripePublishableKey(ctx: TraceContext): Promise<string> {
19381948
const user = this.checkAndBlockUser("getStripePublishableKey");
1939-
await this.ensureIsUsageBasedFeatureFlagEnabled(user);
1949+
await this.ensureStripeApisAllowed({ user });
19401950
const publishableKey = this.config.stripeSecrets?.publishableKey;
19411951
if (!publishableKey) {
19421952
throw new ResponseError(
@@ -1949,7 +1959,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
19491959

19501960
async getStripeSetupIntentClientSecret(ctx: TraceContext): Promise<string> {
19511961
const user = this.checkAndBlockUser("getStripeSetupIntentClientSecret");
1952-
await this.ensureIsUsageBasedFeatureFlagEnabled(user);
1962+
await this.ensureStripeApisAllowed({ user });
19531963
try {
19541964
const setupIntent = await this.stripeService.createSetupIntent();
19551965
if (!setupIntent.client_secret) {
@@ -1963,9 +1973,9 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
19631973
}
19641974

19651975
async findStripeSubscriptionIdForTeam(ctx: TraceContext, teamId: string): Promise<string | undefined> {
1966-
const user = this.checkAndBlockUser("findStripeSubscriptionIdForTeam");
1967-
await this.ensureIsUsageBasedFeatureFlagEnabled(user);
1968-
await this.guardTeamOperation(teamId, "get");
1976+
this.checkAndBlockUser("findStripeSubscriptionIdForTeam");
1977+
const team = await this.guardTeamOperation(teamId, "get");
1978+
await this.ensureStripeApisAllowed({ team });
19691979
try {
19701980
const customer = await this.stripeService.findCustomerByTeamId(teamId);
19711981
if (!customer?.id) {
@@ -1990,9 +2000,8 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
19902000
currency: Currency,
19912001
): Promise<void> {
19922002
const user = this.checkAndBlockUser("subscribeUserToStripe");
1993-
await this.ensureIsUsageBasedFeatureFlagEnabled(user);
1994-
await this.guardTeamOperation(teamId, "update");
1995-
const team = await this.teamDB.findTeamById(teamId);
2003+
const team = await this.guardTeamOperation(teamId, "update");
2004+
await this.ensureStripeApisAllowed({ team });
19962005
try {
19972006
let customer = await this.stripeService.findCustomerByTeamId(team!.id);
19982007
if (!customer) {
@@ -2028,10 +2037,9 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
20282037
}
20292038

20302039
async getStripePortalUrlForTeam(ctx: TraceContext, teamId: string): Promise<string> {
2031-
const user = this.checkAndBlockUser("getStripePortalUrlForTeam");
2032-
await this.ensureIsUsageBasedFeatureFlagEnabled(user);
2033-
await this.guardTeamOperation(teamId, "update");
2034-
const team = await this.teamDB.findTeamById(teamId);
2040+
this.checkAndBlockUser("getStripePortalUrlForTeam");
2041+
const team = await this.guardTeamOperation(teamId, "update");
2042+
await this.ensureStripeApisAllowed({ team });
20352043
try {
20362044
const url = await this.stripeService.getPortalUrlForTeam(team!);
20372045
return url;
@@ -2046,8 +2054,8 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
20462054

20472055
async getSpendingLimitForTeam(ctx: TraceContext, teamId: string): Promise<number | undefined> {
20482056
const user = this.checkAndBlockUser("getSpendingLimitForTeam");
2049-
await this.ensureIsUsageBasedFeatureFlagEnabled(user);
2050-
await this.guardTeamOperation(teamId, "get");
2057+
const team = await this.guardTeamOperation(teamId, "get");
2058+
await this.ensureStripeApisAllowed({ team });
20512059

20522060
const attributionId = AttributionId.render({ kind: "team", teamId });
20532061
await this.guardCostCenterAccess(ctx, user.id, attributionId, "get");
@@ -2061,8 +2069,8 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
20612069

20622070
async setSpendingLimitForTeam(ctx: TraceContext, teamId: string, spendingLimit: number): Promise<void> {
20632071
const user = this.checkAndBlockUser("setSpendingLimitForTeam");
2064-
await this.ensureIsUsageBasedFeatureFlagEnabled(user);
2065-
await this.guardTeamOperation(teamId, "update");
2072+
const team = await this.guardTeamOperation(teamId, "update");
2073+
await this.ensureStripeApisAllowed({ team });
20662074
if (typeof spendingLimit !== "number" || spendingLimit < 0) {
20672075
throw new ResponseError(ErrorCodes.BAD_REQUEST, "Unexpected `spendingLimit` value.");
20682076
}

components/server/src/auth/rate-limiter.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,9 @@ function getConfig(config: RateLimiterConfig): RateLimiterConfig {
211211
subscribeTeamToStripe: { group: "default", points: 1 },
212212
getStripePortalUrlForTeam: { group: "default", points: 1 },
213213
listBilledUsage: { group: "default", points: 1 },
214+
getBillingModeForTeam: { group: "default", points: 1 },
215+
getBillingModeForUser: { group: "default", points: 1 },
216+
214217
trackEvent: { group: "default", points: 1 },
215218
trackLocation: { group: "default", points: 1 },
216219
identifyUser: { group: "default", points: 1 },

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

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,11 @@ import {
108108
} from "@gitpod/gitpod-protocol/lib/team-subscription-protocol";
109109
import { Cancelable } from "@gitpod/gitpod-protocol/lib/util/cancelable";
110110
import { log, LogContext } from "@gitpod/gitpod-protocol/lib/util/logging";
111-
import { InterfaceWithTraceContext, TraceContext } from "@gitpod/gitpod-protocol/lib/util/tracing";
111+
import {
112+
InterfaceWithTraceContext,
113+
TraceContext,
114+
TraceContextWithSpan,
115+
} from "@gitpod/gitpod-protocol/lib/util/tracing";
112116
import {
113117
IdentifyMessage,
114118
RemoteIdentifyMessage,
@@ -170,6 +174,8 @@ import { Currency } from "@gitpod/gitpod-protocol/lib/plans";
170174
import { getExperimentsClientForBackend } from "@gitpod/gitpod-protocol/lib/experiments/configcat-server";
171175
import { BillableSession } from "@gitpod/gitpod-protocol/lib/usage";
172176
import { WorkspaceClusterImagebuilderClientProvider } from "./workspace-cluster-imagebuilder-client-provider";
177+
import { BillingMode } from "@gitpod/gitpod-protocol/lib/billing-modes";
178+
import { BillingModes } from "../billing/billing-mode";
173179

174180
// shortcut
175181
export const traceWI = (ctx: TraceContext, wi: Omit<LogContext, "userId">) => TraceContext.setOWI(ctx, wi); // userId is already taken care of in WebsocketConnectionManager
@@ -238,6 +244,8 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
238244

239245
@inject(IDEConfigService) protected readonly ideConfigService: IDEConfigService;
240246

247+
@inject(BillingModes) protected readonly billingModes: BillingModes;
248+
241249
/** Id the uniquely identifies this server instance */
242250
public readonly uuid: string = uuidv4();
243251
public readonly clientMetadata: ClientMetadata;
@@ -2055,13 +2063,14 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
20552063
return await this.projectsService.getProjectEnvironmentVariables(projectId);
20562064
}
20572065

2058-
protected async guardTeamOperation(teamId: string | undefined, op: ResourceAccessOp): Promise<void> {
2066+
protected async guardTeamOperation(teamId: string | undefined, op: ResourceAccessOp): Promise<Team> {
20592067
const team = await this.teamDB.findTeamById(teamId || "");
20602068
if (!team) {
20612069
throw new ResponseError(ErrorCodes.NOT_FOUND, "Team not found");
20622070
}
20632071
const members = await this.teamDB.findMembersByTeam(team.id);
20642072
await this.guardAccess({ kind: "team", subject: team, members }, op);
2073+
return team;
20652074
}
20662075

20672076
public async getTeams(ctx: TraceContext): Promise<Team[]> {
@@ -3211,6 +3220,22 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
32113220
}
32123221
}
32133222

3223+
async getBillingModeForUser(ctx: TraceContextWithSpan): Promise<BillingMode> {
3224+
traceAPIParams(ctx, {});
3225+
3226+
const user = this.checkUser("getBillingModeForUser");
3227+
return this.billingModes.getBillingModeForUser(user);
3228+
}
3229+
3230+
async getBillingModeForTeam(ctx: TraceContextWithSpan, teamId: string): Promise<BillingMode> {
3231+
traceAPIParams(ctx, { teamId });
3232+
3233+
this.checkAndBlockUser("getBillingModeForTeam");
3234+
const team = await this.guardTeamOperation(teamId, "get");
3235+
3236+
return this.billingModes.getBillingModeForTeam(team);
3237+
}
3238+
32143239
//
32153240
//#endregion
32163241

0 commit comments

Comments
 (0)