From 7be616e5831678b42deb7de98c974369f7bf8967 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Mon, 3 Jun 2024 11:20:51 +0200 Subject: [PATCH] feat(core): Allow customizing rate limits on a per-route basis, and add rate limiting to more endpoints (#9522) Co-authored-by: Omar Ajoue --- packages/cli/src/controllers/auth.controller.ts | 2 +- .../cli/src/controllers/invitation.controller.ts | 2 +- .../src/controllers/passwordReset.controller.ts | 2 +- packages/cli/src/decorators/Route.ts | 8 ++++---- .../cli/src/decorators/registerController.ts | 16 +++++++++------- packages/cli/src/decorators/types.ts | 15 ++++++++++++++- .../unit/decorators/registerController.test.ts | 5 ++--- 7 files changed, 32 insertions(+), 18 deletions(-) diff --git a/packages/cli/src/controllers/auth.controller.ts b/packages/cli/src/controllers/auth.controller.ts index 39285b797..6ba144903 100644 --- a/packages/cli/src/controllers/auth.controller.ts +++ b/packages/cli/src/controllers/auth.controller.ts @@ -39,7 +39,7 @@ export class AuthController { ) {} /** Log in a user */ - @Post('/login', { skipAuth: true, rateLimit: true }) + @Post('/login', { skipAuth: true, rateLimit: {} }) async login(req: LoginRequest, res: Response): Promise { const { email, password, mfaToken, mfaRecoveryCode } = req.body; if (!email) throw new ApplicationError('Email is required to log in'); diff --git a/packages/cli/src/controllers/invitation.controller.ts b/packages/cli/src/controllers/invitation.controller.ts index bb5f006a5..5e75db025 100644 --- a/packages/cli/src/controllers/invitation.controller.ts +++ b/packages/cli/src/controllers/invitation.controller.ts @@ -37,7 +37,7 @@ export class InvitationController { * Send email invite(s) to one or multiple users and create user shell(s). */ - @Post('/') + @Post('/', { rateLimit: { limit: 10 } }) @GlobalScope('user:create') async inviteUser(req: UserRequest.Invite) { const isWithinUsersLimit = this.license.isWithinUsersLimit(); diff --git a/packages/cli/src/controllers/passwordReset.controller.ts b/packages/cli/src/controllers/passwordReset.controller.ts index fdf9e4913..03d19b4de 100644 --- a/packages/cli/src/controllers/passwordReset.controller.ts +++ b/packages/cli/src/controllers/passwordReset.controller.ts @@ -41,7 +41,7 @@ export class PasswordResetController { /** * Send a password reset email. */ - @Post('/forgot-password', { skipAuth: true, rateLimit: true }) + @Post('/forgot-password', { skipAuth: true, rateLimit: { limit: 3 } }) async forgotPassword(req: PasswordResetRequest.Email) { if (!this.mailer.isEmailSetUp) { this.logger.debug( diff --git a/packages/cli/src/decorators/Route.ts b/packages/cli/src/decorators/Route.ts index dee793504..bdaab78c7 100644 --- a/packages/cli/src/decorators/Route.ts +++ b/packages/cli/src/decorators/Route.ts @@ -1,14 +1,14 @@ import type { RequestHandler } from 'express'; import { CONTROLLER_ROUTES } from './constants'; -import type { Method, RouteMetadata } from './types'; +import type { Method, RateLimit, RouteMetadata } from './types'; interface RouteOptions { middlewares?: RequestHandler[]; usesTemplates?: boolean; /** When this flag is set to true, auth cookie isn't validated, and req.user will not be set */ skipAuth?: boolean; - /** When this flag is set to true, calls to this endpoint is rate limited to a max of 5 over a window of 5 minutes **/ - rateLimit?: boolean; + /** When these options are set, calls to this endpoint are rate limited using the options */ + rateLimit?: RateLimit; } const RouteFactory = @@ -25,7 +25,7 @@ const RouteFactory = handlerName: String(handlerName), usesTemplates: options.usesTemplates ?? false, skipAuth: options.skipAuth ?? false, - rateLimit: options.rateLimit ?? false, + rateLimit: options.rateLimit, }); Reflect.defineMetadata(CONTROLLER_ROUTES, routes, controllerClass); }; diff --git a/packages/cli/src/decorators/registerController.ts b/packages/cli/src/decorators/registerController.ts index 3cc0d35d9..f3ed79154 100644 --- a/packages/cli/src/decorators/registerController.ts +++ b/packages/cli/src/decorators/registerController.ts @@ -8,7 +8,7 @@ import type { Class } from 'n8n-core'; import { AuthService } from '@/auth/auth.service'; import config from '@/config'; import { UnauthenticatedError } from '@/errors/response-errors/unauthenticated.error'; -import { inE2ETests, inTest, RESPONSE_ERROR_MESSAGES } from '@/constants'; +import { inProduction, RESPONSE_ERROR_MESSAGES } from '@/constants'; import type { BooleanLicenseFeature } from '@/Interfaces'; import { License } from '@/License'; import type { AuthenticatedRequest } from '@/requests'; @@ -24,16 +24,18 @@ import type { Controller, LicenseMetadata, MiddlewareMetadata, + RateLimit, RouteMetadata, RouteScopeMetadata, } from './types'; import { userHasScope } from '@/permissions/checkAccess'; -const throttle = expressRateLimit({ - windowMs: 5 * 60 * 1000, // 5 minutes - limit: 5, // Limit each IP to 5 requests per `window` (here, per 5 minutes). - message: { message: 'Too many requests' }, -}); +const createRateLimitMiddleware = (rateLimit: RateLimit): RequestHandler => + expressRateLimit({ + windowMs: rateLimit.windowMs, + limit: rateLimit.limit, + message: { message: 'Too many requests' }, + }); export const createLicenseMiddleware = (features: BooleanLicenseFeature[]): RequestHandler => @@ -124,7 +126,7 @@ export const registerController = (app: Application, controllerClass: Class ({ - inE2ETests: false, - inTest: false, + inProduction: true, })); import express from 'express'; @@ -14,7 +13,7 @@ describe('registerController', () => { @RestController('/test') class TestController { @Get('/unlimited', { skipAuth: true }) - @Get('/rate-limited', { skipAuth: true, rateLimit: true }) + @Get('/rate-limited', { skipAuth: true, rateLimit: {} }) endpoint() { return { ok: true }; }