refactor: add optional disabled config

Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
Camila Belo
2024-03-19 10:49:03 +01:00
parent d8c26d5e6d
commit 4e265180aa
3 changed files with 99 additions and 19 deletions
+14 -10
View File
@@ -34,16 +34,20 @@ export interface Config {
* unless you configure credentials for service calls.
*/
dangerouslyDisableDefaultAuthPolicy?: boolean;
rateLimit?: {
/**
* Limit each IP to max requests per window
*/
max?: number;
/**
* The duration for which the rate limit is enforced
*/
window?: HumanDuration;
};
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;
};
};
};
@@ -219,6 +219,8 @@ it('do not limit authenticated requests', async () => {
.send()
.expect(200);
}
jest.useRealTimers();
});
it('limit the number of unauthenticated requests', async () => {
@@ -302,3 +304,62 @@ it('limit the number of unauthenticated requests', async () => {
jest.useRealTimers();
});
it('skip limiting requests when the rate limit is disabled', async () => {
const now = Date.now();
jest.useFakeTimers({ now });
const max = 2;
const cacheMock = mockServices.cache.mock();
const twoMinutesInMilliseconds = 2 * 60 * 1000;
const resetTime = now + twoMinutesInMilliseconds;
// Simulate that the totalHits exceeds the configured max value
cacheMock.get.mockResolvedValue({ totalHits: max + 1, resetTime });
const { app: app1, barrier: barrier1 } = setup({
cache: cacheMock,
config: {
backend: {
auth: {
rateLimit: {
max,
window: { minutes: 2 },
disabled: true,
},
},
},
},
});
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')
.set('authorization', mockCredentials.user.header())
.send()
.expect(200);
}
// Simulate that the totalHits exceeds the default max value
cacheMock.get.mockResolvedValue({ totalHits: 61, resetTime });
const { app: app2, barrier: barrier2 } = setup({
cache: cacheMock,
config: {
backend: {
auth: {
rateLimit: false,
},
},
},
});
barrier2.addAuthPolicy({ allow: 'unauthenticated', path: '/public' });
// exceed the rate limit for authenticated requests
await request(app2).get('/public').send().expect(200);
jest.useRealTimers();
});
@@ -65,24 +65,39 @@ export function createCredentialsBarrier(options: {
const unauthenticatedPredicates = new Array<(path: string) => boolean>();
const cookiePredicates = new Array<(path: string) => boolean>();
// Default rate limit is 60 requests per 1 minute
const max = config?.has('backend.auth.rateLimit.max')
? config.getNumber('backend.auth.rateLimit.max')
: 60;
const rateLimitConfig = config.getOptional('backend.auth.rateLimit');
const duration = config?.has('backend.auth.rateLimit.window')
? readDurationFromConfig(config.getConfig('backend.auth.rateLimit.window'))
: undefined;
const disabled =
rateLimitConfig === false ||
(typeof rateLimitConfig === 'object' &&
config?.getOptionalBoolean('backend.auth.rateLimit.disabled') === true);
const duration =
typeof rateLimitConfig === 'object' &&
config?.has('backend.auth.rateLimit.window')
? readDurationFromConfig(
config.getConfig('backend.auth.rateLimit.window'),
)
: undefined;
// Default rate limit window is 1 minute
const windowMs = duration ? durationToMilliseconds(duration) : 1 * 60 * 1000;
const max =
typeof rateLimitConfig === 'object' &&
config?.has('backend.auth.rateLimit.max')
? config.getNumber('backend.auth.rateLimit.max')
: 60;
// Default rate limit is 60 requests per 1 minute
const limiter = rateLimit({
windowMs,
max,
limit: max,
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers,
store: RateLimitStore.fromOptions({ cache }),
skip() {
return disabled;
},
});
const middleware: RequestHandler = (req, res, next) => {