Skip to content

feat(rate-limit): exempt admins and raise default limits - #612

Merged
Ahmath-Gadji merged 2 commits into
refactor/hexagonalfrom
feat/rate-limit-admin-bypass
Jul 1, 2026
Merged

feat(rate-limit): exempt admins and raise default limits#612
Ahmath-Gadji merged 2 commits into
refactor/hexagonalfrom
feat/rate-limit-admin-bypass

Conversation

@Ahmath-Gadji

@Ahmath-Gadji Ahmath-Gadji commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Why

Admin users already bypass the per-user file quota (check_user_file_quota in openrag/api/dependencies/auth.py), but they were still subject to request rate limiting. Trusted operators — the admin UI polling job status, bulk indexing scripts — shouldn't be throttled. This extends the same admin exemption to RateLimitMiddleware.

While there, the default limits were low for real usage, so this raises them.

Changes

Admin bypass (openrag/api/middleware/rate_limit.py)

  • New RateLimitMiddleware._is_admin(request) reads request.state.user["is_admin"] — the same dict AuthMiddleware populates.
  • dispatch() short-circuits for admins before touching the limiter, mirroring the file-quota bypass. RateLimitMiddleware runs after AuthMiddleware (registration is reverse of execution, see api/main.py), so is_admin is available. Unauthenticated /auth/* paths have no user on request.state, so this only ever exempts an authenticated admin.

Raised default limits (per-identity, per-worker moving window)

Env Old New Rationale
RATE_LIMIT_AUTH 20/min 60/min Keyed on client IP (callers are unauthenticated during login). 20/min ≈ 10 logins/min for the whole IP — a shared corporate/NAT egress IP throttles a legitimate morning login rush.
RATE_LIMIT_CHAT 60/min 120/min 1 req/s is tight for streaming / agentic clients doing rapid tool-call follow-ups.
RATE_LIMIT_DEFAULT 300/min 600/min A UI polling job status while loading lists can approach 5 req/s.

.env.example updated to match, with a note about the IP-keyed /auth/* tier.

Not changed

The IP-keyed auth-failure limiter in AuthMiddleware is deliberately left as-is: it runs before authentication and only counts failed attempts, so a successful admin never trips it — and the caller's identity is unknown at that point, so an admin bypass isn't possible (or desirable, since it's brute-force protection).

Tests

Added to tests/unit/api/middleware/test_rate_limit.py:

  • _is_admin → true for admin dict, false for non-admin and unauthenticated.
  • End-to-end: an admin sails past a 1/min limit (5 requests, all 200).

ruff check / ruff format clean; new tests pass locally.

Note: two pre-existing tests in this file (test_blocks_over_limit_with_retry_after, test_tiers_have_independent_budgets) fail on refactor/hexagonal as well — a TestClient/limits.aio async-storage artifact (the memory store binds to a per-request event loop under the sync test client). Verified the limiter blocks correctly at runtime (hit[True, True, False, False] for a 2/min limit), so it's a test-harness quirk, not a functional regression, and out of scope here.

Summary by CodeRabbit

  • New Features

    • Admin users now bypass request rate limits.
    • Default rate-limit settings have been increased for general, authentication, and API chat requests.
  • Bug Fixes

    • Improved rate-limit handling so authenticated admins are no longer blocked by normal throttling.
    • Added clearer guidance in the sample configuration for rate-limit behavior and recommended limits.

Admin users already bypass the file quota; extend the same treatment to
request rate limiting so trusted operators (admin UI polling, bulk
scripts) are never throttled. RateLimitMiddleware runs after AuthMiddleware,
so request.state.user["is_admin"] is available — short-circuit on it,
mirroring check_user_file_quota.

Also raise the default limits, which were low for real usage:
- RATE_LIMIT_AUTH  20 -> 60/minute: this tier is keyed on client IP
  (callers are unauthenticated during the login flow), so a shared
  corporate/NAT egress IP could throttle a legitimate login rush.
- RATE_LIMIT_CHAT  60 -> 120/minute: 1 req/s is tight for streaming /
  agentic clients doing rapid tool-call follow-ups.
- RATE_LIMIT_DEFAULT 300 -> 600/minute: a UI that polls job status while
  loading lists can approach 5 req/s.

The IP-keyed auth-failure limiter in AuthMiddleware is unchanged: it runs
before authentication and only counts failed attempts, so a successful
admin never trips it and identity is unknown at that point.
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds an admin bypass to the rate-limiting middleware, so authenticated admin requests skip rate-limit checks entirely. Increases default per-minute rate limits for general, auth, and chat paths in code and env example, updates docstrings, and adds unit tests covering the admin bypass behavior.

Changes

Rate Limit Admin Bypass

Layer / File(s) Summary
Admin bypass logic and updated defaults
openrag/api/middleware/rate_limit.py
Docstring documents admin bypass and new RATE_LIMIT_* defaults; __init__ parses increased default/auth/chat limits; dispatch adds _is_admin check that calls downstream directly for admins, skipping limiter checks and 429 responses.
Tests and rate limit env defaults
tests/unit/api/middleware/test_rate_limit.py, infra/compose/.env.example
New tests validate _is_admin for admin/non-admin/unauthenticated users and confirm admins bypass a tight chat rate limit; .env.example enables RATE_LIMIT_ENABLED by default and raises suggested per-minute limits with added comments.

Estimated code review effort: 2 (Simple) | ~15 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant RateLimitMiddleware
  participant Limiter
  participant Downstream

  Client->>RateLimitMiddleware: HTTP request
  RateLimitMiddleware->>RateLimitMiddleware: _is_admin(request)
  alt admin user
    RateLimitMiddleware->>Downstream: call_next(request)
  else non-admin user
    RateLimitMiddleware->>RateLimitMiddleware: _limit_for(path), _identity(request)
    RateLimitMiddleware->>Limiter: hit / get_window_stats
    alt limit exceeded
      RateLimitMiddleware->>Client: 429 JSONResponse with Retry-After
    else within limit
      RateLimitMiddleware->>Downstream: call_next(request)
    end
  end
Loading

Suggested labels: enhancement, security

Suggested reviewers: none identified

🐰 A hop, a skip, past limiter gates —
admins glide through while others wait,
the counters raised, the docstrings clear,
tests confirm the bypass is here.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: admin rate-limit bypass plus increased default limits.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/rate-limit-admin-bypass

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@hedhoud hedhoud left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks good to me. I only noticed one small test-only nit: the admin bypass test comment says the same-tier non-admin still gets 429, but the test itself only exercises the admin path. Either trimming that sentence or adding the non-admin assertion would make the coverage clearer. Not blocking from my side.

The comment claimed a non-admin still gets 429, but the test only
exercises the admin path. Reword it to describe what the test actually
asserts and note that the non-admin side is covered by the _is_admin
unit tests, per PR review feedback.
@Ahmath-Gadji

Copy link
Copy Markdown
Collaborator Author

Good catch @hedhoud — the comment was over-claiming. Fixed in 4a2b336: reworded it to describe what the test actually asserts (admin path only) and noted that the non-admin side is covered by test_is_admin_false_for_non_admin_and_unauthenticated.

I deliberately didn't add a live non-admin 429 assertion: exhausting the limit across TestClient requests is unreliable under limits.aio's per-event-loop memory storage — it's the same artifact that makes the two pre-existing sibling tests (test_blocks_over_limit_with_retry_after, test_tiers_have_independent_budgets) pass in CI but fail on a local uv run pytest. So the discrimination is verified at the unit level instead.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@openrag/api/middleware/rate_limit.py`:
- Around line 44-46: The `/auth/*` rate limit in `RateLimitMiddleware` is too
permissive for unauthenticated traffic keyed by client IP, so tighten it to
better resist credential-stuffing. Adjust the `_auth` bucket in the middleware
initialization to a lower default and/or add a dedicated failed-login limiter
for the login path so repeated failures are throttled more aggressively.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 70e0c1dd-9c17-4a54-8bbe-ca91fe65c561

📥 Commits

Reviewing files that changed from the base of the PR and between 664f332 and 4a2b336.

📒 Files selected for processing (3)
  • infra/compose/.env.example
  • openrag/api/middleware/rate_limit.py
  • tests/unit/api/middleware/test_rate_limit.py

Comment on lines +44 to +46
self._default = parse(os.environ.get("RATE_LIMIT_DEFAULT", "600/minute"))
self._auth = parse(os.environ.get("RATE_LIMIT_AUTH", "60/minute"))
self._chat = parse(os.environ.get("RATE_LIMIT_CHAT", "120/minute"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

recommended rate limit per minute for login endpoints to prevent brute force

💡 Result:

There is no single "correct" rate limit for all login endpoints; the optimal configuration must be tailored to your application's expected user behavior and threat model [1][2][3]. However, authoritative security guidance from sources like OWASP recommends a layered, multi-dimensional approach rather than relying on a single IP-based limit [4][5][2]. Recommended Strategies and Best Practices: 1. Use Layered Buckets (Independent Limits) To effectively stop both targeted brute force and distributed credential stuffing, you must implement independent rate-limiting buckets that are checked separately [4][5]. If a request hits either threshold, it should be rejected [4][5]. * Per-Username Bucket: Limits attempts against any specific account, regardless of the source IP. This defends against distributed attacks where an attacker rotates through many IPs to target one account [4][5]. * Per-IP (or Per-IP+ASN) Bucket: Limits total login attempts originating from a single source (e.g., an IP address). This defends against credential-stuffing sweeps that attempt many different usernames from one location [4][5]. 2. Typical Starting Thresholds While you should tune these based on your specific traffic patterns, common starting points from industry examples include: * Per-IP: Often set between 5–10 attempts per minute [6][2][7]. Some implementations allow for a small burst (e.g., 20) to accommodate legitimate users [8][7]. * Per-Username: Often more conservative, such as 5 attempts per hour, depending on the sensitivity of the service [2][9]. * Adjustment: Start with conservative limits and gradually relax them after monitoring for false positives [6][9]. 3. Implementation Guidelines * Use Edge-Level Filtering: Perform rate limiting at the network edge (CDN, WAF, or load balancer) whenever possible [8][2][7]. This is the most efficient method because it blocks malicious traffic before it consumes application resources (like database connections or PHP workers) [8][2][7]. * Use Appropriate Algorithms: Prefer token-bucket or sliding-window algorithms [5][9][3]. Avoid simple fixed-window counters, which can be easily bypassed by bursting at the window boundary [5][3]. * State Management: In distributed or serverless environments, ensure your rate-limiting state is stored in a shared, high-performance data store like Redis [10]. In-memory counters will fail to protect against distributed attacks in multi-instance deployments [10]. * Consistent Responses: Return a generic 429 Too Many Requests status code [4][10][5]. Avoid including specific diagnostic details (such as which bucket was tripped or the number of remaining attempts) as this helps attackers refine their tools [4][5]. * Supplemental Defenses: Rate limiting alone is insufficient against sophisticated botnets [2][3]. Complement it with bot detection (device fingerprinting), CAPTCHA challenges for suspicious traffic, account lockout policies, and phishing-resistant authentication methods like passkeys [1][8][5][2][11]. Always monitor your authentication logs to distinguish between legitimate user frustration (e.g., mistyping a password) and actual attack traffic, and adjust your thresholds accordingly [6][2][12].

Citations:


🏁 Script executed:

sed -n '1,220p' openrag/api/middleware/rate_limit.py

Repository: linagora/openrag

Length of output: 4283


Tighten /auth/* throttling

The /auth/* bucket is keyed by client IP for unauthenticated requests, so 60/min leaves a high ceiling for credential-stuffing from shared egress IPs. Add a separate failed-login sub-limit, or lower this default if brute-force resistance matters here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openrag/api/middleware/rate_limit.py` around lines 44 - 46, The `/auth/*`
rate limit in `RateLimitMiddleware` is too permissive for unauthenticated
traffic keyed by client IP, so tighten it to better resist credential-stuffing.
Adjust the `_auth` bucket in the middleware initialization to a lower default
and/or add a dedicated failed-login limiter for the login path so repeated
failures are throttled more aggressively.

@Ahmath-Gadji
Ahmath-Gadji merged commit b019030 into refactor/hexagonal Jul 1, 2026
6 checks passed
@Ahmath-Gadji
Ahmath-Gadji deleted the feat/rate-limit-admin-bypass branch July 1, 2026 15:00
@Ahmath-Gadji Ahmath-Gadji added the fix Fix issue label Jul 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix Fix issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants