Skip to content

Conversation

@rohan-chaturvedi
Copy link
Member

@rohan-chaturvedi rohan-chaturvedi commented Dec 4, 2025

🔍 Overview

Adds a rate-limiting system for the REST API based on the DRF Throttling. Redis is used as the cache backend. Rate limits are applied per account, based on the account org plan in Cloud Mode, and based on the value of RATE_LIMIT_DEFAULT in self hosted mode. When rate limited, the response is a 429 with a retry-after header indicating how many seconds to wait before re-trying a request

🖼️ Screenshots or Demo

HTTP/2 429 
server: nginx
date: Fri, 05 Dec 2025 07:18:01 GMT
content-type: application/json
content-length: 68
retry-after: 52
allow: GET, POST, PUT, DELETE, HEAD, OPTIONS
x-frame-options: DENY
vary: Origin
x-content-type-options: nosniff
referrer-policy: same-origin
cross-origin-opener-policy: same-origin

📝 Release Notes

Adds support for API rate limiting

🧪 Testing

Describe the testing strategy. List new tests added, existing tests modified, and any testing gaps.

💚 Did You...

  • Ensure linting passes (code style checks)?
  • Update dependencies and lockfiles (if required)
    - [ ] Update migrations (if required)
    - [ ] Regenerate graphql schema and types (if required)
  • Verify the app builds locally?
  • Manually test the changes on different browsers/devices?

Note

Implements plan-scoped API rate limiting and wires it into key endpoints, with supporting configuration and tests.

  • Introduces PlanBasedRateThrottle (per-user/service-account/service-token cache keys) selecting rates from PLAN_RATE_LIMITS (cloud) or DEFAULT (self-hosted)
  • Applies throttling to E2EESecretsView, PublicSecretsView, and aws_iam_auth
  • Updates backend/settings.py with PLAN_RATE_LIMITS, REST_FRAMEWORK['DEFAULT_THROTTLE_RATES'], Redis cache config (optional SSL), and RQ queues; makes DB PORT int and adds optional Postgres SSL options
  • Optimizes PhaseTokenAuthentication lookups via select_related when resolving environment
  • Dockerfile adds curl/ca-certificates and installs AWS RDS eu-central-1 CA bundle
  • Testing: adds pytest-django, pytest.ini, conftest.py, and tests/api/test_throttling.py covering cache keys and plan-based rate selection

Written by Cursor Bugbot for commit 00548fa. This will update automatically on new commits. Configure here.

Signed-off-by: rohan <rohan.chaturvedi@protonmail.com>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements a plan-based API rate limiting system using Django Rest Framework's throttling capabilities. Rate limits are configured per organisation plan tier (Free, Pro, Enterprise) and stored in Redis. The implementation includes database query optimizations in the authentication layer to prefetch organisation data needed for rate limit checks.

  • Adds configurable rate limits per plan tier (Free: 120/min, Pro: 240/min, Enterprise: 1000/min by default)
  • Implements custom throttle class that dynamically applies rate limits based on organisation plan
  • Optimizes authentication queries to prefetch organisation relationships and avoid N+1 queries

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 8 comments.

File Description
backend/backend/settings.py Adds rate limit configuration per plan tier, configures Redis cache backend for throttling, and sets up DRF throttle classes
backend/api/throttling.py Implements PlanBasedRateThrottle class that applies per-user/service-account rate limits based on organisation plan
backend/api/auth.py Optimizes database queries with select_related() to prefetch environment/app/organisation relationships; removes trailing whitespace

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Signed-off-by: rohan <rohan.chaturvedi@protonmail.com>
Signed-off-by: rohan <rohan.chaturvedi@protonmail.com>
@rohan-chaturvedi rohan-chaturvedi marked this pull request as ready for review December 5, 2025 08:24
rohan-chaturvedi and others added 7 commits December 8, 2025 14:39
Signed-off-by: rohan <rohan.chaturvedi@protonmail.com>
Signed-off-by: rohan <rohan.chaturvedi@protonmail.com>
Signed-off-by: rohan <rohan.chaturvedi@protonmail.com>
Signed-off-by: rohan <rohan.chaturvedi@protonmail.com>
Signed-off-by: rohan <rohan.chaturvedi@protonmail.com>
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is being reviewed by Cursor Bugbot

Details

You are on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

rohan-chaturvedi and others added 3 commits December 15, 2025 14:15
Signed-off-by: rohan <rohan.chaturvedi@protonmail.com>
- Removed default values for rate limits to rely on environment variables.
- Updated Redis SSL configuration to require SSL certificate validation and added CA certificates path from environment variables.
# If self-hosted return the default rate limit. If not set, this will disable throttling
if not CLOUD_HOSTED:
return settings.PLAN_RATE_LIMITS["DEFAULT"]
return settings.PLAN_RATE_LIMITS.get(plan, settings.PLAN_RATE_LIMITS["DEFAULT"])
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Plan rate limit fallback fails for unconfigured plans

In get_rate_for_plan, the expression settings.PLAN_RATE_LIMITS.get(plan, settings.PLAN_RATE_LIMITS["DEFAULT"]) doesn't correctly fall back to the default rate when a plan's rate is None. Since PLAN_RATE_LIMITS is populated with os.getenv() calls that return None for unset environment variables, the keys (FR, PR, EN) always exist in the dictionary with None values. Python's dict.get() only uses the default when the key is missing, not when the value is None. In cloud mode, if RATE_LIMIT_FREE is unset but RATE_LIMIT_DEFAULT is configured, Free plan users would get None as their rate, effectively disabling rate limiting for them.

Additional Locations (1)

Fix in Cursor Fix in Web

elif request.auth.get("service_token"):
ident = f"st_{request.auth['service_token'].id}"
else:
ident = f"anon_{ident}"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing fallback in cache key for unmatched auth types

In get_cache_key, when request.user.is_authenticated and request.auth is truthy but none of the three identity checks (org_member, service_account, service_token) match, the ident variable remains as the raw IP address without any prefix. The else clause adding the anon_ prefix only executes when the outer condition is falsy, not when the inner if-elif chain exhausts without a match. While currently unreachable with PhaseTokenAuthentication (which always sets one of these keys), this structure is fragile. Future auth changes could cause authenticated users to share IP-based rate limits instead of identity-based ones.

Fix in Cursor Fix in Web

nimish-ks and others added 7 commits December 30, 2025 13:41
- Updated REDIS_AUTH to URL-encode the password for better security.
- Added validation to ensure REDIS_CA_CERTS_PATH is set when using SSL.
- Configured RQ_SSL_OPTIONS to require SSL certificate validation and include CA certificates path.
- Added support for Redis username in REDIS_AUTH.
- Updated REDIS_AUTH construction to handle cases with and without username and password.
- Set REDIS_USERNAME to None if not provided, ensuring consistent behavior.
- Added REDIS_USERNAME to RQ_QUEUES configuration for better Redis authentication.
- Added SSL options to the DATABASES configuration for PostgreSQL.
- Configured SSL mode to require and conditionally include SSL root certificate based on environment variables.
nimish-ks and others added 7 commits December 30, 2025 17:22
- Removed unnecessary comments related to development settings and security warnings.
- Streamlined the configuration for better readability and maintainability.
- Added ca-certificates package alongside curl for improved security and functionality.
- Removed unnecessary line break for cleaner formatting.
- Included the AWS RDS CA bundle for the eu-central-1 region to enhance SSL connectivity.
- Set appropriate permissions for the CA bundle file.
- Eliminated the check for REDIS_CA_CERTS_PATH when REDIS_SSL is true, simplifying the configuration.
- Streamlined the Redis settings for improved clarity and maintainability.
- Changed DATABASE_SSL_CA_PATH to be used for PostgreSQL SSL settings, enhancing security.
- Updated Redis SSL configuration to consistently reference REDIS_SSL_CA_PATH for clarity.
- Improved maintainability by ensuring SSL options are conditionally set based on environment variables.
- Updated DATABASE_SSL_CA_PATH usage for PostgreSQL to enhance security with conditional SSL mode.
- Revised Redis SSL settings to consistently use REDIS_SSL_CA_PATH, improving clarity and maintainability.
- Ensured SSL options are dynamically set based on environment variables for better flexibility.
@nimish-ks
Copy link
Member

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants