diff --git a/packages/backend-app-api/config.d.ts b/packages/backend-app-api/config.d.ts index 3625e9323f..cbf65849be 100644 --- a/packages/backend-app-api/config.d.ts +++ b/packages/backend-app-api/config.d.ts @@ -34,21 +34,24 @@ export interface Config { * unless you configure credentials for service calls. */ dangerouslyDisableDefaultAuthPolicy?: boolean; - rateLimit?: - | false - | { - /** - * Limit each IP to max requests per window - */ - max?: number; - /** - * The duration for which the rate limit is enforced - */ - window?: HumanDuration; - - disable?: true; - }; }; + /** @visibility frontend */ + rateLimit?: + | false + | { + /** + * Limit each IP to max requests per window, defaults to 60 requests. + */ + max?: number; + /** + * The duration for which the rate limit is enforced, defaults to 1 minute. + */ + window?: HumanDuration; + /** + * Disable the rate limit verification. + */ + disable?: true; + }; }; /** Discovery options. */ diff --git a/packages/backend-app-api/src/services/implementations/httpRouter/createCredentialsBarrier.test.ts b/packages/backend-app-api/src/services/implementations/httpRouter/createCredentialsBarrier.test.ts index 94109ccfc0..5beeaa3a8e 100644 --- a/packages/backend-app-api/src/services/implementations/httpRouter/createCredentialsBarrier.test.ts +++ b/packages/backend-app-api/src/services/implementations/httpRouter/createCredentialsBarrier.test.ts @@ -188,8 +188,8 @@ it('do not limit authenticated requests', async () => { const max = 2; const configMock = { backend: { - auth: { - rateLimit: { + rateLimit: { + unauthorized: { max, // 2 requests per window window: { minutes: 2 }, // rate limit window expiration time, }, @@ -237,8 +237,8 @@ it('limit the number of unauthenticated requests', async () => { const configMock = { backend: { - auth: { - rateLimit: { + rateLimit: { + unauthorized: { max: 2, // 2 requests per window window: { minutes: 2 }, // rate limit window expiration time, }, @@ -279,25 +279,25 @@ it('limit the number of unauthenticated requests', async () => { expect(cacheMock.get).toHaveBeenCalledTimes(4); expect(cacheMock.set).toHaveBeenNthCalledWith( 1, - `rl_${randomIp}`, // rl is the default prefix for the rate limit store + `unauthorized_rate_limit_${randomIp}`, // unauthorized_rate_limit_ is the default prefix for the rate limit store { resetTime, totalHits: 1 }, { ttl: twoMinutesInMilliseconds }, ); expect(cacheMock.set).toHaveBeenNthCalledWith( 2, - `rl_${randomIp}`, + `unauthorized_rate_limit_${randomIp}`, { resetTime, totalHits: 2 }, { ttl: twoMinutesInMilliseconds }, ); expect(cacheMock.set).toHaveBeenNthCalledWith( 3, - `rl_${randomIp}`, + `unauthorized_rate_limit_${randomIp}`, { resetTime, totalHits: 3 }, { ttl: twoMinutesInMilliseconds }, ); expect(cacheMock.set).toHaveBeenNthCalledWith( 4, - `rl_${randomIp}`, + `unauthorized_rate_limit_${randomIp}`, { resetTime, totalHits: 1 }, { ttl: twoMinutesInMilliseconds }, ); @@ -320,8 +320,8 @@ it('skip limiting requests when the rate limit is disabled', async () => { cache: cacheMock, config: { backend: { - auth: { - rateLimit: { + rateLimit: { + unauthorized: { max, window: { minutes: 2 }, disabled: true, @@ -333,7 +333,6 @@ it('skip limiting requests when the rate limit is disabled', async () => { barrier1.addAuthPolicy({ allow: 'unauthenticated', path: '/public' }); - // exceed the rate limit for authenticated requests for (let i = 0; i < max + 1; i += 1) { await request(app1) .get('/public') @@ -349,8 +348,8 @@ it('skip limiting requests when the rate limit is disabled', async () => { cache: cacheMock, config: { backend: { - auth: { - rateLimit: false, + rateLimit: { + unauthorized: false, }, }, }, @@ -358,7 +357,6 @@ it('skip limiting requests when the rate limit is disabled', async () => { barrier2.addAuthPolicy({ allow: 'unauthenticated', path: '/public' }); - // exceed the rate limit for authenticated requests await request(app2).get('/public').send().expect(200); jest.useRealTimers(); diff --git a/packages/backend-app-api/src/services/implementations/httpRouter/createCredentialsBarrier.ts b/packages/backend-app-api/src/services/implementations/httpRouter/createCredentialsBarrier.ts index aca7838804..b47b926576 100644 --- a/packages/backend-app-api/src/services/implementations/httpRouter/createCredentialsBarrier.ts +++ b/packages/backend-app-api/src/services/implementations/httpRouter/createCredentialsBarrier.ts @@ -65,18 +65,19 @@ export function createCredentialsBarrier(options: { const unauthenticatedPredicates = new Array<(path: string) => boolean>(); const cookiePredicates = new Array<(path: string) => boolean>(); - const rateLimitConfig = config.getOptional('backend.auth.rateLimit'); + const rateLimitConfig = config.getOptional('backend.rateLimit.unauthorized'); const disabled = rateLimitConfig === false || (typeof rateLimitConfig === 'object' && - config?.getOptionalBoolean('backend.auth.rateLimit.disabled') === true); + config?.getOptionalBoolean('backend.rateLimit.unauthorized.disabled') === + true); const duration = typeof rateLimitConfig === 'object' && - config?.has('backend.auth.rateLimit.window') + config?.has('backend.rateLimit.unauthorized.window') ? readDurationFromConfig( - config.getConfig('backend.auth.rateLimit.window'), + config.getConfig('backend.rateLimit.unauthorized.window'), ) : undefined; @@ -84,8 +85,8 @@ export function createCredentialsBarrier(options: { const max = typeof rateLimitConfig === 'object' && - config?.has('backend.auth.rateLimit.max') - ? config.getNumber('backend.auth.rateLimit.max') + config?.has('backend.rateLimit.unauthorized.max') + ? config.getNumber('backend.rateLimit.unauthorized.max') : 60; // Default rate limit is 60 requests per 1 minute diff --git a/packages/backend-app-api/src/services/implementations/httpRouter/rateLimitStore.test.ts b/packages/backend-app-api/src/services/implementations/httpRouter/rateLimitStore.test.ts index 44035fc62e..22ab7c816d 100644 --- a/packages/backend-app-api/src/services/implementations/httpRouter/rateLimitStore.test.ts +++ b/packages/backend-app-api/src/services/implementations/httpRouter/rateLimitStore.test.ts @@ -18,13 +18,12 @@ import { ServiceMock, mockServices } from '@backstage/backend-test-utils'; import { RateLimitStore } from './rateLimitStore'; import { CacheService } from '@backstage/backend-plugin-api'; -jest.setSystemTime(Date.parse('2024-03-15T08:39:11.869Z')); - describe('RateLimitStore', () => { let cacheServiceMock: ServiceMock; let rateLimitStore: RateLimitStore; beforeEach(() => { + jest.useFakeTimers({ now: Date.parse('2024-03-15T08:39:11.869Z') }); jest.clearAllMocks(); cacheServiceMock = mockServices.cache.mock(); rateLimitStore = RateLimitStore.fromOptions({ @@ -33,6 +32,10 @@ describe('RateLimitStore', () => { }); }); + afterEach(() => { + jest.useRealTimers(); + }); + it('should initialize with default options', () => { expect(rateLimitStore.windowMs).toBe(1 * 60 * 1000); }); diff --git a/packages/backend-app-api/src/services/implementations/httpRouter/rateLimitStore.ts b/packages/backend-app-api/src/services/implementations/httpRouter/rateLimitStore.ts index 423a1012a9..6286522a2a 100644 --- a/packages/backend-app-api/src/services/implementations/httpRouter/rateLimitStore.ts +++ b/packages/backend-app-api/src/services/implementations/httpRouter/rateLimitStore.ts @@ -51,7 +51,7 @@ export class RateLimitStore implements Store { #cache: CacheService; private constructor(options: CacheStoreOptions) { - this.prefix = options.prefix ?? 'rl_'; + this.prefix = options.prefix ?? 'unauthorized_rate_limit_'; this.#cache = options.cache; }