[CLAUDE] Auth: gate ReportsController tai-chinh HD behind Reports.Read policy (reviewer S118)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 5m34s

Lo hong (reviewer S118 phat hien khi phan quyen PRO tra.bui): ReportsController chi co
[Authorize] tran (khong policy) -> MOI user dang nhap goi duoc GET /reports/dashboard (tong gia
tri HD dang chay, top NCC/du an theo gia tri, xu huong 12 thang) + GET /reports/contracts/export
(xuat toan bo HD ra Excel). Nguoc quyet dinh S92 "HD chi Admin thay" (menu an nhung API van mo,
go URL/API truc tiep lay duoc).

Fix (owner chot: chi Admin qua Reports.Read policy -- linh hoat mo role khac qua ma tran sau):
- Dashboard() + ExportContracts() -> them [Authorize(Policy = "Reports.Read")] per-method. Hien
  chi Admin co Reports.Read -> non-admin 403. Admin bypass moi policy nhu cu.
- MyDashboard() GIU MO (chi [Authorize] class-level) -- scope theo currentUser, moi user chi thay
  so cua CHINH MINH (draft/pending/SLA/gia tri draft cua minh), khong lo toan cong ty.

Test: +4 reflection authz regression (AuthorizePolicyRegressionTests, mirror gotcha #44 S45):
dashboard/export co Reports.Read; my-dashboard KHONG co action-policy (guard chong ai khoa nham
dashboard ca nhan). 505 -> 509. Gotcha #82 (menu-flag grant != quyen API that).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-07-14 15:55:18 +07:00
parent 1491d29b10
commit c808b9ad81
3 changed files with 84 additions and 0 deletions

View File

@ -14,15 +14,24 @@ namespace SolutionErp.Api.Controllers;
[Authorize]
public class ReportsController(IMediator mediator) : ControllerBase
{
// [S118 — reviewer catch] Báo cáo TOÀN CÔNG TY (tổng giá trị HĐ, top NCC/dự án theo giá trị,
// xu hướng 12 tháng) = nhạy cảm → gate Reports.Read (owner chốt: chỉ Admin, khớp S92 "HĐ chỉ
// Admin thấy"). Hiện chỉ Admin có Reports.Read → non-admin 403. Mở cho role khác = cấp Reports.Read
// qua ma trận (không sửa code). Trước S118: chỉ [Authorize] trần → mọi user đăng nhập gọi được.
[HttpGet("dashboard")]
[Authorize(Policy = "Reports.Read")]
public async Task<ActionResult<DashboardStatsDto>> Dashboard(CancellationToken ct)
=> Ok(await mediator.Send(new GetDashboardStatsQuery(), ct));
// GIỮ MỞ (chỉ [Authorize] class-level): my-dashboard scope theo currentUser.UserId — mỗi user
// chỉ thấy số của CHÍNH MÌNH (draft/pending/SLA/giá trị draft của mình). Không lộ toàn công ty.
[HttpGet("my-dashboard")]
public async Task<ActionResult<MyDashboardDto>> MyDashboard(CancellationToken ct)
=> Ok(await mediator.Send(new GetMyDashboardQuery(), ct));
// [S118 — reviewer catch] Xuất TOÀN BỘ HĐ ra Excel = nhạy cảm → gate Reports.Read (chỉ Admin, S92).
[HttpGet("contracts/export")]
[Authorize(Policy = "Reports.Read")]
public async Task<IActionResult> ExportContracts(
[FromQuery] ContractPhase? phase,
[FromQuery] Guid? supplierId,