From 80ad117299686b6164ae968c384c9ea78f16c296 Mon Sep 17 00:00:00 2001 From: Marco Cucchi Date: Wed, 10 Dec 2025 18:35:04 +0100 Subject: [PATCH 1/3] rate limits for unauthenticated --- cads_processing_api_service/config.py | 10 ++- cads_processing_api_service/limits.py | 24 ++++++- tests/test_30_limits.py | 90 +++++++++++++++++++++++++++ 3 files changed, 121 insertions(+), 3 deletions(-) diff --git a/cads_processing_api_service/config.py b/cads_processing_api_service/config.py index 233b423..292e9c5 100644 --- a/cads_processing_api_service/config.py +++ b/cads_processing_api_service/config.py @@ -116,7 +116,7 @@ class RateLimitsRouteParamConfig(pydantic.BaseModel): model_config = pydantic.ConfigDict(extra="allow") -class RateLimitsConfig(pydantic.BaseModel): +class RateLimitsUserConfig(pydantic.BaseModel): default: RateLimitsRouteConfig = pydantic.Field( default=RateLimitsRouteConfig(), validate_default=True ) @@ -151,6 +151,14 @@ class RateLimitsConfig(pydantic.BaseModel): ) +class RateLimitsConfig(RateLimitsUserConfig): + """Rate limits configuration for the service.""" + + unauthenticated: RateLimitsUserConfig = pydantic.Field( + default=RateLimitsUserConfig(), validate_default=True + ) + + def load_rate_limits(rate_limits_file: str | None) -> RateLimitsConfig: rate_limits = RateLimitsConfig() if rate_limits_file is not None: diff --git a/cads_processing_api_service/limits.py b/cads_processing_api_service/limits.py index 20e7b46..33f31d5 100644 --- a/cads_processing_api_service/limits.py +++ b/cads_processing_api_service/limits.py @@ -70,6 +70,26 @@ def get_rate_limits_defaulted( return rate_limits +def get_rate_limits_for_user( + rate_limits_config: config.RateLimitsConfig, + user_uid: str, + route: str, + method: str, + request_origin: str, + route_param: str | None = None, +) -> list[str]: + rate_limits = [] + if user_uid == "unauthenticated": + rate_limits = get_rate_limits_defaulted( + rate_limits_config.unauthenticated, route, method, request_origin, route_param + ) + if not rate_limits: + rate_limits = get_rate_limits_defaulted( + rate_limits_config, route, method, request_origin, route_param + ) + return rate_limits + + def check_rate_limits_for_user( user_uid: str, rate_limits: list[limits.RateLimitItem] ) -> None: @@ -104,8 +124,8 @@ def check_rate_limits( """Check if the rate limits are exceeded.""" request_origin = auth_info.request_origin user_uid = auth_info.user_uid - rate_limits = get_rate_limits_defaulted( - rate_limits_config, route, method, request_origin, route_param + rate_limits = get_rate_limits_for_user( + rate_limits_config, user_uid, route, method, request_origin, route_param ) rate_limits_parsed = [limits.parse(rate_limit) for rate_limit in rate_limits] check_rate_limits_for_user(user_uid, rate_limits_parsed) diff --git a/tests/test_30_limits.py b/tests/test_30_limits.py index 43a5d35..645e6ab 100644 --- a/tests/test_30_limits.py +++ b/tests/test_30_limits.py @@ -190,6 +190,96 @@ def test_get_rate_limits_undefined() -> None: assert rate_limits == exp_rate_limits +def test_get_rate_limits_for_user_unauthenticated() -> None: + rate_limits = { + "default": { + "get": {"api": ["5/second"]}, + "post": {"api": ["10/second"]}, + }, + "/jobs/{job_id}": {"delete": {"api": ["1/second"]}}, + "unauthenticated": { + "default": {"post": {"api": ["2/second"]}}, + "/jobs/{job_id}": {"get": {"api": ["3/second"]}} + }, + } + rate_limits_config = config.RateLimitsConfig.model_validate(rate_limits) + + route = "jobs_jobsid" + method = "get" + request_origin = "api" + user_uid = "unauthenticated" + rate_limits = cads_processing_api_service.limits.get_rate_limits_for_user( + rate_limits_config, user_uid, route, method, request_origin + ) + exp_rate_limits = ["3/second"] + assert rate_limits == exp_rate_limits + + route = "jobs_jobsid" + method = "post" + request_origin = "api" + user_uid = "unauthenticated" + rate_limits = cads_processing_api_service.limits.get_rate_limits_for_user( + rate_limits_config, user_uid, route, method, request_origin + ) + exp_rate_limits = ["2/second"] + assert rate_limits == exp_rate_limits + + route = "jobs_jobsid" + method = "delete" + request_origin = "api" + user_uid = "unauthenticated" + rate_limits = cads_processing_api_service.limits.get_rate_limits_for_user( + rate_limits_config, user_uid, route, method, request_origin + ) + exp_rate_limits = ["1/second"] + assert rate_limits == exp_rate_limits + + +def test_get_rate_limits_for_user_authenticated() -> None: + rate_limits = { + "default": { + "get": {"api": ["5/second"]}, + "post": {"api": ["10/second"]}, + }, + "/jobs/{job_id}": {"delete": {"api": ["1/second"]}}, + "unauthenticated": { + "default": {"post": {"api": ["2/second"]}}, + "/jobs/{job_id}": {"get": {"api": ["3/second"]}} + }, + } + rate_limits_config = config.RateLimitsConfig.model_validate(rate_limits) + + route = "jobs_jobsid" + method = "get" + request_origin = "api" + user_uid = "user_uid" + rate_limits = cads_processing_api_service.limits.get_rate_limits_for_user( + rate_limits_config, user_uid, route, method, request_origin + ) + exp_rate_limits = ["5/second"] + assert rate_limits == exp_rate_limits + + route = "jobs_jobsid" + method = "post" + request_origin = "api" + user_uid = "user_uid" + rate_limits = cads_processing_api_service.limits.get_rate_limits_for_user( + rate_limits_config, user_uid, route, method, request_origin + ) + exp_rate_limits = ["10/second"] + assert rate_limits == exp_rate_limits + + route = "jobs_jobsid" + method = "delete" + request_origin = "api" + user_uid = "user_uid" + rate_limits = cads_processing_api_service.limits.get_rate_limits_for_user( + rate_limits_config, user_uid, route, method, request_origin + ) + exp_rate_limits = ["1/second"] + assert rate_limits == exp_rate_limits + + def test_check_rate_limits_for_user() -> None: rate_limit_ids = ["1/second"] rate_limits = [limits.parse(rate_limit_id) for rate_limit_id in rate_limit_ids] From 9ecdc092ce02979adffd97458decee499e543a34 Mon Sep 17 00:00:00 2001 From: Marco Cucchi Date: Wed, 10 Dec 2025 18:41:19 +0100 Subject: [PATCH 2/3] qa --- cads_processing_api_service/limits.py | 6 +++++- tests/test_30_limits.py | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/cads_processing_api_service/limits.py b/cads_processing_api_service/limits.py index 33f31d5..6e6b187 100644 --- a/cads_processing_api_service/limits.py +++ b/cads_processing_api_service/limits.py @@ -81,7 +81,11 @@ def get_rate_limits_for_user( rate_limits = [] if user_uid == "unauthenticated": rate_limits = get_rate_limits_defaulted( - rate_limits_config.unauthenticated, route, method, request_origin, route_param + rate_limits_config.unauthenticated, + route, + method, + request_origin, + route_param, ) if not rate_limits: rate_limits = get_rate_limits_defaulted( diff --git a/tests/test_30_limits.py b/tests/test_30_limits.py index 645e6ab..59b0513 100644 --- a/tests/test_30_limits.py +++ b/tests/test_30_limits.py @@ -199,7 +199,7 @@ def test_get_rate_limits_for_user_unauthenticated() -> None: "/jobs/{job_id}": {"delete": {"api": ["1/second"]}}, "unauthenticated": { "default": {"post": {"api": ["2/second"]}}, - "/jobs/{job_id}": {"get": {"api": ["3/second"]}} + "/jobs/{job_id}": {"get": {"api": ["3/second"]}}, }, } rate_limits_config = config.RateLimitsConfig.model_validate(rate_limits) @@ -244,7 +244,7 @@ def test_get_rate_limits_for_user_authenticated() -> None: "/jobs/{job_id}": {"delete": {"api": ["1/second"]}}, "unauthenticated": { "default": {"post": {"api": ["2/second"]}}, - "/jobs/{job_id}": {"get": {"api": ["3/second"]}} + "/jobs/{job_id}": {"get": {"api": ["3/second"]}}, }, } rate_limits_config = config.RateLimitsConfig.model_validate(rate_limits) From 690096f6763ad54b4ccf2b13d748caab006bc040 Mon Sep 17 00:00:00 2001 From: Marco Cucchi Date: Thu, 11 Dec 2025 09:33:15 +0100 Subject: [PATCH 3/3] fix types --- cads_processing_api_service/limits.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cads_processing_api_service/limits.py b/cads_processing_api_service/limits.py index 6e6b187..80b742d 100644 --- a/cads_processing_api_service/limits.py +++ b/cads_processing_api_service/limits.py @@ -30,7 +30,7 @@ def get_rate_limits( - rate_limits_config: config.RateLimitsConfig, + rate_limits_config: config.RateLimitsUserConfig, route: str, method: str, request_origin: str, @@ -49,7 +49,7 @@ def get_rate_limits( def get_rate_limits_defaulted( - rate_limits_config: config.RateLimitsConfig, + rate_limits_config: config.RateLimitsUserConfig, route: str, method: str, request_origin: str,