Skip to content

Fix orders 401/403 conflation, minor rate-limit gaps, stale mockClient docs - #20

Merged
Badri-Rajendran merged 1 commit into
mainfrom
fix_orders_401_and_stale_docs
Sep 7, 2026
Merged

Badri-Rajendran merged 1 commit into
mainfrom
fix_orders_401_and_stale_docs

Conversation

@Badri-Rajendran

Copy link
Copy Markdown
Owner

Problem

An independent QA pass (extensive live API testing against the running local backend) found orders.py conflating "no session" with "wrong role" on all four routes:

if not current_user.is_authenticated or current_user.role != "customer":
    raise ForbiddenError("Only a customer may place an order")

GET /api/v1/orders with no cookie at all returned 403 {"code":"forbidden"} instead of 401. Every other blueprint gets this right — portfolios.py's _authorize_customer_id is the reference pattern already used correctly elsewhere in the codebase. A frontend that keys "session expired → re-login" off 401 would never trigger re-auth on any orders screen.

Two smaller gaps surfaced by the same pass, unrelated but cheap to fix alongside:

  • POST /auth/logout carried no rate limit, unlike every sibling auth route.
  • RateLimitedError's bare code="rate_limited" is never what a flask-limiter throttle actually puts on the wire (that path goes through the generic HTTPException handler and emits too_many_requests instead) — misleading to a future reader, though the class itself is genuinely used elsewhere (chat.py's daily query cap, always with its own code= override).

Also: two frontend comments and one README bullet claimed the /lots, /admin/customers*, /admin/kyc-overrides/<id>, and /statements/<period>/export backend routes "don't exist yet" — false, all four are live, registered, and covered by the 799 backend tests. The frontend genuinely hasn't been wired to them yet (still on mockClient, honestly labelled "Simulated" in the UI per docs/requirements/real-vs-simulated-rules.md — that disclosure is correct and unchanged), but the routes exist.

Implementation

  • Split each of the four orders.py checks into two: UnauthenticatedError (401) when there's no session, ForbiddenError (403) only once a session exists but the role is wrong.
  • POST /auth/logout gets @limiter.limit("60 per minute"), matching /session's own limit.
  • Added a comment on RateLimitedError explaining the two 429 paths, rather than removing anything (deleting the class would break chat.py's real usage).
  • Corrected the three stale "doesn't exist yet" claims (two frontend comments, one README bullet) to reflect that the backend routes exist and are tested.

Validation

  • uv run pytest tests/api/test_orders.py -q → 18/18 passed, including an updated assertion (test_list_orders_with_no_session_is_rejected previously asserted the wrong 403) and a new test (test_get_order_with_no_session_is_rejected, untested for this exact case before).
  • Full backend suite run twice — 799 passed, 7 skipped, 2 xpassed both times, no flakes.
  • ruff check and mypy --strict clean on every touched file (2 pre-existing, unrelated mypy errors remain elsewhere — confirmed present before this change).
  • Live-verified against the running local backend: GET /api/v1/orders with no cookie now returns 401 {"code":"unauthenticated"}.
  • Frontend: comment-only changes, npm run lint and npm run build clean.

Security / DB / API impact

  • Security: none negative — this narrows the response to the correct status code for an existing, already-enforced check; no new access is granted or denied that wasn't already.
  • DB: none.
  • API: the four order routes now return 401 instead of 403 for a fully unauthenticated request. Any client correctly distinguishing 401 (re-authenticate) from 403 (forbidden) benefits; nothing currently in this codebase relied on the wrong code.

🤖 Generated with Claude Code

…mit gaps

QA pass found orders.py conflating "no session" with "wrong role" on all
four routes -- e.g. create_order's `if not current_user.is_authenticated
or current_user.role != "customer": raise ForbiddenError(...)` raised
403 for both cases. Every other blueprint (portfolios.py's own
_authorize_customer_id is the reference pattern) correctly returns 401
for no session and 403 only once a session exists with the wrong role.
A frontend that keys "session expired -> re-login" off 401 wouldn't
trigger re-auth on any orders screen.

Split each of the four checks into two: UnauthenticatedError first (401)
when there's no session at all, ForbiddenError after (403) only once a
session exists but the role is wrong. list_orders/get_order already had
the checks in separate if-statements but raised the wrong class on the
first one; create_order/approve_order had them combined in one boolean
and needed splitting. Updated test_list_orders_with_no_session_is_rejected
(previously asserted the wrong 403) and added
test_get_order_with_no_session_is_rejected (untested for this exact case
before).

Two smaller gaps from the same pass:
- POST /auth/logout carried no rate limit, unlike every sibling auth
  route. Added 60/min, matching /session's own limit -- logout has no
  meaningful abuse surface, but consistency with the stated per-endpoint
  throttling policy matters.
- RateLimitedError's bare `code="rate_limited"` is never what a
  flask-limiter throttle actually puts on the wire (that path goes
  through the generic HTTPException handler and emits
  "too_many_requests" instead) -- the class itself is genuinely used
  (chat.py's daily query cap, always with its own code= override), just
  its default was flagged as misleading. Added a comment explaining the
  two paths rather than removing anything, since deleting the class
  would break chat.py's real usage.

Also: two frontend comments and one README bullet claimed the /lots,
/admin/customers*, /admin/kyc-overrides/<id>, and
/statements/<period>/export backend routes "don't exist yet" -- false;
all four are live, registered, and covered by the 799 backend tests.
The frontend genuinely hasn't been wired to them (still on mockClient,
honestly labelled "Simulated" in the UI per
docs/requirements/real-vs-simulated-rules.md -- that disclosure is
correct and unchanged), but the routes exist. Corrected all three
claims to say so.

Verified: full backend suite (uv run pytest) run twice -- 799 passed,
7 skipped, 2 xpassed both times, no flakes. ruff and mypy --strict clean
on every touched file (2 pre-existing, unrelated mypy errors remain
elsewhere, confirmed not introduced by this change). Live-verified the
401 fix against the running local backend: GET /api/v1/orders with no
cookie now returns 401 {"code":"unauthenticated"}. Frontend lint and
build clean (comment-only changes, no behavior touched).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 7, 2026 00:41

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@Badri-Rajendran
Badri-Rajendran merged commit 6cd8e7c into main Sep 7, 2026
6 checks passed
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