Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add rate limit to endpoints #5304

Draft
wants to merge 4 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
25 changes: 22 additions & 3 deletions backend/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
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

Expand All @@ -35,7 +37,11 @@ def format_url(endpoint):
migrate = Migrate()
mail = Mail()
oauth = OAuth()

limiter = Limiter(
storage_uri=EnvironmentConfig.REDIS_URI,
key_func=get_remote_address,
headers_enabled=True,
)
osm = oauth.remote_app("osm", app_key="OSM_OAUTH_SETTINGS")

# Import all models so that they are registered with SQLAlchemy
Expand Down Expand Up @@ -64,6 +70,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")

Expand Down Expand Up @@ -121,9 +128,21 @@ def add_api_endpoints(app):
"""
Define the routes the API exposes using Flask-Restful.
"""
app.logger.debug("Adding routes to API endpoints")
api = Api(app)
rate_limit_error = {
"RateLimitExceeded": {
"SubCode": "RateLimitExceeded",
"message": "You have exceeded the rate limit. Please try again later.",
"status": 429,
},
"ConnectionError": {
"SubCode": "RedisConnectionError",
"message": "Connection to Redis server refused.",
"status": 500,
},
}
api = Api(app, errors=rate_limit_error)

app.logger.debug("Adding routes to API endpoints")
# Projects API import
from backend.api.projects.resources import (
ProjectsRestAPI,
Expand Down
6 changes: 6 additions & 0 deletions backend/api/campaigns/resources.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions backend/api/projects/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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):
Expand Down
7 changes: 7 additions & 0 deletions backend/api/projects/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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):
"""
Expand Down
7 changes: 7 additions & 0 deletions backend/api/projects/statistics.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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
Expand Down
12 changes: 11 additions & 1 deletion backend/api/system/authentication.py
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions backend/api/system/general.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions backend/api/system/image_upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down
36 changes: 36 additions & 0 deletions backend/api/tasks/actions.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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):
"""
Expand Down
6 changes: 6 additions & 0 deletions backend/api/teams/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
"""
Expand Down
6 changes: 6 additions & 0 deletions backend/api/users/actions.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions backend/api/users/statistics.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
"""
Expand Down
6 changes: 6 additions & 0 deletions backend/api/users/tasks.py
Original file line number Diff line number Diff line change
@@ -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):
"""
Expand Down