From d7bf37645686b606e01269cd5aecbfa900077dd2 Mon Sep 17 00:00:00 2001 From: Aadesh-Baral Date: Sun, 14 Aug 2022 19:23:17 +0545 Subject: [PATCH] Apply rate limit to high-resource endpoints --- backend/__init__.py | 6 ++++- backend/api/campaigns/resources.py | 6 +++++ backend/api/projects/actions.py | 11 +++++++++ backend/api/projects/resources.py | 7 ++++++ backend/api/projects/statistics.py | 7 ++++++ backend/api/system/authentication.py | 12 +++++++++- backend/api/system/general.py | 6 +++++ backend/api/system/image_upload.py | 6 +++++ backend/api/tasks/actions.py | 36 ++++++++++++++++++++++++++++ backend/api/teams/actions.py | 6 +++++ backend/api/users/actions.py | 6 +++++ backend/api/users/statistics.py | 6 +++++ backend/api/users/tasks.py | 6 +++++ backend/config.py | 5 ++++ requirements.txt | 1 + 15 files changed, 125 insertions(+), 2 deletions(-) diff --git a/backend/__init__.py b/backend/__init__.py index 9bfb659508..7a96e453be 100644 --- a/backend/__init__.py +++ b/backend/__init__.py @@ -2,13 +2,15 @@ import os from logging.handlers import RotatingFileHandler -from flask import Flask, redirect +from flask import Flask, redirect, make_response, jsonify from flask_cors import CORS from flask_migrate import Migrate from flask_oauthlib.client import OAuth from flask_restful import Api from flask_sqlalchemy import SQLAlchemy from flask_mail import Mail +from flask_limiter import Limiter +from flask_limiter.util import get_remote_address from backend.config import EnvironmentConfig @@ -35,6 +37,7 @@ def format_url(endpoint): migrate = Migrate() mail = Mail() oauth = OAuth() +limiter = Limiter(key_func=get_remote_address) osm = oauth.remote_app("osm", app_key="OSM_OAUTH_SETTINGS") @@ -64,6 +67,7 @@ def create_app(env="backend.config.EnvironmentConfig"): db.init_app(app) migrate.init_app(app, db) mail.init_app(app) + limiter.init_app(app) app.logger.debug("Add root redirect route") diff --git a/backend/api/campaigns/resources.py b/backend/api/campaigns/resources.py index 47bd9af1a9..127fb3165c 100644 --- a/backend/api/campaigns/resources.py +++ b/backend/api/campaigns/resources.py @@ -1,6 +1,7 @@ from flask_restful import Resource, request, current_app from schematics.exceptions import DataError +from backend import limiter, EnvironmentConfig from backend.models.dtos.campaign_dto import CampaignDTO, NewCampaignDTO from backend.services.campaign_service import CampaignService from backend.services.organisation_service import OrganisationService @@ -212,6 +213,11 @@ def delete(self, campaign_id): class CampaignsAllAPI(Resource): + + decorators = [ + limiter.limit(EnvironmentConfig.DEFAULT_RATE_LIMIT_THRESHOLD, methods=["POST"]) + ] + def get(self): """ Get all active campaigns diff --git a/backend/api/projects/actions.py b/backend/api/projects/actions.py index f43152b22f..2ea191cd6c 100644 --- a/backend/api/projects/actions.py +++ b/backend/api/projects/actions.py @@ -3,6 +3,7 @@ from flask_restful import Resource, request, current_app from schematics.exceptions import DataError +from backend import limiter, EnvironmentConfig from backend.models.dtos.message_dto import MessageDTO from backend.models.dtos.grid_dto import GridDTO from backend.services.project_service import ProjectService, NotFound @@ -78,6 +79,11 @@ def post(self, project_id): class ProjectsActionsMessageContributorsAPI(Resource): + + decorators = [ + limiter.limit(EnvironmentConfig.DEFAULT_RATE_LIMIT_THRESHOLD, methods=["POST"]) + ] + @token_auth.login_required def post(self, project_id): """ @@ -355,6 +361,11 @@ def post(self, project_id): class ProjectActionsIntersectingTilesAPI(Resource): + + decorators = [ + limiter.limit(EnvironmentConfig.DEFAULT_RATE_LIMIT_THRESHOLD, methods=["POST"]) + ] + @tm.pm_only() @token_auth.login_required def post(self): diff --git a/backend/api/projects/resources.py b/backend/api/projects/resources.py index 0dbcb2e807..d649a3a4c7 100644 --- a/backend/api/projects/resources.py +++ b/backend/api/projects/resources.py @@ -4,6 +4,8 @@ from flask_restful import Resource, current_app, request from schematics.exceptions import DataError from distutils.util import strtobool + +from backend import limiter, EnvironmentConfig from backend.models.dtos.project_dto import ( DraftProjectDTO, ProjectDTO, @@ -32,6 +34,11 @@ class ProjectsRestAPI(Resource): + + decorators = [ + limiter.limit(EnvironmentConfig.DEFAULT_RATE_LIMIT_THRESHOLD, methods=["POST"]) + ] + @token_auth.login_required(optional=True) def get(self, project_id): """ diff --git a/backend/api/projects/statistics.py b/backend/api/projects/statistics.py index 566291d050..05e4682eee 100644 --- a/backend/api/projects/statistics.py +++ b/backend/api/projects/statistics.py @@ -1,4 +1,6 @@ from flask_restful import Resource, current_app + +from backend import limiter, EnvironmentConfig from backend.services.stats_service import NotFound, StatsService from backend.services.project_service import ProjectService @@ -28,6 +30,11 @@ def get(self): class ProjectsStatisticsAPI(Resource): + + decorators = [ + limiter.limit(EnvironmentConfig.DEFAULT_RATE_LIMIT_THRESHOLD, methods=["GET"]) + ] + def get(self, project_id): """ Get Project Stats diff --git a/backend/api/system/authentication.py b/backend/api/system/authentication.py index 6d01f22aef..0907ff59ba 100644 --- a/backend/api/system/authentication.py +++ b/backend/api/system/authentication.py @@ -1,7 +1,7 @@ from flask import session, current_app, redirect, request from flask_restful import Resource -from backend import osm +from backend import osm, limiter, EnvironmentConfig from backend.services.users.authentication_service import ( AuthenticationService, AuthServiceError, @@ -17,6 +17,11 @@ def get_oauth_token(): class SystemAuthenticationLoginAPI(Resource): + + decorators = [ + limiter.limit(EnvironmentConfig.DEFAULT_RATE_LIMIT_THRESHOLD, methods=["GET"]) + ] + def get(self): """ Redirects user to OSM to authenticate @@ -44,6 +49,11 @@ def get(self): class SystemAuthenticationCallbackAPI(Resource): + + decorators = [ + limiter.limit(EnvironmentConfig.DEFAULT_RATE_LIMIT_THRESHOLD, methods=["GET"]) + ] + def get(self): """ Handles the OSM OAuth callback diff --git a/backend/api/system/general.py b/backend/api/system/general.py index f7c32e3db2..acd9727468 100644 --- a/backend/api/system/general.py +++ b/backend/api/system/general.py @@ -2,6 +2,7 @@ from flask_restful import Resource, request, current_app from flask_swagger import swagger +from backend import limiter, EnvironmentConfig from backend.services.settings_service import SettingsService from backend.services.messaging.smtp_service import SMTPService @@ -184,6 +185,11 @@ def get(self): class SystemContactAdminRestAPI(Resource): + + decorators = [ + limiter.limit(EnvironmentConfig.DEFAULT_RATE_LIMIT_THRESHOLD, methods=["POST"]) + ] + def post(self): """ Send an email to the system admin diff --git a/backend/api/system/image_upload.py b/backend/api/system/image_upload.py index 8aa7dbb808..fa985c5f6a 100644 --- a/backend/api/system/image_upload.py +++ b/backend/api/system/image_upload.py @@ -3,10 +3,16 @@ from flask_restful import Resource, request, current_app +from backend import limiter, EnvironmentConfig from backend.services.users.authentication_service import token_auth class SystemImageUploadRestAPI(Resource): + + decorators = [ + limiter.limit(EnvironmentConfig.DEFAULT_RATE_LIMIT_THRESHOLD, methods=["POST"]) + ] + @token_auth.login_required def post(self): """ diff --git a/backend/api/tasks/actions.py b/backend/api/tasks/actions.py index dbd142dee8..bf0cea5755 100644 --- a/backend/api/tasks/actions.py +++ b/backend/api/tasks/actions.py @@ -1,6 +1,7 @@ from flask_restful import Resource, current_app, request from schematics.exceptions import DataError +from backend import limiter, EnvironmentConfig from backend.models.dtos.grid_dto import SplitTaskDTO from backend.models.postgis.utils import NotFound, InvalidGeoJson from backend.services.grid.split_service import SplitService, SplitServiceError @@ -196,6 +197,11 @@ def post(self, project_id, task_id): class TasksActionsMappingUnlockAPI(Resource): + + decorators = [ + limiter.limit(EnvironmentConfig.DEFAULT_RATE_LIMIT_THRESHOLD, methods=["POST"]) + ] + @token_auth.login_required def post(self, project_id, task_id): """ @@ -519,6 +525,11 @@ def post(self, project_id): class TasksActionsValidationUnlockAPI(Resource): + + decorators = [ + limiter.limit(EnvironmentConfig.DEFAULT_RATE_LIMIT_THRESHOLD, methods=["POST"]) + ] + @token_auth.login_required def post(self, project_id): """ @@ -599,6 +610,11 @@ def post(self, project_id): class TasksActionsMapAllAPI(Resource): + + decorators = [ + limiter.limit(EnvironmentConfig.DEFAULT_RATE_LIMIT_THRESHOLD, methods=["POST"]) + ] + @token_auth.login_required def post(self, project_id): """ @@ -656,6 +672,11 @@ def post(self, project_id): class TasksActionsValidateAllAPI(Resource): + + decorators = [ + limiter.limit(EnvironmentConfig.DEFAULT_RATE_LIMIT_THRESHOLD, methods=["POST"]) + ] + @token_auth.login_required def post(self, project_id): """ @@ -713,6 +734,11 @@ def post(self, project_id): class TasksActionsInvalidateAllAPI(Resource): + + decorators = [ + limiter.limit(EnvironmentConfig.DEFAULT_RATE_LIMIT_THRESHOLD, methods=["POST"]) + ] + @token_auth.login_required def post(self, project_id): """ @@ -770,6 +796,11 @@ def post(self, project_id): class TasksActionsResetBadImageryAllAPI(Resource): + + decorators = [ + limiter.limit(EnvironmentConfig.DEFAULT_RATE_LIMIT_THRESHOLD, methods=["POST"]) + ] + @token_auth.login_required def post(self, project_id): """ @@ -829,6 +860,11 @@ def post(self, project_id): class TasksActionsResetAllAPI(Resource): + + decorators = [ + limiter.limit(EnvironmentConfig.DEFAULT_RATE_LIMIT_THRESHOLD, methods=["POST"]) + ] + @token_auth.login_required def post(self, project_id): """ diff --git a/backend/api/teams/actions.py b/backend/api/teams/actions.py index f193d9d4f6..844d9bcf67 100644 --- a/backend/api/teams/actions.py +++ b/backend/api/teams/actions.py @@ -2,6 +2,7 @@ from schematics.exceptions import DataError import threading +from backend import limiter, EnvironmentConfig from backend.models.dtos.message_dto import MessageDTO from backend.services.team_service import TeamService, NotFound, TeamJoinNotAllowed from backend.services.users.authentication_service import token_auth, tm @@ -252,6 +253,11 @@ def post(self, team_id): class TeamsActionsMessageMembersAPI(Resource): + + decorators = [ + limiter.limit(EnvironmentConfig.DEFAULT_RATE_LIMIT_THRESHOLD, methods=["POST"]) + ] + @token_auth.login_required def post(self, team_id): """ diff --git a/backend/api/users/actions.py b/backend/api/users/actions.py index 45a108d68a..1fee031bbe 100644 --- a/backend/api/users/actions.py +++ b/backend/api/users/actions.py @@ -1,6 +1,7 @@ from flask_restful import Resource, current_app, request from schematics.exceptions import DataError +from backend import limiter, EnvironmentConfig from backend.models.dtos.user_dto import UserDTO, UserRegisterEmailDTO from backend.services.messaging.message_service import MessageService from backend.services.users.authentication_service import token_auth, tm @@ -314,6 +315,11 @@ def patch(self): class UsersActionsRegisterEmailAPI(Resource): + + decorators = [ + limiter.limit(EnvironmentConfig.DEFAULT_RATE_LIMIT_THRESHOLD, methods=["POST"]) + ] + def post(self): """ Registers users without OpenStreetMap account diff --git a/backend/api/users/statistics.py b/backend/api/users/statistics.py index 05a7dedb3a..b40bfb8dd4 100644 --- a/backend/api/users/statistics.py +++ b/backend/api/users/statistics.py @@ -2,6 +2,7 @@ from datetime import date, timedelta from flask_restful import Resource, request, current_app +from backend import limiter, EnvironmentConfig from backend.services.users.user_service import UserService, NotFound from backend.services.stats_service import StatsService from backend.services.interests_service import InterestService @@ -98,6 +99,11 @@ def get(self, user_id): class UsersStatisticsAllAPI(Resource): + + decorators = [ + limiter.limit(EnvironmentConfig.DEFAULT_RATE_LIMIT_THRESHOLD, methods=["GET"]) + ] + @token_auth.login_required def get(self): """ diff --git a/backend/api/users/tasks.py b/backend/api/users/tasks.py index 792022843a..7563a47de0 100644 --- a/backend/api/users/tasks.py +++ b/backend/api/users/tasks.py @@ -1,11 +1,17 @@ from flask_restful import Resource, current_app, request from dateutil.parser import parse as date_parse +from backend import limiter, EnvironmentConfig from backend.services.users.authentication_service import token_auth from backend.services.users.user_service import UserService, NotFound class UsersTasksAPI(Resource): + + decorators = [ + limiter.limit(EnvironmentConfig.DEFAULT_RATE_LIMIT_THRESHOLD, methods=["GET"]) + ] + @token_auth.login_required def get(self, user_id): """ diff --git a/backend/config.py b/backend/config.py index c30c073268..9df90bd8ce 100644 --- a/backend/config.py +++ b/backend/config.py @@ -105,6 +105,11 @@ class EnvironmentConfig: # If disabled project update emails will not be sent. SEND_PROJECT_EMAIL_UPDATES = int(os.getenv("TM_SEND_PROJECT_EMAIL_UPDATES", True)) + # Threshold for rate limiting api calls + DEFAULT_RATE_LIMIT_THRESHOLD = os.getenv( + "TM_API_RATE_LIMIT_THRESHOLD", "100 per hour" + ) + # Languages offered by the Tasking Manager # Please note that there must be exactly the same number of Codes as languages. SUPPORTED_LANGUAGES = { diff --git a/requirements.txt b/requirements.txt index df3c30387a..01c931326d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,6 +22,7 @@ Flask-RESTful==0.3.8 Flask-Script==2.0.6 Flask-SQLAlchemy==2.4.4 flask-swagger==0.2.14 +Flask-Limiter==1.5 gevent==20.9.0 GeoAlchemy2==0.8.4 geojson==1.3.4