@@ -30,6 +30,13 @@ import { APIStatsService as StatsServiceAPI } from "./stats";
30
30
import { APITeamsService as TeamsServiceAPI } from "./teams" ;
31
31
import { APIUserService as UserServiceAPI } from "./user" ;
32
32
import { WorkspaceServiceAPI } from "./workspace-service-api" ;
33
+ import { IRateLimiterOptions , RateLimiterMemory , RateLimiterRedis , RateLimiterRes } from "rate-limiter-flexible" ;
34
+ import { Redis } from "ioredis" ;
35
+ import { RateLimited } from "./rate-limited" ;
36
+ import { Config } from "../config" ;
37
+ import { ApplicationError , ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error" ;
38
+ import { UserService } from "../user/user-service" ;
39
+ import { User } from "@gitpod/gitpod-protocol" ;
33
40
34
41
decorate ( injectable ( ) , PublicAPIConverter ) ;
35
42
@@ -46,6 +53,9 @@ export class API {
46
53
@inject ( HelloServiceAPI ) private readonly helloServiceApi : HelloServiceAPI ;
47
54
@inject ( SessionHandler ) private readonly sessionHandler : SessionHandler ;
48
55
@inject ( PublicAPIConverter ) private readonly apiConverter : PublicAPIConverter ;
56
+ @inject ( Redis ) private readonly redis : Redis ;
57
+ @inject ( Config ) private readonly config : Config ;
58
+ @inject ( UserService ) private readonly userService : UserService ;
49
59
50
60
listenPrivate ( ) : http . Server {
51
61
const app = express ( ) ;
@@ -174,9 +184,30 @@ export class API {
174
184
175
185
const context = args [ 1 ] as HandlerContext ;
176
186
187
+ const rateLimit = async ( subjectId : string ) => {
188
+ const key = `${ grpc_service } /${ grpc_method } ` ;
189
+ const options = self . config . rateLimits ?. [ key ] || RateLimited . getOptions ( target , prop ) ;
190
+ try {
191
+ await self . getRateLimitter ( options ) . consume ( `${ subjectId } _${ key } ` ) ;
192
+ } catch ( e ) {
193
+ if ( e instanceof RateLimiterRes ) {
194
+ throw new ConnectError ( "rate limit exceeded" , Code . ResourceExhausted , {
195
+ // http compatibility, can be respected by gRPC clients as well
196
+ // instead of doing an ad-hoc retry, the client can wait for the given amount of seconds
197
+ "Retry-After" : e . msBeforeNext / 1000 ,
198
+ "X-RateLimit-Limit" : options . points ,
199
+ "X-RateLimit-Remaining" : e . remainingPoints ,
200
+ "X-RateLimit-Reset" : new Date ( Date . now ( ) + e . msBeforeNext ) ,
201
+ } ) ;
202
+ }
203
+ throw e ;
204
+ }
205
+ } ;
206
+
177
207
const apply = async < T > ( ) : Promise < T > => {
178
- const user = await self . verify ( context ) ;
179
- context . user = user ;
208
+ const subjectId = await self . verify ( context ) ;
209
+ await rateLimit ( subjectId ) ;
210
+ context . user = await self . ensureFgaMigration ( subjectId ) ;
180
211
181
212
return Reflect . apply ( target [ prop as any ] , target , args ) ;
182
213
} ;
@@ -211,16 +242,49 @@ export class API {
211
242
} ;
212
243
}
213
244
214
- private async verify ( context : HandlerContext ) {
215
- const user = await this . sessionHandler . verify ( context . requestHeader . get ( "cookie" ) || "" ) ;
216
- if ( ! user ) {
245
+ private async verify ( context : HandlerContext ) : Promise < string > {
246
+ const claims = await this . sessionHandler . verifyJWTCookie ( context . requestHeader . get ( "cookie" ) || "" ) ;
247
+ const subjectId = claims ?. sub ;
248
+ if ( ! subjectId ) {
217
249
throw new ConnectError ( "unauthenticated" , Code . Unauthenticated ) ;
218
250
}
219
- const fgaChecksEnabled = await isFgaChecksEnabled ( user . id ) ;
251
+ return subjectId ;
252
+ }
253
+
254
+ private async ensureFgaMigration ( userId : string ) : Promise < User > {
255
+ const fgaChecksEnabled = await isFgaChecksEnabled ( userId ) ;
220
256
if ( ! fgaChecksEnabled ) {
221
257
throw new ConnectError ( "unauthorized" , Code . PermissionDenied ) ;
222
258
}
223
- return user ;
259
+ try {
260
+ return await this . userService . findUserById ( userId , userId ) ;
261
+ } catch ( e ) {
262
+ if ( e instanceof ApplicationError && e . code === ErrorCodes . NOT_FOUND ) {
263
+ throw new ConnectError ( "unauthorized" , Code . PermissionDenied ) ;
264
+ }
265
+ throw e ;
266
+ }
267
+ }
268
+
269
+ private readonly rateLimiters = new Map < string , RateLimiterRedis > ( ) ;
270
+ private getRateLimitter ( options : IRateLimiterOptions ) : RateLimiterRedis {
271
+ const sortedKeys = Object . keys ( options ) . sort ( ) ;
272
+ const sortedObject : { [ key : string ] : any } = { } ;
273
+ for ( const key of sortedKeys ) {
274
+ sortedObject [ key ] = options [ key as keyof IRateLimiterOptions ] ;
275
+ }
276
+ const key = JSON . stringify ( sortedObject ) ;
277
+
278
+ let rateLimiter = this . rateLimiters . get ( key ) ;
279
+ if ( ! rateLimiter ) {
280
+ rateLimiter = new RateLimiterRedis ( {
281
+ storeClient : this . redis ,
282
+ ...options ,
283
+ insuranceLimiter : new RateLimiterMemory ( options ) ,
284
+ } ) ;
285
+ this . rateLimiters . set ( key , rateLimiter ) ;
286
+ }
287
+ return rateLimiter ;
224
288
}
225
289
226
290
static bindAPI ( bind : interfaces . Bind ) : void {
0 commit comments