Skip to content

Commit

Permalink
fix(#1157): update request rate limiting
Browse files Browse the repository at this point in the history
  • Loading branch information
Clm-Roig committed Jul 20, 2023
1 parent e1ad2b0 commit 2e601d5
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 111 deletions.
3 changes: 0 additions & 3 deletions api/services/RightService.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,6 @@ module.exports = {
MARK_AS_SENSITIVE: 'mark as sensitive',
MERGE: 'merge',
MERGE_DUPLICATES: 'merge duplicates',
NO_REQUEST_LIMIT: 'no request limit',
NO_MODERATOR_DELETE_REQUEST_LIMIT: 'no moderator delete request limit',
NO_USER_DELETE_REQUEST_LIMIT: 'no user delete request limit',
SUBSCRIBE: 'subscribe',
UNLINK_RESOURCE: 'unlink resource',
UNMARK_AS_SENSITIVE: 'unmark as sensitive',
Expand Down
12 changes: 6 additions & 6 deletions config/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ module.exports.http = {

middleware: {
// Requests limiter configuration
generalRateLimit: rateLimiter.generalRateLimit,
userDeleteRateLimit: rateLimiter.userDeleteRateLimit,
moderatorDeleteRateLimit: rateLimiter.moderatorDeleteRateLimit,
visitorRateLimit: rateLimiter.visitorRateLimit,
authenticatedRateLimit: rateLimiter.authenticatedRateLimit,
deleteRateLimit: rateLimiter.deleteRateLimit,

/** *************************************************************************
* *
Expand All @@ -39,9 +39,9 @@ module.exports.http = {

order: [
'parseAuthToken',
'generalRateLimit',
'userDeleteRateLimit',
'moderatorDeleteRateLimit',
'visitorRateLimit',
'authenticatedRateLimit',
'deleteRateLimit',
'responseTimeLogger',
'requestLogger',
'fileMiddleware',
Expand Down
181 changes: 88 additions & 93 deletions config/rateLimit/rateLimiter.js
Original file line number Diff line number Diff line change
@@ -1,141 +1,136 @@
const rateLimit = require('express-rate-limit');
const RightService = require('../../api/services/RightService');

const RATE_LIMIT_WINDOW = 600000; // 10 minutes
const DELETE_RATE_LIMIT_WINDOW = 3600000; // 1 hour

const VISITOR_RATE_LIMIT_PER_WINDOW = 200;
const USER_RATE_LIMIT_PER_WINDOW = 400;
const USER_DELETE_RATE_LIMIT_PER_WINDOW = 1;
const DELETE_RATE_LIMIT_PER_WINDOW = 20;
const LEADER_RATE_LIMIT_PER_WINDOW = 1000;
const MODERATOR_RATE_LIMIT_PER_WINDOW = 10000;

const ADMIN_GROUP_NAME = 'Administrator';
const LEADER_GROUP_NAME = 'Leader';
const MODERATOR_GROUP_NAME = 'Moderator';

const isAdmin = (token) =>
token.groups.some((g) => g.name === ADMIN_GROUP_NAME);
const isLeader = (token) =>
token.groups.some((g) => g.name === LEADER_GROUP_NAME);
const isModerator = (token) =>
token.groups.some((g) => g.name === MODERATOR_GROUP_NAME);

// If an user is from group X, it is also from other "less important" groups.
// Admin > Moderator > Leader > User
// So, to check only with isUser() is not enough to know the "real" group of an user
const isMoreThanUser = (token) =>
isLeader(token) || isModerator(token) || isAdmin(token);
const isMoreThanLeader = (token) => isModerator(token) || isAdmin(token);
const isMoreThanModerator = (token) => isAdmin(token);

const isFromAnotherAppAndNotInTest = (req) =>
req.headers.origin !== sails.config.custom.baseUrl &&
process.env.NODE_ENV !== 'test';

module.exports = {
generalRateLimit: rateLimit({
windowMs: process.env.RATE_LIMIT_WINDOW
? process.env.RATE_LIMIT_WINDOWS
: 1 * 30 * 1000, // 30 seconds
max: process.env.RATE_LIMIT_PER_WINDOW
? process.env.RATE_LIMIT_PER_WINDOW
: 100, // limit each IP to 100 requests per windowMs
visitorRateLimit: rateLimit({
windowMs: RATE_LIMIT_WINDOW,
max: VISITOR_RATE_LIMIT_PER_WINDOW,
message: 'Too many requests with the same IP, try again later.',
standardHeaders: true,
statusCode: 429,
skip: async (req, res) => {
// Ignore OPTIONS request
if (req.method.toUpperCase() === 'OPTIONS') {
return true;
}

// Currently, ignore limiting when in test
if (process.env.NODE_ENV === 'test') {
skip: async (req /* , res */) => {
/* Ignore:
- OPTIONS requests
- login route
- api in test env
*/
if (
req.method.toUpperCase() === 'OPTIONS' ||
req.originalUrl.includes('login') ||
process.env.NODE_ENV === 'test'
) {
return true;
}

// If you are not authenticated, you are limited
if (!req.token) {
return false;
}
const hasNoRequestLimit = await sails.helpers.checkRight
.with({
groups: req.token.groups,
rightEntity: RightService.RightEntities.APPLICATION,
rightAction: RightService.RightActions.NO_REQUEST_LIMIT,
})
.intercept('rightNotFound', () =>
res.serverError(
'A server error occured when checking your right to not having a request limit.'
)
);

return hasNoRequestLimit;
return !!req.token;
},
}),

userDeleteRateLimit: rateLimit({
windowMs: process.env.USER_DELETE_RATE_LIMIT_WINDOWS
? process.env.USER_DELETE_RATE_LIMIT_WINDOWS
: 12 * 60 * 60 * 1000, // 12h
max: process.env.USER_DELETE_RATE_LIMIT_PER_WINDOW
? process.env.USER_DELETE_RATE_LIMIT_PER_WINDOW
: 1, // limit each IP to 1 request per windowMs
message:
'Too many DELETE requests with the same IP as an user, try again later.',
authenticatedRateLimit: rateLimit({
windowMs: RATE_LIMIT_WINDOW,
max: (req) => {
if (!isMoreThanUser(req.token)) {
return USER_RATE_LIMIT_PER_WINDOW;
}
if (!isMoreThanLeader(req.token)) {
return LEADER_RATE_LIMIT_PER_WINDOW;
}
if (!isMoreThanModerator(req.token)) {
return MODERATOR_RATE_LIMIT_PER_WINDOW;
}
return 0;
},
message: 'Too many requests with the same IP as an user, try again later.',
standardHeaders: true,
statusCode: 429,
skip: async (req, res) => {
// Ignore request others than DELETE
if (req.method.toUpperCase() !== 'DELETE') {
skip: async (req /* , res */) => {
// Ignore DELETE requests (see userDeleteRateLimit below)
if (req.method.toUpperCase() === 'DELETE') {
return true;
}

// If you are not authenticated, you are limited
if (!req.token) {
return false;
}
// If the request doesn't come from our main client and the app is not in test phase,
// you are limited
if (
req.headers.origin !== sails.config.custom.baseUrl &&
process.env.NODE_ENV !== 'test'
) {

if (isFromAnotherAppAndNotInTest(req)) {
sails.log.error(
`User ${req.token.nickname} (id=${req.token.id}) is being limited because the request doesn't come from our main client app.`
);
return false;
}

const hasNoRequestLimit = await sails.helpers.checkRight
.with({
groups: req.token.groups,
rightEntity: RightService.RightEntities.APPLICATION,
rightAction: RightService.RightActions.NO_USER_DELETE_REQUEST_LIMIT,
})
.intercept('rightNotFound', () =>
res.serverError(
'A server error occured when checking your right to not having a request limit on DELETE actions.'
)
);
if (!hasNoRequestLimit) {
sails.log.error(
`User ${req.token.nickname} (id=${req.token.id}) is being limited on DELETE requests as an user.`
);
}

return hasNoRequestLimit;
return false;
},
}),

moderatorDeleteRateLimit: rateLimit({
windowMs: process.env.MODERATOR_DELETE_RATE_LIMIT_WINDOWS
? process.env.MODERATOR_DELETE_RATE_LIMIT_WINDOWS
: 1 * 60 * 60 * 1000, // 1h
max: process.env.MODERATOR_DELETE_RATE_LIMIT_PER_WINDOW
? process.env.MODERATOR_DELETE_RATE_LIMIT_PER_WINDOW
: 20, // limit each IP to request per windowMs
deleteRateLimit: rateLimit({
windowMs: DELETE_RATE_LIMIT_WINDOW,
max: (req) => {
if (!isMoreThanUser(req.token)) {
return USER_DELETE_RATE_LIMIT_PER_WINDOW;
}
if (isAdmin(req.token)) {
return 0; // no limiting for admins
}
return DELETE_RATE_LIMIT_PER_WINDOW;
},
message:
'Too many DELETE requests with the same IP as a moderator, try again later.',
'Too many DELETE requests with the same IP as an user, try again later.',
standardHeaders: true,
statusCode: 429,
skip: async (req, res) => {
skip: async (req /* , res */) => {
// Ignore request others than DELETE
if (req.method.toUpperCase() !== 'DELETE') {
return true;
}
// If you are not authenticated, you are limited
if (!req.token) {
return false;
}

const hasNoRequestLimit = await sails.helpers.checkRight
.with({
groups: req.token.groups,
rightEntity: RightService.RightEntities.APPLICATION,
rightAction:
RightService.RightActions.NO_MODERATOR_DELETE_REQUEST_LIMIT,
})
.intercept('rightNotFound', () =>
res.serverError(
'A server error occured when checking your right to not having a request limit on DELETE actions.'
)
);

if (!hasNoRequestLimit) {
if (isFromAnotherAppAndNotInTest(req)) {
sails.log.error(
`User ${req.token.nickname} (id=${req.token.id}) is being limited on DELETE requests as an user.`
`User ${req.token.nickname} (id=${req.token.id}) is being limited because the request doesn't come from our main client app.`
);
return false;
}

return hasNoRequestLimit;
return true;
},
}),
};
3 changes: 0 additions & 3 deletions sql/2_2021_09_22_2_rights.sql
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,10 @@ INSERT INTO public.t_right (id,name,"comments") VALUES
(98,'Location - edit any',NULL),
(100,'Description - create',NULL),
(101,'Location - create',NULL),
(103,'Application - no request limit',NULL),
(104,'Document - edit not validated',NULL),
(105,'Document - csv import',NULL),
(106,'Entrance - csv import',NULL),
(107,'Cave - merge',NULL),
(108,'Application - no moderator delete request limit',NULL),
(109,'Application - no user delete request limit',NULL),
(110,'Document Duplicate - create',NULL),
(111,'Entrance Duplicate - create',NULL),
(112,'Document Duplicate - delete any',NULL),
Expand Down
6 changes: 0 additions & 6 deletions sql/2_2021_09_22_3_groups_rights.sql
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,6 @@ INSERT INTO public.j_group_right (id_group,id_right) VALUES
(3,23),
(3,101),
(3,96),
(1,103),
(2,103),
(3,103),
(3,34),
(3,35),
(2,104),
Expand All @@ -56,15 +53,12 @@ INSERT INTO public.j_group_right (id_group,id_right) VALUES
(3,51),
(2,106),
(3,107),
(1,108),
(2,110),
(4,4),
(4,10),
(2,111),
(2,112),
(2,113),
(1,109),
(2,109),
(3,114),
(1,115),
(3,116),
Expand Down

0 comments on commit 2e601d5

Please sign in to comment.