From 8ab236a42c7f7f2fbcf3d2192359cebb12d79054 Mon Sep 17 00:00:00 2001 From: j-awada Date: Thu, 8 Jan 2026 14:45:18 +0100 Subject: [PATCH 1/8] introduce throttling for the auth endpoint --- studio/settings.py | 3 +++ studio/views.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/studio/settings.py b/studio/settings.py index 92c03bba..e06345eb 100644 --- a/studio/settings.py +++ b/studio/settings.py @@ -350,6 +350,9 @@ "DEFAULT_VERSION": "v1", "DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",), "DEFAULT_PARSER_CLASSES": ("rest_framework.parsers.JSONParser",), + "DEFAULT_THROTTLE_RATES": { + "user": "5/minute", + }, } # Tagulous serialization settings diff --git a/studio/views.py b/studio/views.py index a9562266..eaf8d5b3 100644 --- a/studio/views.py +++ b/studio/views.py @@ -15,6 +15,7 @@ from rest_framework.authentication import SessionAuthentication, TokenAuthentication from rest_framework.permissions import BasePermission, IsAuthenticated from rest_framework.response import Response +from rest_framework.throttling import UserRateThrottle from rest_framework.views import APIView from apps.app_registry import APP_REGISTRY @@ -112,6 +113,7 @@ class AuthView(APIView): authentication_classes = [ModifiedSessionAuthentication, TokenAuthentication] permission_classes = [IsAuthenticated, AccessPermission] content_negotiation_class = IgnoreClientContentNegotiation + throttle_classes = [UserRateThrottle] def get(self, request: Response, format: str | None = None) -> Response: content = { From dff2fad7515290ca2056b4451a1570d1a698c6d1 Mon Sep 17 00:00:00 2001 From: j-awada Date: Fri, 9 Jan 2026 17:56:49 +0100 Subject: [PATCH 2/8] whitelist ips from rate limiting --- .env.template | 2 ++ studio/settings.py | 7 ++++--- studio/utils.py | 20 ++++++++++++++++++++ studio/views.py | 5 ++--- 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/.env.template b/.env.template index 3ac680c7..6fafd06c 100644 --- a/.env.template +++ b/.env.template @@ -50,3 +50,5 @@ BASE_URL=http://studio.127.0.0.1.nip.io:8080 TLS_SSL_VERIFICATION=0 # if need to disable tls/ssl verification APP_PROBE_STATUSES=Running,Deleted # PROBE_PF=38123 # specify a port for port-forwaring, or for more apps: PROBE_PF=sp-status:38121,shiny-probe-test:38123 + +RATE_LIMIT_WHITELIST=130.238.0.0/16 diff --git a/studio/settings.py b/studio/settings.py index e06345eb..c0c1efbe 100644 --- a/studio/settings.py +++ b/studio/settings.py @@ -350,11 +350,12 @@ "DEFAULT_VERSION": "v1", "DEFAULT_RENDERER_CLASSES": ("rest_framework.renderers.JSONRenderer",), "DEFAULT_PARSER_CLASSES": ("rest_framework.parsers.JSONParser",), - "DEFAULT_THROTTLE_RATES": { - "user": "5/minute", - }, } +# Rate limit whitelist for certain IP ranges +RATE_LIMIT_WHITELIST = os.environ.get("RATE_LIMIT_WHITELIST", "130.238.0.0/16").split(",") +RATE_LIMIT_WHITELIST = [ip.strip() for ip in RATE_LIMIT_WHITELIST] + # Tagulous serialization settings SERIALIZATION_MODULES = { "xml": "tagulous.serializers.xml_serializer", diff --git a/studio/utils.py b/studio/utils.py index 1252331e..e186488d 100644 --- a/studio/utils.py +++ b/studio/utils.py @@ -1,8 +1,12 @@ import logging +from ipaddress import ip_address, ip_network from typing import Any, List import structlog from django.conf import settings +from django.http import HttpRequest +from rest_framework.throttling import UserRateThrottle +from rest_framework.views import APIView def get_logger(name: str) -> Any: @@ -29,3 +33,19 @@ def add_loggers(logging: dict[str, Any], installed_apps: List[str]) -> dict[str, } return logging + + +class WhitelistThrottleFilter(UserRateThrottle): + """ + Custom throttle filter that whitelists certain IP ranges + """ + + rate = "1/minute" + + def allow_request(self, request: HttpRequest, view: APIView) -> Any: + incomming_ip = self.get_ident(request) + whitelist_range = getattr(settings, "RATE_LIMIT_WHITELIST", []) + for network in whitelist_range: + if ip_address(incomming_ip) in ip_network(network): + return True + return super().allow_request(request, view) diff --git a/studio/views.py b/studio/views.py index eaf8d5b3..6449d614 100644 --- a/studio/views.py +++ b/studio/views.py @@ -15,7 +15,6 @@ from rest_framework.authentication import SessionAuthentication, TokenAuthentication from rest_framework.permissions import BasePermission, IsAuthenticated from rest_framework.response import Response -from rest_framework.throttling import UserRateThrottle from rest_framework.views import APIView from apps.app_registry import APP_REGISTRY @@ -24,7 +23,7 @@ from common.tasks import send_email_task from models.models import Model from projects.models import Project -from studio.utils import get_logger +from studio.utils import WhitelistThrottleFilter, get_logger from .helpers import do_delete_account from .negotiation import IgnoreClientContentNegotiation @@ -113,7 +112,7 @@ class AuthView(APIView): authentication_classes = [ModifiedSessionAuthentication, TokenAuthentication] permission_classes = [IsAuthenticated, AccessPermission] content_negotiation_class = IgnoreClientContentNegotiation - throttle_classes = [UserRateThrottle] + throttle_classes = [WhitelistThrottleFilter] def get(self, request: Response, format: str | None = None) -> Response: content = { From 9f13e77289e13947d570144119172525b5119945 Mon Sep 17 00:00:00 2001 From: j-awada Date: Fri, 9 Jan 2026 18:08:26 +0100 Subject: [PATCH 3/8] increase auth rate limit --- studio/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/studio/utils.py b/studio/utils.py index e186488d..617da565 100644 --- a/studio/utils.py +++ b/studio/utils.py @@ -40,7 +40,7 @@ class WhitelistThrottleFilter(UserRateThrottle): Custom throttle filter that whitelists certain IP ranges """ - rate = "1/minute" + rate = "10/minute" def allow_request(self, request: HttpRequest, view: APIView) -> Any: incomming_ip = self.get_ident(request) From d2e43f71825818fece0560f19cc2bab3b5510bb4 Mon Sep 17 00:00:00 2001 From: j-awada Date: Mon, 12 Jan 2026 12:24:10 +0100 Subject: [PATCH 4/8] fix ci test issue - import order --- studio/settings.py | 2 +- studio/throttle.py | 21 +++++++++++++++++++++ studio/utils.py | 20 -------------------- studio/views.py | 3 ++- 4 files changed, 24 insertions(+), 22 deletions(-) create mode 100644 studio/throttle.py diff --git a/studio/settings.py b/studio/settings.py index 342c21bf..2426b914 100644 --- a/studio/settings.py +++ b/studio/settings.py @@ -353,7 +353,7 @@ } # Rate limit whitelist for certain IP ranges -RATE_LIMIT_WHITELIST = os.environ.get("RATE_LIMIT_WHITELIST", "130.238.0.0/16").split(",") +RATE_LIMIT_WHITELIST = os.environ.get("RATE_LIMIT_WHITELIST", "10.42.0.0/16").split(",") RATE_LIMIT_WHITELIST = [ip.strip() for ip in RATE_LIMIT_WHITELIST] # Tagulous serialization settings diff --git a/studio/throttle.py b/studio/throttle.py new file mode 100644 index 00000000..9d8adaf5 --- /dev/null +++ b/studio/throttle.py @@ -0,0 +1,21 @@ +from ipaddress import ip_address, ip_network +from typing import Any + +from django.conf import settings +from django.http import HttpRequest +from rest_framework.throttling import UserRateThrottle +from rest_framework.views import APIView + +class WhitelistThrottleFilter(UserRateThrottle): + """ + Custom throttle filter that whitelists certain IP ranges + """ + rate = "100/minute" + + def allow_request(self, request: HttpRequest, view: APIView) -> Any: + incomming_ip = self.get_ident(request) + whitelist_range = getattr(settings, "RATE_LIMIT_WHITELIST", []) + for network in whitelist_range: + if ip_address(incomming_ip) in ip_network(network): + return True + return super().allow_request(request, view) diff --git a/studio/utils.py b/studio/utils.py index 617da565..1252331e 100644 --- a/studio/utils.py +++ b/studio/utils.py @@ -1,12 +1,8 @@ import logging -from ipaddress import ip_address, ip_network from typing import Any, List import structlog from django.conf import settings -from django.http import HttpRequest -from rest_framework.throttling import UserRateThrottle -from rest_framework.views import APIView def get_logger(name: str) -> Any: @@ -33,19 +29,3 @@ def add_loggers(logging: dict[str, Any], installed_apps: List[str]) -> dict[str, } return logging - - -class WhitelistThrottleFilter(UserRateThrottle): - """ - Custom throttle filter that whitelists certain IP ranges - """ - - rate = "10/minute" - - def allow_request(self, request: HttpRequest, view: APIView) -> Any: - incomming_ip = self.get_ident(request) - whitelist_range = getattr(settings, "RATE_LIMIT_WHITELIST", []) - for network in whitelist_range: - if ip_address(incomming_ip) in ip_network(network): - return True - return super().allow_request(request, view) diff --git a/studio/views.py b/studio/views.py index 6449d614..fd346319 100644 --- a/studio/views.py +++ b/studio/views.py @@ -23,7 +23,8 @@ from common.tasks import send_email_task from models.models import Model from projects.models import Project -from studio.utils import WhitelistThrottleFilter, get_logger +from studio.throttle import WhitelistThrottleFilter +from studio.utils import get_logger from .helpers import do_delete_account from .negotiation import IgnoreClientContentNegotiation From 53e5810eb75497844e32405f8f250dcda191d0c2 Mon Sep 17 00:00:00 2001 From: j-awada Date: Mon, 12 Jan 2026 12:31:02 +0100 Subject: [PATCH 5/8] lint code and adjust rate limit value --- studio/throttle.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/studio/throttle.py b/studio/throttle.py index 9d8adaf5..a4b05544 100644 --- a/studio/throttle.py +++ b/studio/throttle.py @@ -6,11 +6,13 @@ from rest_framework.throttling import UserRateThrottle from rest_framework.views import APIView + class WhitelistThrottleFilter(UserRateThrottle): """ Custom throttle filter that whitelists certain IP ranges """ - rate = "100/minute" + + rate = "10/minute" def allow_request(self, request: HttpRequest, view: APIView) -> Any: incomming_ip = self.get_ident(request) From eab145f3378327b41b6581492f9c68186a76b9bd Mon Sep 17 00:00:00 2001 From: j-awada Date: Mon, 12 Jan 2026 13:23:39 +0100 Subject: [PATCH 6/8] add variable for auth rate limit value --- .env.template | 4 +++- studio/settings.py | 7 ++++--- studio/throttle.py | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.env.template b/.env.template index 7c72a4a3..0da5c78e 100644 --- a/.env.template +++ b/.env.template @@ -51,7 +51,9 @@ TLS_SSL_VERIFICATION=0 # if need to disable tls/ssl verification APP_PROBE_STATUSES=Running,Deleted # PROBE_PF=38123 # specify a port for port-forwaring, or for more apps: PROBE_PF=sp-status:38121,shiny-probe-test:38123 -RATE_LIMIT_WHITELIST=130.238.0.0/16 +# Rate limiting configuration for the auth endpoint +AUTH_RATE_LIMIT_VALUE= +AUTH_RATE_LIMIT_WHITELIST= # Invenio API INVENIO_URL= diff --git a/studio/settings.py b/studio/settings.py index 2426b914..8e3b8164 100644 --- a/studio/settings.py +++ b/studio/settings.py @@ -352,9 +352,10 @@ "DEFAULT_PARSER_CLASSES": ("rest_framework.parsers.JSONParser",), } -# Rate limit whitelist for certain IP ranges -RATE_LIMIT_WHITELIST = os.environ.get("RATE_LIMIT_WHITELIST", "10.42.0.0/16").split(",") -RATE_LIMIT_WHITELIST = [ip.strip() for ip in RATE_LIMIT_WHITELIST] +# Rate limit whitelist for certain IP ranges on the auth endpoint +AUTH_RATE_LIMIT_VALUE = os.environ.get("AUTH_RATE_LIMIT_VALUE", "10/minute") +AUTH_RATE_LIMIT_WHITELIST = os.environ.get("AUTH_RATE_LIMIT_WHITELIST", "10.42.0.0/16").split(",") +AUTH_RATE_LIMIT_WHITELIST = [ip.strip() for ip in AUTH_RATE_LIMIT_WHITELIST] # Tagulous serialization settings SERIALIZATION_MODULES = { diff --git a/studio/throttle.py b/studio/throttle.py index a4b05544..b9ae159e 100644 --- a/studio/throttle.py +++ b/studio/throttle.py @@ -12,7 +12,7 @@ class WhitelistThrottleFilter(UserRateThrottle): Custom throttle filter that whitelists certain IP ranges """ - rate = "10/minute" + rate = getattr(settings, "AUTH_RATE_LIMIT_VALUE", "10/minute") def allow_request(self, request: HttpRequest, view: APIView) -> Any: incomming_ip = self.get_ident(request) From f2bdc4560da5e49329d73c3be80ffd7a5274af22 Mon Sep 17 00:00:00 2001 From: j-awada Date: Mon, 12 Jan 2026 15:12:03 +0100 Subject: [PATCH 7/8] fix typos --- studio/throttle.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/studio/throttle.py b/studio/throttle.py index b9ae159e..ba8f2d4e 100644 --- a/studio/throttle.py +++ b/studio/throttle.py @@ -15,9 +15,9 @@ class WhitelistThrottleFilter(UserRateThrottle): rate = getattr(settings, "AUTH_RATE_LIMIT_VALUE", "10/minute") def allow_request(self, request: HttpRequest, view: APIView) -> Any: - incomming_ip = self.get_ident(request) - whitelist_range = getattr(settings, "RATE_LIMIT_WHITELIST", []) + incoming_ip = self.get_ident(request) + whitelist_range = getattr(settings, "AUTH_RATE_LIMIT_WHITELIST", []) for network in whitelist_range: - if ip_address(incomming_ip) in ip_network(network): + if ip_address(incoming_ip) in ip_network(network): return True return super().allow_request(request, view) From 779d85e07aade6875e631397dcf7b0e28c13ba0a Mon Sep 17 00:00:00 2001 From: j-awada Date: Tue, 13 Jan 2026 15:09:16 +0100 Subject: [PATCH 8/8] use forwarded ip address --- studio/settings.py | 6 +++--- studio/throttle.py | 43 +++++++++++++++++++++++++++++++++++++------ 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/studio/settings.py b/studio/settings.py index 8e3b8164..923c4154 100644 --- a/studio/settings.py +++ b/studio/settings.py @@ -353,9 +353,9 @@ } # Rate limit whitelist for certain IP ranges on the auth endpoint -AUTH_RATE_LIMIT_VALUE = os.environ.get("AUTH_RATE_LIMIT_VALUE", "10/minute") -AUTH_RATE_LIMIT_WHITELIST = os.environ.get("AUTH_RATE_LIMIT_WHITELIST", "10.42.0.0/16").split(",") -AUTH_RATE_LIMIT_WHITELIST = [ip.strip() for ip in AUTH_RATE_LIMIT_WHITELIST] +AUTH_RATE_LIMIT_VALUE = os.environ.get("AUTH_RATE_LIMIT_VALUE", None) +AUTH_RATE_LIMIT_WHITELIST_RAW = os.environ.get("AUTH_RATE_LIMIT_WHITELIST", "") +AUTH_RATE_LIMIT_WHITELIST = [ip.strip() for ip in AUTH_RATE_LIMIT_WHITELIST_RAW.split(",")] # Tagulous serialization settings SERIALIZATION_MODULES = { diff --git a/studio/throttle.py b/studio/throttle.py index ba8f2d4e..2ba8bc28 100644 --- a/studio/throttle.py +++ b/studio/throttle.py @@ -12,12 +12,43 @@ class WhitelistThrottleFilter(UserRateThrottle): Custom throttle filter that whitelists certain IP ranges """ - rate = getattr(settings, "AUTH_RATE_LIMIT_VALUE", "10/minute") + rate = getattr(settings, "AUTH_RATE_LIMIT_VALUE", None) + + def get_ident(self, request: HttpRequest) -> Any: + """ + Extract the real client IP from proxy headers + """ + + # Try X-Forwarded-For first (standard proxy header) + xff = request.META.get("HTTP_X_FORWARDED_FOR") + if xff: + ip = xff.split(",")[0].strip() + return ip + + # Try X-Real-IP (nginx specific) + real_ip = request.META.get("HTTP_X_REAL_IP") + if real_ip: + return real_ip + + # Fallback to Django's standard remote address + fallback = request.META.get("REMOTE_ADDR", "unknown") + return fallback def allow_request(self, request: HttpRequest, view: APIView) -> Any: - incoming_ip = self.get_ident(request) - whitelist_range = getattr(settings, "AUTH_RATE_LIMIT_WHITELIST", []) - for network in whitelist_range: - if ip_address(incoming_ip) in ip_network(network): - return True + # If no rate is configured, throttling is disabled entirely + if not self.rate: + return True + + whitelist_range = getattr(settings, "AUTH_RATE_LIMIT_WHITELIST", None) + + # If whitelist is configured, check if IP is whitelisted + if whitelist_range: + incoming_ip = self.get_ident(request) + for network in whitelist_range if isinstance(whitelist_range, list) else [whitelist_range]: + try: + if ip_address(incoming_ip) in ip_network(network): + return True # Whitelisted, allow through + except ValueError: + continue # Skip invalid network/IP formats + return super().allow_request(request, view)