parity(api,apps): security hotfix stage 1 - close unauthenticated write paths

Authorised by rulings D7 and T1-T3 in docs/parity/DECISIONS.md. This is the
CLAUDE.md section 3 carve-out: matching the reference here would keep a money
write path open to anonymous callers.

Closed, each behind the rid the legacy vocabulary already had:
  GET   accounting/customers/:id/balance    -> wallet.list
  POST  accounting/customers/:id/reconcile  -> wallet.charge
  POST  accounting/repayments/gold          -> document.create
  POST  accounting/repayments/rial          -> document.create
  POST  accounting/entries/:id/confirm      -> document.list.confirm
  PATCH orders/:id/status                   -> order.list.sts.submit

Four of the accounting routes WRITE to the ledger. The order-status route was
worse in kind: its DTO takes a client-supplied actorAdminUserId, so an
anonymous caller could move any order and pin it on any admin. It had no
caller - the admin panel posts to admin/orders/status.

GET /customers kept its public status (both apps' login still matches the
phone number in the browser) but now selects only id/mobile/isActive/
isArchived. It previously returned every active customer's full name, national
code, both debts, credit limit and the URLs of their identity-document scans.
CustomerSummary narrowed in BOTH user apps in the same commit, per the
v1/v2 parity rule.

Not changed: /uploads stays public per ruling T2. The filename check requested
alongside it passed unchanged - md5(time + name + uuid), no directory listing.

Tests: apps/api/scripts/auth-smoke.sh asserts 401 without a token, 403 with a
low-privilege token (ut 88, 5 unrelated rids) and pass-through for a super
admin on every closed route, plus that the public routes still answer 200 and
that GET /customers exposes only the four login fields. 23 passed, 0 failed
against a live server. Typecheck clean on api, user-app and user-app-v2.

[ref: henza_server/routes/admin.php:12,272; ApiMobileController.php:39-78]

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
s.esmaeili
2026-09-22 00:43:11 +03:30
co-authored by Claude Opus 5
parent 004a3945c2
commit fdf833565f
11 changed files with 356 additions and 23 deletions
+21 -6
View File
@@ -7,12 +7,27 @@
| Field | Value |
|---|---|
| **Phase** | Phase 0 ✅ passed → **Phase 1 starting** |
| **Branch** | `parity/phase-0` (next: `parity/phase-1-admin`) |
| **Status** | Gate passed. Decisions D0–D12 recorded in `DECISIONS.md`. Urgent traces (percent, @Public, login leak) done and delivered. |
| **Next step** | Show the user the `percent` trace + `@Public` list (done), get approval on (a) protecting the 5 ledger routes and (b) the `percent` schema change → then build `01-ADMIN-MATRIX.md` **field-by-field** (D12) |
| **Blocked on** | user approval for: the 5 `@Public` ledger routes (D7), the `percent` column (D9), the D0 migration file (approval required before running) |
| **Last updated** | 2026-09-21 |
| **Phase** | Security hotfix (out-of-band, authorised by D7/T1–T3) — **stage 1 ✅ done**, stage 2 next |
| **Branch** | `parity/security-hotfix` |
| **Status** | Stage 1 complete and tested live: 23/23 smoke assertions pass. Decisions D0–D12 + T1–T3 recorded. |
| **Next step** | Stage 2 — customer JWT (`typ` claim, separate secret) + real OTP + migrate both apps' login; then remove `GET /customers` from the public surface |
| **Blocked on** | nothing for stage 2. Still pending later: D0 migration + `percent` migration must be **shown and approved before running** |
| **Last updated** | 2026-09-22 |
### Security hotfix — stage 1 result
| Route | Was | Now | Rid |
|---|---|---|---|
| `GET accounting/customers/:id/balance` | public | admin | `wallet.list` |
| `POST accounting/customers/:id/reconcile` | public **write** | admin | `wallet.charge` |
| `POST accounting/repayments/gold` | public **write** | admin | `document.create` |
| `POST accounting/repayments/rial` | public **write** | admin | `document.create` |
| `POST accounting/entries/:id/confirm` | public **write** | admin | `document.list.confirm` |
| `PATCH orders/:id/status` | public **write** | admin | `order.list.sts.submit` |
| `GET customers` | 18 fields incl. national ID, debts, doc URLs | 4 fields | still public until stage 2 |
Test: `apps/api/scripts/auth-smoke.sh` — **23 passed, 0 failed**.
Typecheck: api ✅ · user-app ✅ · user-app-v2 ✅.
## ⚠️ Read `DECISIONS.md` before doing anything