diff --git a/api/services/RightService.js b/api/services/RightService.js index ffbdf56b4..3e540ffe0 100644 --- a/api/services/RightService.js +++ b/api/services/RightService.js @@ -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', diff --git a/config/http.js b/config/http.js index 8e8f4116b..fe2cced68 100644 --- a/config/http.js +++ b/config/http.js @@ -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, /** ************************************************************************* * * @@ -39,9 +39,9 @@ module.exports.http = { order: [ 'parseAuthToken', - 'generalRateLimit', - 'userDeleteRateLimit', - 'moderatorDeleteRateLimit', + 'visitorRateLimit', + 'authenticatedRateLimit', + 'deleteRateLimit', 'responseTimeLogger', 'requestLogger', 'fileMiddleware', diff --git a/config/rateLimit/rateLimiter.js b/config/rateLimit/rateLimiter.js index 992e2efad..3af0ba4ee 100644 --- a/config/rateLimit/rateLimiter.js +++ b/config/rateLimit/rateLimiter.js @@ -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; }, }), }; diff --git a/sql/2_2021_09_22_2_rights.sql b/sql/2_2021_09_22_2_rights.sql index cf69bd98c..836ec829b 100644 --- a/sql/2_2021_09_22_2_rights.sql +++ b/sql/2_2021_09_22_2_rights.sql @@ -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), diff --git a/sql/2_2021_09_22_3_groups_rights.sql b/sql/2_2021_09_22_3_groups_rights.sql index 7ea77d11f..0bc35cb1e 100644 --- a/sql/2_2021_09_22_3_groups_rights.sql +++ b/sql/2_2021_09_22_3_groups_rights.sql @@ -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), @@ -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),