Skip to content

feat(ENG-27): add rate limiting and account lockout on login#11

Merged
sreenandpk merged 2 commits into
developfrom
feat/ENG-27-rate-limiting-and-lockout
Jul 9, 2026
Merged

feat(ENG-27): add rate limiting and account lockout on login#11
sreenandpk merged 2 commits into
developfrom
feat/ENG-27-rate-limiting-and-lockout

Conversation

@Anjaldev-vk

@Anjaldev-vk Anjaldev-vk commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Added login rate limiting on authentication endpoints to help prevent abusive traffic.
    • Introduced account lockout protection after repeated failed sign-ins.
  • Bug Fixes

    • Successful logins now clear prior failed-attempt counts and remove expired lockouts.
    • Test environments now allow local host access and use safer database setup for automated checks.

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e949f08f-fd4e-4340-8906-5e56b7bd1139

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ENG-27-rate-limiting-and-lockout

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

@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: 2

🧹 Nitpick comments (3)
tests/conftest.py (1)

23-26: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Consider guarding against pointing test DB derivation at a non-local host.

Deriving TEST_DATABASE_URL/admin_url from settings.database_url (previously hardcoded to localhost) means a misconfigured DATABASE_URL env var (e.g. pointing at a shared/staging Postgres) would cause the test suite to silently create/connect to trading_test_db and the postgres admin DB on that remote host instead of failing fast locally.

♻️ Optional safety guard
 db_url = settings.database_url.get_secret_value()
 parsed = urlsplit(db_url)
+if parsed.hostname not in {"localhost", "127.0.0.1", "db", "postgres"}:
+    raise RuntimeError(
+        f"Refusing to run tests against non-local database host: {parsed.hostname!r}"
+    )
 TEST_DATABASE_URL = urlunsplit(parsed._replace(path="/trading_test_db"))
 admin_url = urlunsplit(parsed._replace(path="/postgres"))
🤖 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 `@tests/conftest.py` around lines 23 - 26, The test DB URL derivation in
conftest.py now depends on settings.database_url, so add a safety check before
building TEST_DATABASE_URL and admin_url to ensure the parsed host is local (for
example, localhost/127.0.0.1 or equivalent). If the host is not local, fail fast
with a clear error instead of silently rewriting the path in the
urlsplit/urlunsplit flow; keep the guard close to the db_url parsing logic so
the behavior is enforced wherever the settings-based database URL is used.
tests/test_rate_limiting.py (1)

80-102: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Rate-limit counters aren't reset before this test — assumption of exactly 10 prior hits is fragile.

The shared limiter uses an in-memory store keyed by client address that isn't cleared between tests. This test assumes it starts from a clean count for its key; if the store retains hits from an earlier enabled-limiter run in the same process (test retries, pytest -k, future tests hitting /auth/login with the limiter enabled), the 429 could trigger earlier than request #11, making this test flaky.

♻️ Suggested fix: reset storage before asserting counts
     from ai_trading_discipline_copilot.core.limiter import limiter

     # Temporarily enable the limiter
     limiter.enabled = True
+    limiter.reset()
     try:

Please confirm Limiter/its storage backend in slowapi 0.1.9 exposes a .reset() (or equivalent) to clear counters between test runs.

🤖 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 `@tests/test_rate_limiting.py` around lines 80 - 102, The login rate-limit test
is flaky because it assumes the shared Limiter starts with zero hits for the
client key, but the in-memory counter may persist across tests. Update
test_rate_limiting_on_login to clear or reset the limiter storage before sending
requests, using the Limiter instance from
ai_trading_discipline_copilot.core.limiter (or its backend equivalent) so the
11th request is the first guaranteed 429. If slowapi’s Limiter exposes a
reset/clear method, call it in setup/teardown around the assertions; otherwise
add the appropriate storage reset hook before enabling limiter.enabled.
src/ai_trading_discipline_copilot/routers/auth.py (1)

27-27: 🔒 Security & Privacy | 🔵 Trivial

Per-process rate limiting won't hold under multiple workers.

The shared limiter (core/limiter.py) uses the default in-memory storage (no storage_uri). If the app runs with multiple Uvicorn/Gunicorn workers or is horizontally scaled, each process tracks its own counters, so the effective global rate for /login, /register, /forgot-password, and /resend-verification becomes roughly N× the configured limit for N workers/replicas — weakening the brute-force protection these decorators are meant to provide. Consider a shared backend (e.g. Redis) for Limiter(storage_uri=...) before running with more than one worker in production.

Also applies to: 148-148

🤖 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 `@src/ai_trading_discipline_copilot/routers/auth.py` at line 27, The auth
routes rely on a process-local rate limiter import, so `/login`, `/register`,
`/forgot-password`, and `/resend-verification` are only enforced per worker.
Update the shared `limiter` defined in `core/limiter.py` to use a distributed
storage backend via `Limiter(storage_uri=...)` (for example Redis), and make
sure the decorators in `routers/auth.py` keep using that shared instance so
limits remain global across multiple Uvicorn/Gunicorn workers or replicas.
🤖 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 `@src/ai_trading_discipline_copilot/core/config.py`:
- Around line 53-56: The shared default in Config.allowed_hosts should not
include the test-only host, since it is passed directly into
TrustedHostMiddleware via settings.allowed_hosts in main.py. Remove "testserver"
from the default list in config.py and keep only safe production defaults, then
add "testserver" only in test-specific configuration such as a test .env/CI
override or a direct override in tests/conftest.py.

In `@src/ai_trading_discipline_copilot/services/auth_service.py`:
- Around line 54-85: The failed-login counter update in auth_service is subject
to a lost-update race because `user.failed_login_attempts += 1` is done in
memory before `db.commit()`. Update the lockout path in the authentication flow
to use an atomic database-side increment (for example via an `UPDATE` on the
user record) so concurrent failed attempts cannot overwrite each other. Make
sure the same logic still sets `lockout_until` when
`settings.max_login_attempts` is reached and keep the existing lockout
checks/reset behavior in this auth service method.

---

Nitpick comments:
In `@src/ai_trading_discipline_copilot/routers/auth.py`:
- Line 27: The auth routes rely on a process-local rate limiter import, so
`/login`, `/register`, `/forgot-password`, and `/resend-verification` are only
enforced per worker. Update the shared `limiter` defined in `core/limiter.py` to
use a distributed storage backend via `Limiter(storage_uri=...)` (for example
Redis), and make sure the decorators in `routers/auth.py` keep using that shared
instance so limits remain global across multiple Uvicorn/Gunicorn workers or
replicas.

In `@tests/conftest.py`:
- Around line 23-26: The test DB URL derivation in conftest.py now depends on
settings.database_url, so add a safety check before building TEST_DATABASE_URL
and admin_url to ensure the parsed host is local (for example,
localhost/127.0.0.1 or equivalent). If the host is not local, fail fast with a
clear error instead of silently rewriting the path in the urlsplit/urlunsplit
flow; keep the guard close to the db_url parsing logic so the behavior is
enforced wherever the settings-based database URL is used.

In `@tests/test_rate_limiting.py`:
- Around line 80-102: The login rate-limit test is flaky because it assumes the
shared Limiter starts with zero hits for the client key, but the in-memory
counter may persist across tests. Update test_rate_limiting_on_login to clear or
reset the limiter storage before sending requests, using the Limiter instance
from ai_trading_discipline_copilot.core.limiter (or its backend equivalent) so
the 11th request is the first guaranteed 429. If slowapi’s Limiter exposes a
reset/clear method, call it in setup/teardown around the assertions; otherwise
add the appropriate storage reset hook before enabling limiter.enabled.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d117936d-4c8a-4dd0-bff8-2cbe7a8ae30f

📥 Commits

Reviewing files that changed from the base of the PR and between 3d04a0f and a081440.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • .pre-commit-config.yaml
  • alembic/versions/54a7fb8168af_add_lockout_columns_to_users.py
  • pyproject.toml
  • src/ai_trading_discipline_copilot/core/config.py
  • src/ai_trading_discipline_copilot/core/limiter.py
  • src/ai_trading_discipline_copilot/main.py
  • src/ai_trading_discipline_copilot/models/user.py
  • src/ai_trading_discipline_copilot/routers/auth.py
  • src/ai_trading_discipline_copilot/services/auth_service.py
  • tests/conftest.py
  • tests/test_rate_limiting.py

Comment thread src/ai_trading_discipline_copilot/core/config.py Outdated
Comment thread src/ai_trading_discipline_copilot/services/auth_service.py
@sreenandpk
sreenandpk merged commit 8514a1f into develop Jul 9, 2026
1 check passed
@sreenandpk
sreenandpk deleted the feat/ENG-27-rate-limiting-and-lockout branch July 16, 2026 13:17
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.

2 participants