From 69e8a10c30e0d606106259fc523f34c72fdf37b3 Mon Sep 17 00:00:00 2001 From: Gero Posmyk-Leinemann Date: Thu, 15 Jul 2021 11:59:51 +0000 Subject: [PATCH 1/3] [dev] clean bash in gitpod-io/gitpod workspaces --- dev/image/Dockerfile | 2 +- scripts/setup-google-adc.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dev/image/Dockerfile b/dev/image/Dockerfile index fa1c17daa3b086..91231a0e475596 100644 --- a/dev/image/Dockerfile +++ b/dev/image/Dockerfile @@ -180,7 +180,7 @@ RUN mkdir -p ~/.terraform \ && curl -fsSL -o terraform_linux_amd64.zip ${RELEASE_URL} \ && unzip *.zip \ && rm -f *.zip \ - && printf "terraform -install-autocomplete\n" >>~/.bashrc + && printf "terraform -install-autocomplete 2> /dev/null\n" >>~/.bashrc # Install GraphViz to help debug terraform scripts RUN sudo install-packages graphviz diff --git a/scripts/setup-google-adc.sh b/scripts/setup-google-adc.sh index ebd3b9948f6954..6a878c7b958615 100755 --- a/scripts/setup-google-adc.sh +++ b/scripts/setup-google-adc.sh @@ -16,7 +16,7 @@ if [ ! -f "$GCLOUD_ADC_PATH" ]; then return; fi echo "$GCP_ADC_FILE" > "$GCLOUD_ADC_PATH" - echo "Set GOOGLE_APPLICATION_CREDENTIALS value based on contents from GCP_ADC_FILE" + #echo "Set GOOGLE_APPLICATION_CREDENTIALS value based on contents from GCP_ADC_FILE" fi export GOOGLE_APPLICATION_CREDENTIALS="$GCLOUD_ADC_PATH" From 2fec1a6c243f5b0a5e13fbc7d7be9141768b0c9a Mon Sep 17 00:00:00 2001 From: Gero Posmyk-Leinemann Date: Thu, 15 Jul 2021 14:35:53 +0000 Subject: [PATCH 2/3] [server] Remove dead code: WorkspacePortAuthenticationService --- components/server/src/container-module.ts | 3 - components/server/src/user/user-controller.ts | 7 - .../user/workspace-port-auth-service.spec.ts | 176 ---------------- .../src/user/workspace-port-auth-service.ts | 191 ------------------ 4 files changed, 377 deletions(-) delete mode 100644 components/server/src/user/workspace-port-auth-service.spec.ts delete mode 100644 components/server/src/user/workspace-port-auth-service.ts diff --git a/components/server/src/container-module.ts b/components/server/src/container-module.ts index 209b1ff3d1c6ea..151fb2017068d3 100644 --- a/components/server/src/container-module.ts +++ b/components/server/src/container-module.ts @@ -48,7 +48,6 @@ import { ImageSourceProvider } from './workspace/image-source-provider'; import { WorkspaceGarbageCollector } from './workspace/garbage-collector'; import { TokenGarbageCollector } from './user/token-garbage-collector'; import { WorkspaceDownloadService } from './workspace/workspace-download-service'; -import { WorkspacePortAuthorizationService } from './user/workspace-port-auth-service'; import { WebsocketConnectionManager } from './websocket-connection-manager'; import { OneTimeSecretServer } from './one-time-secret-server'; import { GitpodServer, GitpodClient } from '@gitpod/gitpod-protocol'; @@ -175,8 +174,6 @@ export const productionContainerModule = new ContainerModule((bind, unbind, isBo bind(WorkspaceGarbageCollector).toSelf().inSingletonScope(); bind(WorkspaceDownloadService).toSelf().inSingletonScope(); - bind(WorkspacePortAuthorizationService).toSelf().inSingletonScope(); - bind(OneTimeSecretServer).toSelf().inSingletonScope(); bind(AuthProviderService).toSelf().inSingletonScope(); diff --git a/components/server/src/user/user-controller.ts b/components/server/src/user/user-controller.ts index aed2a122c9bc85..2e7668a1979fbc 100644 --- a/components/server/src/user/user-controller.ts +++ b/components/server/src/user/user-controller.ts @@ -15,7 +15,6 @@ import { GitpodCookie } from "../auth/gitpod-cookie"; import { AuthorizationService } from "./authorization-service"; import { Permission } from "@gitpod/gitpod-protocol/lib/permission"; import { UserService } from "./user-service"; -import { WorkspacePortAuthorizationService } from "./workspace-port-auth-service"; import { parseWorkspaceIdFromHostname } from "@gitpod/gitpod-protocol/lib/util/parse-workspace-id"; import { SessionHandlerProvider } from "../session-handler"; import { URL } from 'url'; @@ -42,7 +41,6 @@ export class UserController { @inject(TosCookie) protected readonly tosCookie: TosCookie; @inject(AuthorizationService) protected readonly authService: AuthorizationService; @inject(UserService) protected readonly userService: UserService; - @inject(WorkspacePortAuthorizationService) protected readonly workspacePortAuthService: WorkspacePortAuthorizationService; @inject(HostContextProvider) protected readonly hostContextProvider: HostContextProvider; @inject(IAnalyticsWriter) protected readonly analytics: IAnalyticsWriter; @inject(SessionHandlerProvider) protected readonly sessionHandlerProvider: SessionHandlerProvider; @@ -292,11 +290,6 @@ export class UserController { res.sendStatus(200); }); - router.get("/auth/workspace-port/:port", async (req: express.Request, res: express.Response, next: express.NextFunction) => { - const authenticatedUser = req.isAuthenticated() && User.is(req.user) && req.user || undefined; - const access = await this.workspacePortAuthService.authorizeWorkspacePortAccess(req.params.port, req.hostname, authenticatedUser, req.header("x-gitpod-port-auth")); - res.sendStatus(access ? 200 : 403); - }); router.get("/auth/monitor", async (req: express.Request, res: express.Response, next: express.NextFunction) => { if (!req.isAuthenticated() || !User.is(req.user)) { // Pretend there's nothing to see diff --git a/components/server/src/user/workspace-port-auth-service.spec.ts b/components/server/src/user/workspace-port-auth-service.spec.ts deleted file mode 100644 index 88d11f9f8a1c19..00000000000000 --- a/components/server/src/user/workspace-port-auth-service.spec.ts +++ /dev/null @@ -1,176 +0,0 @@ -/** - * Copyright (c) 2020 Gitpod GmbH. All rights reserved. - * Licensed under the GNU Affero General Public License (AGPL). - * See License-AGPL.txt in the project root for license information. - */ - -import * as chai from 'chai'; -import { suite, test } from 'mocha-typescript'; -import { WorkspacePortAuthorizationService } from './workspace-port-auth-service'; -const expect = chai.expect; - -@suite -export class WorkspacePortAuthorizationServiceSpec { - - decide(input: { portAccessForUsersOnly: boolean, hasValidCookieForWorkspace: boolean, userAuthenticated?: boolean, isPortPublic: boolean, isWsShared: boolean, isUserWsOwner?: boolean }): boolean { - const cut = new WorkspacePortAuthorizationService(); - const actual = (cut as any).decide(input.portAccessForUsersOnly, input.hasValidCookieForWorkspace, !!input.userAuthenticated, { - isPortPublic: input.isPortPublic, - isWsShared: input.isWsShared, - isUserWsOwner: input.isUserWsOwner - }); - return actual; - } - - @test public decide_all_notOwner_unshared_private() { - expect(this.decide({ - portAccessForUsersOnly: false, - hasValidCookieForWorkspace: false, - isPortPublic: false, - isWsShared: false - })).to.equal(false); - } - - @test public decide_all_owner_unshared_private() { - expect(this.decide({ - portAccessForUsersOnly: false, - hasValidCookieForWorkspace: true, - isUserWsOwner: true, - isPortPublic: false, - isWsShared: false - })).to.equal(true); - } - - @test public decide_all_notOwner_shared_private() { - expect(this.decide({ - portAccessForUsersOnly: false, - hasValidCookieForWorkspace: false, - isUserWsOwner: true, - isPortPublic: false, - isWsShared: true - })).to.equal(true); - } - - @test public decide_all_notOwner_unshared_public() { - expect(this.decide({ - portAccessForUsersOnly: false, - hasValidCookieForWorkspace: false, - isPortPublic: true, - isWsShared: false - })).to.equal(true); - } - - @test public decide_usersOnly_owner_unshared_private() { - expect(this.decide({ - portAccessForUsersOnly: true, - hasValidCookieForWorkspace: true, - isUserWsOwner: true, - isPortPublic: false, - isWsShared: false - })).to.equal(true); - } - - @test public decide_usersOnly_notOwner_unshared_private() { - expect(this.decide({ - portAccessForUsersOnly: true, - hasValidCookieForWorkspace: false, - userAuthenticated: false, - isPortPublic: false, - isWsShared: false - })).to.equal(false); - } - - @test public decide_usersOnly_notOwner_shared_private() { - expect(this.decide({ - portAccessForUsersOnly: true, - hasValidCookieForWorkspace: false, - userAuthenticated: false, - isPortPublic: false, - isWsShared: true - })).to.equal(false); - } - - @test public decide_usersOnly_notOwner_unshared_public() { - expect(this.decide({ - portAccessForUsersOnly: true, - hasValidCookieForWorkspace: false, - userAuthenticated: false, - isPortPublic: true, - isWsShared: false - })).to.equal(false); - } - - @test public decide_usersOnly_notOwner_shared_public() { - expect(this.decide({ - portAccessForUsersOnly: true, - hasValidCookieForWorkspace: false, - userAuthenticated: false, - isPortPublic: true, - isWsShared: true - })).to.equal(false); - } - - @test public decide_usersOnly_notOwner_unshared_private_authdUser() { - expect(this.decide({ - portAccessForUsersOnly: true, - hasValidCookieForWorkspace: false, - userAuthenticated: true, - isPortPublic: false, - isWsShared: false - })).to.equal(false); - } - - @test public decide_usersOnly_notOwner_shared_private_authdUser() { - expect(this.decide({ - portAccessForUsersOnly: true, - hasValidCookieForWorkspace: false, - userAuthenticated: true, - isPortPublic: false, - isWsShared: true - })).to.equal(true); - } - - @test public decide_usersOnly_notOwner_unshared_public_authdUser() { - expect(this.decide({ - portAccessForUsersOnly: true, - hasValidCookieForWorkspace: false, - userAuthenticated: true, - isPortPublic: true, - isWsShared: false - })).to.equal(true); - } - - @test public decide_usersOnly_notOwner_shared_public_authdUser() { - expect(this.decide({ - portAccessForUsersOnly: true, - hasValidCookieForWorkspace: false, - userAuthenticated: true, - isPortPublic: true, - isWsShared: true - })).to.equal(true); - } - - @test public decide_usersOnly_owner_shared_private_authdUser() { - expect(this.decide({ - portAccessForUsersOnly: true, - hasValidCookieForWorkspace: true, - userAuthenticated: true, - isUserWsOwner: true, - isPortPublic: false, - isWsShared: true - })).to.equal(true); - } - - @test public decide_usersOnly_notOwner_exshared_private_authdUser() { - expect(this.decide({ - portAccessForUsersOnly: true, - hasValidCookieForWorkspace: true, - userAuthenticated: true, - isUserWsOwner: false, - isPortPublic: false, - isWsShared: false - })).to.equal(false); - } -} - -module.exports = new WorkspacePortAuthorizationServiceSpec() diff --git a/components/server/src/user/workspace-port-auth-service.ts b/components/server/src/user/workspace-port-auth-service.ts deleted file mode 100644 index 16c58ada41aded..00000000000000 --- a/components/server/src/user/workspace-port-auth-service.ts +++ /dev/null @@ -1,191 +0,0 @@ -/** - * Copyright (c) 2020 Gitpod GmbH. All rights reserved. - * Licensed under the GNU Affero General Public License (AGPL). - * See License-AGPL.txt in the project root for license information. - */ - -import { injectable, inject } from "inversify"; - -import { log } from '@gitpod/gitpod-protocol/lib/util/logging'; -import { WorkspaceDB, UserDB } from "@gitpod/gitpod-db/lib"; -import { User } from "@gitpod/gitpod-protocol"; -import { WorkspaceManagerClientProvider } from "@gitpod/ws-manager/lib/client-provider"; -import { DescribeWorkspaceRequest, PortVisibility } from "@gitpod/ws-manager/lib"; -import { worspacePortAuthCookieName as workspacePortAuthCookieName } from "@gitpod/gitpod-protocol/lib/util/workspace-port-authentication"; -import { GarbageCollectedCache } from "@gitpod/gitpod-protocol/lib/util/garbage-collected-cache"; -import { parseWorkspaceIdFromHostname } from "@gitpod/gitpod-protocol/lib/util/parse-workspace-id"; - -import { Env } from "../env"; -import { TokenService } from "./token-service"; - -@injectable() -export class WorkspacePortAuthorizationService { - @inject(Env) protected readonly env: Env; - @inject(WorkspaceDB) protected readonly workspaceDB: WorkspaceDB; - @inject(UserDB) protected readonly userDb: UserDB; - @inject(WorkspaceManagerClientProvider) protected readonly workspaceManagerClientProvider: WorkspaceManagerClientProvider; - - protected readonly accessDecisionCache: GarbageCollectedCache = new GarbageCollectedCache(5, 10); - - async authorizeWorkspacePortAccess(port: any, hostname: string, authenticatedUser: User | undefined, portAuthHeader: string | undefined): Promise { - const workspacePort = this.parseWorkspacePortFromParameter(port); - const workspaceId = parseWorkspaceIdFromHostname(hostname); - if (!workspaceId || !workspacePort) { - return false; - } - - let portAuthCookieValue = undefined; - if (portAuthHeader) { - const portAuthCookieName = workspacePortAuthCookieName(this.env.hostUrl.toStringWoRootSlash(), workspaceId); - const cookieValue = this.cookieValue(portAuthHeader.split(';'), portAuthCookieName); - if (cookieValue && typeof cookieValue === 'string') { - portAuthCookieValue = cookieValue; - } - } - - // up until now everything is sync, now we start with the async + DB + ws-manager turnarounds: cache the response for 5s - let cacheKey = `${workspaceId}/${workspacePort}`; - if (portAuthCookieValue) { - // We have two kind of requests: - // - for public ports / shared workspace: come without cookie - // - private ports with cookie - // Both should be cached, but we also have to be as specific as possible to avoid unauthorized users share the - // authorization with an authorized users (on private ports) - cacheKey = `${workspaceId}/${workspacePort}/${portAuthCookieValue}`; - } - const cachedDecision = this.accessDecisionCache.get(cacheKey); - if (cachedDecision) { - return cachedDecision; - } - - const [hasValidCookieForWorkspace, wsAndPortConfig] = await Promise.all([ - this.checkPortAuthHeader(workspaceId, portAuthCookieValue), - this.checkWorkspaceAndPortVisibility(workspaceId, workspacePort, authenticatedUser && authenticatedUser.id) - ]); - - const decision = this.decide(this.env.portAccessForUsersOnly, hasValidCookieForWorkspace, !!authenticatedUser, wsAndPortConfig); - this.accessDecisionCache.set(cacheKey, decision); - - return decision; - } - - protected decide(portAccessForUsersOnly: boolean, hasValidCookieForWorkspace: boolean, userAuthenticated: boolean, wsAndPortConfig: WsAndPortConfig): boolean { - let access = false; - if (portAccessForUsersOnly) { - // Originally implemented for DWave - if (hasValidCookieForWorkspace) { - // This matches owner and users shared with - even up to 20 minutes after unsharing, due to cookie validity - if (!wsAndPortConfig.isWsShared) { - // WS not shared: Only owner may access - if (wsAndPortConfig.isUserWsOwner) { - access = true; - } - } else { - // Workspace is shared: owner and all users may access - access = true; - } - } else if (wsAndPortConfig.isPortPublic || wsAndPortConfig.isWsShared) { - if (userAuthenticated) { - access = true; - } - } - } else { - // The default case for gitpod.io - if (hasValidCookieForWorkspace || wsAndPortConfig.isPortPublic || wsAndPortConfig.isWsShared) { - access = true; - } - } - - return access; - } - - protected async checkPortAuthHeader(workspaceId: string, portAuthCookieValue: string | undefined): Promise { - if (!portAuthCookieValue) { - return false; - } - - const maybeTokenEntry = await this.userDb.findTokenEntryById(portAuthCookieValue); - if (!maybeTokenEntry) { - return false; - } - - const expectedTokenValue = TokenService.generateWorkspacePortAuthScope(workspaceId); - if (maybeTokenEntry.token.scopes.some(s => s === expectedTokenValue)) { - // This is true for everyone who has had a frontend open during the last 30mins (owner, people shared with) - return true; - } - - return false; - } - - protected async checkWorkspaceAndPortVisibility(workspaceId: string, workspacePort: number, userId: string | undefined): Promise { - // Is the workspace port public? - const authData = await this.workspaceDB.findWorkspacePortsAuthDataById(workspaceId); - if (!authData) { - return {}; - } - const isUserWsOwner = userId ? authData.workspace.ownerId === userId : undefined; - - // In case the workspace is shared: go for it! - const isWsShared = authData.workspace.shareable; - - // The workspace is not shared: Now we need to look for the specific port: Is it public? - const visibility = await this.getWorkspacePortVisibility(authData.instance.id, authData.instance.region, workspacePort); - const isPortPublic = visibility === PortVisibility.PORT_VISIBILITY_PUBLIC; - - // Whe workspace is not shared, port not public - return { isPortPublic, isWsShared, isUserWsOwner }; - } - - protected parseWorkspacePortFromParameter(maybePort: any): number | undefined { - if (!maybePort) { - return undefined; - } - try { - const workspacePort = Number.parseInt(maybePort); - if (workspacePort < 0) { - return undefined; - } - return workspacePort; - } catch (err) { - return undefined; - } - } - - protected async getWorkspacePortVisibility(instanceId: string, region: string, workspacePort: number): Promise { - try { - const describeRequest = new DescribeWorkspaceRequest(); - describeRequest.setId(instanceId); - - const client = await this.workspaceManagerClientProvider.get(region); - const describeWorkspaceResponse = await client.describeWorkspace({}, describeRequest); - const portSpecs = describeWorkspaceResponse.getStatus()!.getSpec()!.getExposedPortsList(); - - const portSpec = portSpecs.find((p) => p.getPort() === workspacePort); - if (!portSpec) { - return undefined; - } - return portSpec.getVisibility(); - } catch (err) { - log.debug({ instanceId }, "Error while determening port visibility", err, { region, workspacePort }); - return undefined; - } - } - - protected cookieValue(cookies: string[], cookieName: string): string | undefined { - for (const c of cookies) { - const [k, v] = c.split("="); - const key = k.trim(); - if (key === cookieName && !!v) { - return v.trim(); - } - } - return undefined; - } -} - -interface WsAndPortConfig { - isPortPublic?: boolean; - isWsShared?: boolean; - isUserWsOwner?: boolean; -} \ No newline at end of file From 25a5fd6c7498ac33b6b87fca3c0299128a1d35be Mon Sep 17 00:00:00 2001 From: Gero Posmyk-Leinemann Date: Tue, 27 Jul 2021 08:38:37 +0000 Subject: [PATCH 3/3] [server] Introduce Config (without using it) To be able to easily map Env+EnvEE into config there was some minor refactoring/cleanup necessary. This has be done in a way to be as straigth forward as possible to minimize the risk of breaking things, while making it possible to easily write an alternative Config parser. --- components/server/ee/src/container-module.ts | 1 + components/server/ee/src/env.ts | 2 - .../server/ee/src/prebuilds/github-app.ts | 7 +- components/server/src/auth/rate-limiter.ts | 19 +- .../server/src/code-sync/code-sync-service.ts | 15 +- components/server/src/config.ts | 218 ++++++++++++++++++ components/server/src/container-module.ts | 39 +++- components/server/src/env.ts | 47 +--- components/server/src/server.ts | 3 + .../theia-plugin/theia-plugin-controller.ts | 6 +- .../src/theia-plugin/theia-plugin-service.ts | 3 +- .../src/websocket-connection-manager.ts | 7 +- 12 files changed, 290 insertions(+), 77 deletions(-) create mode 100644 components/server/src/config.ts diff --git a/components/server/ee/src/container-module.ts b/components/server/ee/src/container-module.ts index ecc239aaf8bad6..9c2c7851a16699 100644 --- a/components/server/ee/src/container-module.ts +++ b/components/server/ee/src/container-module.ts @@ -87,6 +87,7 @@ export const productionEEContainerModule = new ContainerModule((bind, unbind, is // various bind(EnvEE).toSelf().inSingletonScope(); rebind(Env).to(EnvEE).inSingletonScope(); + rebind(MessageBusIntegration).to(MessageBusIntegrationEE).inSingletonScope(); rebind(HostContainerMapping).to(HostContainerMappingEE).inSingletonScope(); bind(EMailDomainService).to(EMailDomainServiceImpl).inSingletonScope(); diff --git a/components/server/ee/src/env.ts b/components/server/ee/src/env.ts index c81ae2d5d0ddab..70357b461c5ed1 100644 --- a/components/server/ee/src/env.ts +++ b/components/server/ee/src/env.ts @@ -10,8 +10,6 @@ import { getEnvVar, filePathTelepresenceAware } from '@gitpod/gitpod-protocol/li import { readOptionsFromFile } from '@gitpod/gitpod-payment-endpoint/lib/chargebee'; import { Env } from '../../src/env'; - - @injectable() export class EnvEE extends Env { readonly chargebeeProviderOptions = readOptionsFromFile(filePathTelepresenceAware('/chargebee/providerOptions')); diff --git a/components/server/ee/src/prebuilds/github-app.ts b/components/server/ee/src/prebuilds/github-app.ts index 6b21d5aea2e3a2..566cf65fb55781 100644 --- a/components/server/ee/src/prebuilds/github-app.ts +++ b/components/server/ee/src/prebuilds/github-app.ts @@ -55,14 +55,15 @@ export class GithubApp { appId: env.githubAppAppID, privateKey: GithubApp.loadPrivateKey(env.githubAppCertPath), secret: env.githubAppWebhookSecret, - logLevel: env.githubAppLogLevel as Options["logLevel"] + logLevel: env.githubAppLogLevel as Options["logLevel"], + baseUrl: env.githubAppGHEHost, }) - }) + }); log.debug("Starting GitHub app integration", { appId: env.githubAppAppID, cert: env.githubAppCertPath, secret: env.githubAppWebhookSecret - }) + }); this.server.load(this.buildApp.bind(this)); } } diff --git a/components/server/src/auth/rate-limiter.ts b/components/server/src/auth/rate-limiter.ts index 394bc6634233f5..9c58e64836e248 100644 --- a/components/server/src/auth/rate-limiter.ts +++ b/components/server/src/auth/rate-limiter.ts @@ -24,12 +24,12 @@ type FunctionsConfig = { points: number, } } -type RateLimiterConfig = { +export type RateLimiterConfig = { groups: GroupsConfig, functions: FunctionsConfig, }; -function readConfig(): RateLimiterConfig { +function getConfig(config: RateLimiterConfig): RateLimiterConfig { const defaultGroups: GroupsConfig = { default: { points: 60000, // 1,000 calls per user per second @@ -167,11 +167,9 @@ function readConfig(): RateLimiterConfig { "trackEvent": { group: "default", points: 1 }, }; - const fromEnv = JSON.parse(process.env.RATE_LIMITER_CONFIG || "{}") - return { - groups: { ...defaultGroups, ...fromEnv.groups }, - functions: { ...defaultFunctions, ...fromEnv.functions } + groups: { ...defaultGroups, ...config.groups }, + functions: { ...defaultFunctions, ...config.functions } }; } @@ -184,9 +182,9 @@ export class UserRateLimiter { private static instance_: UserRateLimiter; - public static instance(): UserRateLimiter { + public static instance(config: RateLimiterConfig): UserRateLimiter { if (!UserRateLimiter.instance_) { - UserRateLimiter.instance_ = new UserRateLimiter(); + UserRateLimiter.instance_ = new UserRateLimiter(config); } return UserRateLimiter.instance_; } @@ -194,9 +192,8 @@ export class UserRateLimiter { private readonly config: RateLimiterConfig; private readonly limiters: { [key: string]: RateLimiterMemory }; - private constructor() { - this.config = readConfig(); - + private constructor(config: RateLimiterConfig) { + this.config = getConfig(config); this.limiters = {}; Object.keys(this.config.groups).forEach(group => { this.limiters[group] = new RateLimiterMemory({ diff --git a/components/server/src/code-sync/code-sync-service.ts b/components/server/src/code-sync/code-sync-service.ts index ae868e8b7b5784..70f4ea622f8093 100644 --- a/components/server/src/code-sync/code-sync-service.ts +++ b/components/server/src/code-sync/code-sync-service.ts @@ -21,12 +21,13 @@ import uuid = require('uuid'); import { accessCodeSyncStorage, UserRateLimiter } from '../auth/rate-limiter'; import { increaseApiCallUserCounter } from '../prometheus-metrics'; import { TheiaPluginService } from '../theia-plugin/theia-plugin-service'; +import { Env } from '../env'; // By default: 5 kind of resources * 20 revs * 1Mb = 100Mb max in the content service for user data. const defautltRevLimit = 20; // It should keep it aligned with client_max_body_size for /code-sync location. const defaultContentLimit = '1Mb'; -const codeSyncConfig: Partial<{ +export type CodeSyncConfig = Partial<{ revLimit: number contentLimit: number resources: { @@ -34,7 +35,7 @@ const codeSyncConfig: Partial<{ revLimit?: number } } -}> = JSON.parse(process.env.CODE_SYNC_CONFIG || "{}"); +}>; const objectPrefix = 'code-sync/'; function toObjectName(resource: ServerResource, rev: string): string { @@ -55,6 +56,9 @@ const userSettingsUri = 'user_storage:settings.json'; @injectable() export class CodeSyncService { + @inject(Env) + private readonly env: Env; + @inject(BearerAuth) private readonly auth: BearerAuth; @@ -71,6 +75,7 @@ export class CodeSyncService { private readonly userStorageResourcesDB: UserStorageResourcesDB; get apiRouter(): express.Router { + const config = this.env.codeSyncConfig; const router = express.Router(); router.use((_, res, next) => { // to correlate errors reported by users with errors logged by the server @@ -87,7 +92,7 @@ export class CodeSyncService { const id = req.user.id; increaseApiCallUserCounter(accessCodeSyncStorage, id); try { - await UserRateLimiter.instance().consume(id, accessCodeSyncStorage); + await UserRateLimiter.instance(this.env.rateLimiter).consume(id, accessCodeSyncStorage); } catch (e) { if (e instanceof Error) { throw e; @@ -207,7 +212,7 @@ export class CodeSyncService { res.send(content); }); router.post('/v1/resource/:resource', bodyParser.text({ - limit: codeSyncConfig?.contentLimit || defaultContentLimit + limit: config?.contentLimit || defaultContentLimit }), async (req, res) => { if (!User.is(req.user)) { res.sendStatus(204); @@ -222,7 +227,7 @@ export class CodeSyncService { if (latestRev === fromTheiaRev) { latestRev = undefined; } - const revLimit = resourceKey === 'machines' ? 1 : codeSyncConfig.resources?.[resourceKey]?.revLimit || codeSyncConfig?.revLimit || defautltRevLimit; + const revLimit = resourceKey === 'machines' ? 1 : config.resources?.[resourceKey]?.revLimit || config?.revLimit || defautltRevLimit; const userId = req.user.id; let oldObject: string | undefined; const contentType = req.headers['content-type'] || '*/*'; diff --git a/components/server/src/config.ts b/components/server/src/config.ts new file mode 100644 index 00000000000000..8dae50d8a4bf76 --- /dev/null +++ b/components/server/src/config.ts @@ -0,0 +1,218 @@ +/** + * Copyright (c) 2020 Gitpod GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License-AGPL.txt in the project root for license information. + */ + +import { GitpodHostUrl } from '@gitpod/gitpod-protocol/lib/util/gitpod-host-url'; +import { AuthProviderParams } from './auth/auth-provider'; + +import { Branding, NamedWorkspaceFeatureFlag } from '@gitpod/gitpod-protocol'; + +import { RateLimiterConfig } from './auth/rate-limiter'; +import { Env } from './env'; +import { CodeSyncConfig } from './code-sync/code-sync-service'; +import { ChargebeeProviderOptions } from "@gitpod/gitpod-payment-endpoint/lib/chargebee"; +import { EnvEE } from '../ee/src/env'; + +export const Config = Symbol("Config"); +export interface Config { + version: string; + hostUrl: GitpodHostUrl; + + license: string | undefined; + trialLicensePrivateKey: string | undefined; + + heartbeat: { + intervalSeconds: number; + timeoutSeconds: number, + }; + + workspaceDefaults: { + ideVersion: string; + ideImageRepo: string; + ideImage: string; + ideImageAliases: { [index: string]: string }; + workspaceImage: string; + previewFeatureFlags: NamedWorkspaceFeatureFlag[]; + defaultFeatureFlags: NamedWorkspaceFeatureFlag[]; + }; + + session: { + maxAgeMs: number; + secret: string; + }; + + githubApp?: { + enabled: boolean; + appId: number; + webhookSecret: string; + authProviderId: string; + certPath: string; + marketplaceName: string; + logLevel?: string; + }; + + definitelyGpDisabled: boolean; + + workspaceGarbageCollection: { + disabled: boolean; + startDate: number; + chunkLimit: number; + minAgeDays: number; + minAgePrebuildDays: number; + contentRetentionPeriodDays: number; + contentChunkLimit: number; + }; + + enableLocalApp: boolean; + + authProviderConfigs: AuthProviderParams[]; + disableDynamicAuthProviderLogin: boolean; + + // TODO(gpl) Can we remove this? + brandingConfig: Branding; + + /** + * The maximum number of environment variables a user can have configured in their list at any given point in time. + * Note: This limit should be so high that no regular user ever reaches it. + */ + maxEnvvarPerUserCount: number; + + /** maxConcurrentPrebuildsPerRef is the maximum number of prebuilds we allow per ref type at any given time */ + maxConcurrentPrebuildsPerRef: number; + + incrementalPrebuilds: { + repositoryPassList: string[]; + commitHistory: number; + }; + + blockNewUsers: { + enabled: boolean; + passlist: string[]; + } + + makeNewUsersAdmin: boolean; + + /** this value - if present - overrides the default naming scheme for the GCloud bucket that Theia Plugins are stored in */ + theiaPluginsBucketNameOverride: string | undefined; + + /** defaultBaseImageRegistryWhitelist is the list of registryies users get acces to by default */ + defaultBaseImageRegistryWhitelist: string[]; + + // TODO(gpl): can we remove this? We never set the value anywhere it seems + insecureNoDomain: boolean; + + runDbDeleter: boolean; + + oauthServer: { + enabled: boolean; + jwtSecret: string; + } + + /** + * The configuration for the rate limiter we (mainly) use for the websocket API + */ + rateLimiter: RateLimiterConfig; + + /** + * The address content service clients connect to + * Example: content-service:8080 + */ + contentServiceAddress: string; + + /** + * TODO(gpl) Looks like this is not used anymore! Verify and remove + */ + serverProxyApiKey?: string; + + codeSyncConfig: CodeSyncConfig; + + /** + * Payment related options + */ + chargebeeProviderOptions?: ChargebeeProviderOptions; + enablePayment?: boolean; +} + +export namespace EnvConfig { + export function fromEnv(env: Env): Config { + return { + version: env.version, + hostUrl: env.hostUrl, + license: env.gitpodLicense, + trialLicensePrivateKey: env.trialLicensePrivateKey, + heartbeat: { + intervalSeconds: env.theiaHeartbeatInterval, + timeoutSeconds: env.workspaceUserTimeout, + }, + workspaceDefaults: { + ideVersion: env.theiaVersion, + ideImageRepo: env.theiaImageRepo, + ideImage: env.ideDefaultImage, + ideImageAliases: env.ideImageAliases, + workspaceImage: env.workspaceDefaultImage, + previewFeatureFlags: env.previewFeatureFlags, + defaultFeatureFlags: env.defaultFeatureFlags, + }, + session: { + maxAgeMs: env.sessionMaxAgeMs, + secret: env.sessionSecret, + }, + githubApp: { + enabled: env.githubAppEnabled, + appId: env.githubAppAppID, + webhookSecret: env.githubAppWebhookSecret, + authProviderId: env.githubAppAuthProviderId, + certPath: env.githubAppCertPath, + marketplaceName: env.githubAppMarketplaceName, + logLevel: env.githubAppLogLevel, + }, + definitelyGpDisabled: env.definitelyGpDisabled, + workspaceGarbageCollection: { + disabled: env.garbageCollectionDisabled, + startDate: env.garbageCollectionStartDate, + chunkLimit: env.garbageCollectionLimit, + minAgeDays: env.daysBeforeGarbageCollection, + minAgePrebuildDays: env.daysBeforeGarbageCollectingPrebuilds, + contentRetentionPeriodDays: env.workspaceDeletionRetentionPeriodDays, + contentChunkLimit: env.workspaceDeletionLimit, + }, + enableLocalApp: env.enableLocalApp, + authProviderConfigs: env.authProviderConfigs, + disableDynamicAuthProviderLogin: env.disableDynamicAuthProviderLogin, + brandingConfig: env.brandingConfig, + maxEnvvarPerUserCount: env.maxUserEnvvarCount, + maxConcurrentPrebuildsPerRef: env.maxConcurrentPrebuildsPerRef, + incrementalPrebuilds: { + repositoryPassList: env.incrementalPrebuildsRepositoryPassList, + commitHistory: env.incrementalPrebuildsCommitHistory, + }, + blockNewUsers: { + enabled: env.blockNewUsers, + passlist: env.blockNewUsersPassList, + }, + makeNewUsersAdmin: env.makeNewUsersAdmin, + theiaPluginsBucketNameOverride: env.theiaPluginsBucketNameOverride, + defaultBaseImageRegistryWhitelist: env.defaultBaseImageRegistryWhitelist, + insecureNoDomain: env.insecureNoDomain, + runDbDeleter: env.runDbDeleter, + oauthServer: { + enabled: env.enableOAuthServer, + jwtSecret: env.oauthServerJWTSecret, + }, + rateLimiter: env.rateLimiter, + contentServiceAddress: env.contentServiceAddress, + serverProxyApiKey: env.serverProxyApiKey, + codeSyncConfig: env.codeSyncConfig, + }; + } + export function fromEnvEE(env: EnvEE): Config { + const config = EnvConfig.fromEnv(env); + return { + ...config, + chargebeeProviderOptions: env.chargebeeProviderOptions, + enablePayment: env.enablePayment, + } + } +} \ No newline at end of file diff --git a/components/server/src/container-module.ts b/components/server/src/container-module.ts index 151fb2017068d3..564f0000456e84 100644 --- a/components/server/src/container-module.ts +++ b/components/server/src/container-module.ts @@ -79,9 +79,14 @@ import { HeadlessLogController } from './workspace/headless-log-controller'; import { IAnalyticsWriter } from '@gitpod/gitpod-protocol/lib/analytics'; import { HeadlessLogServiceClient } from '@gitpod/content-service/lib/headless-log_grpc_pb'; import { ProjectsService } from './projects/projects-service'; +import { EnvConfig, Config } from './config'; export const productionContainerModule = new ContainerModule((bind, unbind, isBound, rebind) => { bind(Env).toSelf().inSingletonScope(); + bind(Config).toDynamicValue(ctx => { + const env = ctx.container.get(Env); + return EnvConfig.fromEnv(env); + }).inSingletonScope(); bind(UserService).toSelf().inSingletonScope(); bind(UserDeletionService).toSelf().inSingletonScope(); @@ -124,7 +129,8 @@ export const productionContainerModule = new ContainerModule((bind, unbind, isBo bind(WebsocketConnectionManager).toDynamicValue(ctx => { const serverFactory = () => ctx.container.get>(GitpodServerImpl); const hostContextProvider = ctx.container.get(HostContextProvider); - return new WebsocketConnectionManager(serverFactory, hostContextProvider); + const env = ctx.container.get(Env); + return new WebsocketConnectionManager(serverFactory, hostContextProvider, env.rateLimiter); } ).inSingletonScope(); @@ -181,17 +187,26 @@ export const productionContainerModule = new ContainerModule((bind, unbind, isBo bind(TermsProvider).toSelf().inSingletonScope(); - const contentServiceAddress = process.env.CONTENT_SERVICE_ADDRESS || "content-service:8080"; - const contentServiceClient = new ContentServiceClient(contentServiceAddress, grpc.credentials.createInsecure()) - bind(ContentServiceClient).toConstantValue(contentServiceClient); - const blobServiceClient = new BlobServiceClient(contentServiceAddress, grpc.credentials.createInsecure()) - bind(BlobServiceClient).toConstantValue(blobServiceClient); - const workspaceServiceClient = new WorkspaceServiceClient(contentServiceAddress, grpc.credentials.createInsecure()) - bind(WorkspaceServiceClient).toConstantValue(workspaceServiceClient); - const idePluginServiceClient = new IDEPluginServiceClient(contentServiceAddress, grpc.credentials.createInsecure()) - bind(IDEPluginServiceClient).toConstantValue(idePluginServiceClient); - const headlessLogServiceClient = new HeadlessLogServiceClient(contentServiceAddress, grpc.credentials.createInsecure()) - bind(HeadlessLogServiceClient).toConstantValue(headlessLogServiceClient); + bind(ContentServiceClient).toDynamicValue(ctx => { + const env = ctx.container.get(Env); + return new ContentServiceClient(env.contentServiceAddress, grpc.credentials.createInsecure()); + }); + bind(BlobServiceClient).toDynamicValue(ctx => { + const env = ctx.container.get(Env); + return new BlobServiceClient(env.contentServiceAddress, grpc.credentials.createInsecure()); + }); + bind(WorkspaceServiceClient).toDynamicValue(ctx => { + const env = ctx.container.get(Env); + return new WorkspaceServiceClient(env.contentServiceAddress, grpc.credentials.createInsecure()); + }); + bind(IDEPluginServiceClient).toDynamicValue(ctx => { + const env = ctx.container.get(Env); + return new IDEPluginServiceClient(env.contentServiceAddress, grpc.credentials.createInsecure()); + }); + bind(HeadlessLogServiceClient).toDynamicValue(ctx => { + const env = ctx.container.get(Env); + return new HeadlessLogServiceClient(env.contentServiceAddress, grpc.credentials.createInsecure()); + }); bind(StorageClient).to(ContentServiceStorageClient).inSingletonScope(); diff --git a/components/server/src/env.ts b/components/server/src/env.ts index bf45be5ab126f4..59d405d9b6e318 100644 --- a/components/server/src/env.ts +++ b/components/server/src/env.ts @@ -10,19 +10,16 @@ import { GitpodHostUrl } from '@gitpod/gitpod-protocol/lib/util/gitpod-host-url' import { AbstractComponentEnv, getEnvVar } from '@gitpod/gitpod-protocol/lib/env'; import { AuthProviderParams, parseAuthProviderParamsFromEnv } from './auth/auth-provider'; -import * as fs from "fs"; import { Branding, NamedWorkspaceFeatureFlag, WorkspaceFeatureFlags } from '@gitpod/gitpod-protocol'; import { BrandingParser } from './branding-parser'; +import { RateLimiterConfig } from './auth/rate-limiter'; @injectable() export class Env extends AbstractComponentEnv { readonly serverVersion = process.env.SERVER_VERSION || 'dev'; - readonly hostUrl = new GitpodHostUrl(process.env.HOST_URL || 'https://gitpod.io'); - readonly localhostUrl?: GitpodHostUrl = process.env.LOCALHOST_URL ? new GitpodHostUrl(process.env.LOCALHOST_URL) : undefined; - readonly theiaPort = Number.parseInt(process.env.THEIA_PORT || '23000', 10) || 23000; get theiaHeartbeatInterval() { const envValue = process.env.THEIA_HEARTBEAT_INTERVAL; return envValue ? parseInt(envValue, 10) : (1 * 60 * 1000); @@ -34,7 +31,6 @@ export class Env extends AbstractComponentEnv { readonly theiaVersion = process.env.THEIA_VERSION || this.serverVersion; readonly theiaImageRepo = process.env.THEIA_IMAGE_REPO || 'unknown'; - readonly theiaMounted = process.env.THEIA_MOUNTED === "true"; readonly ideDefaultImage = `${this.theiaImageRepo}:${this.theiaVersion}`; readonly workspaceDefaultImage = process.env.WORKSPACE_DEFAULT_IMAGE || "gitpod/workspace-full:latest"; @@ -66,8 +62,6 @@ export class Env extends AbstractComponentEnv { return value; } - readonly gitpodRegion: string = process.env.GITPOD_REGION || 'unknown'; - readonly sessionMaxAgeMs: number = Number.parseInt(process.env.SESSION_MAX_AGE_MS || '259200000' /* 3 days */, 10); readonly githubAppEnabled: boolean = process.env.GITPOD_GITHUB_APP_ENABLED == "true"; @@ -77,6 +71,7 @@ export class Env extends AbstractComponentEnv { readonly githubAppCertPath: string = process.env.GITPOD_GITHUB_APP_CERT_PATH || "unknown"; readonly githubAppMarketplaceName: string = process.env.GITPOD_GITHUB_APP_MKT_NAME || "unknown"; readonly githubAppLogLevel?: string = process.env.LOG_LEVEL; + readonly githubAppGHEHost?: string = process.env.GHE_HOST; readonly definitelyGpDisabled: boolean = process.env.GITPOD_DEFINITELY_GP_DISABLED == "true"; @@ -132,28 +127,6 @@ export class Env extends AbstractComponentEnv { })() readonly incrementalPrebuildsCommitHistory: number = Number.parseInt(process.env.INCREMENTAL_PREBUILDS_COMMIT_HISTORY || '100', 10) || 100; - protected gitpodLayernameFromFilesystem: string | null | undefined; - protected readGitpodLayernameFromFilesystem(): string | undefined { - if (this.gitpodLayernameFromFilesystem === null) { - // we've tried reading in the past, but were not able to do so - return undefined; - } - if (this.gitpodLayernameFromFilesystem !== undefined) { - // we have read this name previously and it worked - return this.gitpodLayernameFromFilesystem; - } - - try { - this.gitpodLayernameFromFilesystem = fs.readFileSync("/gplayername.txt").toString().trim().split("\n")[0]; - } catch (err) { - console.debug('unable to read /gplayername.txt - this might be ok', err) - this.gitpodLayernameFromFilesystem = null; - return undefined; - } - - return this.gitpodLayernameFromFilesystem; - } - protected parseBool(name: string) { return getEnvVar(name, 'false') === 'true'; } @@ -188,12 +161,7 @@ export class Env extends AbstractComponentEnv { } })(); - /** defaults to: false */ - readonly portAccessForUsersOnly: boolean = this.parsePortAccessForUsersOnly(); - protected parsePortAccessForUsersOnly() { - return getEnvVar('PORT_ACCESS_FOR_USERS_ONLY', 'false') === 'true'; - } - + // TODO(gpl): can we remove this? readonly insecureNoDomain: boolean = getEnvVar('SERVE_INSECURE_NO_DOMAIN', 'false') === 'true'; readonly sessionSecret = this.parseSessionSecret(); @@ -207,4 +175,13 @@ export class Env extends AbstractComponentEnv { readonly oauthServerJWTSecret = getEnvVar("OAUTH_SERVER_JWT_SECRET"); readonly imageBuilderAddress = getEnvVar('IMAGE_BUILDER_SERVICE', "image-builder-mk3:8080"); + + readonly rateLimiter: RateLimiterConfig = JSON.parse(process.env.RATE_LIMITER_CONFIG || "{}"); + + readonly contentServiceAddress = process.env.CONTENT_SERVICE_ADDRESS || "content-service:8080"; + + /** TODO(gpl) Looks like this is not used anymore! Verify and remove */ + readonly serverProxyApiKey = process.env.SERVER_PROXY_APIKEY; + + readonly codeSyncConfig = JSON.parse(process.env.CODE_SYNC_CONFIG || "{}"); } diff --git a/components/server/src/server.ts b/components/server/src/server.ts index 28278396dc4535..10b140fb881b9f 100644 --- a/components/server/src/server.ts +++ b/components/server/src/server.ts @@ -41,12 +41,14 @@ import { CodeSyncService } from './code-sync/code-sync-service'; import { increaseHttpRequestCounter, observeHttpRequestDuration } from './prometheus-metrics'; import { OAuthController } from './oauth-server/oauth-controller'; import { HeadlessLogController } from './workspace/headless-log-controller'; +import { Config } from './config'; @injectable() export class Server { static readonly EVENT_ON_START = 'start'; @inject(Env) protected readonly env: Env; + @inject(Config) protected readonly config: Config; @inject(SessionHandlerProvider) protected sessionHandlerProvider: SessionHandlerProvider; @inject(Authenticator) protected authenticator: Authenticator; @inject(UserController) protected readonly userController: UserController; @@ -80,6 +82,7 @@ export class Server { public async init(app: express.Application) { log.info('Initializing'); + log.info('config', { config: JSON.stringify(this.config, undefined, 2) }); // metrics app.use((req: express.Request, res: express.Response, next: express.NextFunction) => { diff --git a/components/server/src/theia-plugin/theia-plugin-controller.ts b/components/server/src/theia-plugin/theia-plugin-controller.ts index 51a46c0e49349b..34e2ff5472c2bf 100644 --- a/components/server/src/theia-plugin/theia-plugin-controller.ts +++ b/components/server/src/theia-plugin/theia-plugin-controller.ts @@ -20,8 +20,6 @@ export class TheiaPluginController { @inject(TheiaPluginDB) protected readonly pluginDB: TheiaPluginDB; @inject(TheiaPluginService) protected readonly pluginService: TheiaPluginService; - protected readonly SERVER_PROXY_APIKEY = process.env.SERVER_PROXY_APIKEY; - get apiRouter(): express.Router { const router = express.Router(); this.addPreflightHandler(router); @@ -36,7 +34,7 @@ export class TheiaPluginController { */ router.get("/preflight", async (req, res, next) => { const token = req.query.token || req.headers["token"] || "unauthorized"; - if (this.SERVER_PROXY_APIKEY != token) { + if (this.env.serverProxyApiKey != token) { log.warn("Unauthorized attempted to access the /plugins/preflight endpoint", req); res.sendStatus(401); return; @@ -67,7 +65,7 @@ export class TheiaPluginController { router.get("/checkin", async (req, res, next) => { const token = req.query.token || req.headers["token"] || "unauthorized"; - if (this.SERVER_PROXY_APIKEY != token) { + if (this.env.serverProxyApiKey != token) { log.warn("Unauthorized attempted to access the /plugins/checkin endpoint", req); res.sendStatus(401); return; diff --git a/components/server/src/theia-plugin/theia-plugin-service.ts b/components/server/src/theia-plugin/theia-plugin-service.ts index 4f440c30bfe47a..c95d981d594bb7 100644 --- a/components/server/src/theia-plugin/theia-plugin-service.ts +++ b/components/server/src/theia-plugin/theia-plugin-service.ts @@ -9,7 +9,6 @@ import { injectable, inject } from 'inversify'; import { ResolvePluginsParams, ResolvedPlugins, TheiaPlugin, PreparePluginUploadParams, InstallPluginsParams, UninstallPluginParams, ResolvedPluginKind } from '@gitpod/gitpod-protocol'; import { TheiaPluginDB, UserStorageResourcesDB } from "@gitpod/gitpod-db/lib"; import { Env } from '../env'; -import { GitpodHostUrl } from '@gitpod/gitpod-protocol/lib/util/gitpod-host-url'; import { log } from '@gitpod/gitpod-protocol/lib/util/logging'; import { ResponseError } from 'vscode-jsonrpc'; import { ErrorCodes } from '@gitpod/gitpod-protocol/lib/messaging/error'; @@ -152,7 +151,7 @@ export class TheiaPluginService { } protected getPublicPluginURL(pluginEntryId: string) { - return new GitpodHostUrl(process.env.HOST_URL) + return this.env.hostUrl .with({ pathname: '/plugins', search: `id=${pluginEntryId}` diff --git a/components/server/src/websocket-connection-manager.ts b/components/server/src/websocket-connection-manager.ts index 993522f7672174..745c3f70d3b724 100644 --- a/components/server/src/websocket-connection-manager.ts +++ b/components/server/src/websocket-connection-manager.ts @@ -14,7 +14,7 @@ import * as express from "express"; import { ErrorCodes as RPCErrorCodes, MessageConnection, ResponseError } from "vscode-jsonrpc"; import { AllAccessFunctionGuard, FunctionAccessGuard, WithFunctionAccessGuard } from "./auth/function-access"; import { HostContextProvider } from "./auth/host-context-provider"; -import { RateLimiter, UserRateLimiter } from "./auth/rate-limiter"; +import { RateLimiter, RateLimiterConfig, UserRateLimiter } from "./auth/rate-limiter"; import { CompositeResourceAccessGuard, OwnerResourceGuard, ResourceAccessGuard, SharedWorkspaceAccessGuard, WithResourceAccessGuard, WorkspaceLogAccessGuard } from "./auth/resource-access"; import { increaseApiCallCounter, increaseApiConnectionClosedCounter, increaseApiConnectionCounter, increaseApiCallUserCounter } from "./prometheus-metrics"; import { GitpodServerImpl } from "./workspace/gitpod-server-impl"; @@ -36,7 +36,8 @@ export class WebsocketConnectionManager, - protected readonly hostContextProvider: HostContextProvider) { + protected readonly hostContextProvider: HostContextProvider, + protected readonly rateLimiterConfig: RateLimiterConfig) { this.jsonRpcConnectionHandler = new GitpodJsonRpcConnectionHandler( this.path, this.createProxyTarget.bind(this), @@ -104,7 +105,7 @@ export class WebsocketConnectionManager UserRateLimiter.instance().consume(id, method), + consume: (method) => UserRateLimiter.instance(this.rateLimiterConfig).consume(id, method), } }