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:
co-authored by
Claude Opus 5
parent
004a3945c2
commit
fdf833565f
@@ -2,34 +2,53 @@ import { Body, Controller, Get, Param, Post } from '@nestjs/common';
|
||||
import { LedgerService } from './ledger.service';
|
||||
import { PostGoldRepaymentDto } from './dto/post-gold-repayment.dto';
|
||||
import { PostRialRepaymentDto } from './dto/post-rial-repayment.dto';
|
||||
import { Public } from '../auth/guards';
|
||||
import { RequirePermission } from '../auth/guards';
|
||||
|
||||
@Public() // pre-existing unauthenticated endpoint; see Phase 1 notes
|
||||
/**
|
||||
* Ledger operations — admin only.
|
||||
*
|
||||
* These were `@Public()` until the security hotfix. That was faithful to the
|
||||
* reference, where the whole admin route file is unauthenticated (the
|
||||
* `auth:api` group wrapper is commented out at henza_server/routes/admin.php:12
|
||||
* and :272), but it contradicted decision D1 in docs/parity/DECISIONS.md, which
|
||||
* confirms the target keeps its stricter auth. Four of these five routes WRITE
|
||||
* to the ledger, so an anonymous caller could reconcile a balance or post and
|
||||
* confirm a repayment.
|
||||
*
|
||||
* Rids follow the legacy vocabulary: reading a balance is a wallet read,
|
||||
* repayments are financial documents, and confirming an entry is the existing
|
||||
* document-confirm permission.
|
||||
*/
|
||||
@Controller('accounting')
|
||||
export class AccountingController {
|
||||
constructor(private readonly ledger: LedgerService) {}
|
||||
|
||||
@Get('customers/:customerId/balance')
|
||||
@RequirePermission('wallet.list')
|
||||
getBalance(@Param('customerId') customerId: string) {
|
||||
return this.ledger.getBalance(customerId);
|
||||
}
|
||||
|
||||
@Post('customers/:customerId/reconcile')
|
||||
@RequirePermission('wallet.charge')
|
||||
reconcile(@Param('customerId') customerId: string) {
|
||||
return this.ledger.reconcileBalance(customerId);
|
||||
}
|
||||
|
||||
@Post('repayments/gold')
|
||||
@RequirePermission('document.create')
|
||||
postGoldRepayment(@Body() dto: PostGoldRepaymentDto) {
|
||||
return this.ledger.postGoldRepayment(dto);
|
||||
}
|
||||
|
||||
@Post('repayments/rial')
|
||||
@RequirePermission('document.create')
|
||||
postRialRepayment(@Body() dto: PostRialRepaymentDto) {
|
||||
return this.ledger.postRialRepayment(dto);
|
||||
}
|
||||
|
||||
@Post('entries/:entryId/confirm')
|
||||
@RequirePermission('document.list.confirm')
|
||||
confirmEntry(@Param('entryId') entryId: string) {
|
||||
return this.ledger.confirmEntry(entryId);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user