[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

@ -200,4 +200,71 @@ public class AuthorizePolicyRegressionTests
attr.Should().NotBeNull("POST satellite CreateWorkHistory phải có action-level policy");
attr!.Policy.Should().Be("Hrm_HoSo.Create");
}
// ===================================================================
// S118 (2026-07-14) — Reviewer catch: ReportsController lỗ hổng lộ tài chính toàn công ty.
//
// Trước S118: cả controller chỉ [Authorize] class-level trần → MỌI user đăng nhập gọi được
// GET /reports/dashboard (tổng giá trị HĐ, top NCC/dự án theo giá trị, xu hướng 12 tháng) +
// GET /reports/contracts/export (xuất TOÀN BỘ HĐ ra Excel) → lộ tài chính HĐ toàn công ty.
//
// Fix (owner chốt, khớp S92 "HĐ chỉ Admin thấy"): 2 endpoint company-wide gate Policy
// "Reports.Read" (hiện chỉ Admin có) — mở role khác = cấp qua ma trận, KHÔNG sửa code.
// my-dashboard CỐ Ý giữ mở (scope theo currentUser — mỗi user chỉ thấy số của chính mình).
//
// Regression coverage: nếu ai gỡ Policy khỏi 2 endpoint company-wide → tái lộ tài chính;
// hoặc thêm nhầm Policy vào my-dashboard → khóa dashboard CÁ NHÂN của role không-Admin.
// Cả 2 hướng đều FAIL test ngay, không cần UAT reproduce.
// ===================================================================
[Fact]
public void ReportsController_ClassLevel_AuthorizeOnly_NoPolicy_NoRoles()
{
// Class-level [Authorize] trần (any-authenticated) — gate company-wide đặt PER-ACTION.
// Nếu hardcode Policy/Roles ở class-level sẽ khóa luôn my-dashboard cá nhân (gotcha #44).
var attr = GetClassLevelAuthorize(typeof(ReportsController));
attr.Should().NotBeNull("ReportsController phải có [Authorize] class-level chặn anonymous");
attr!.Policy.Should().BeNull(
"class-level KHÔNG được hardcode Policy — my-dashboard cần mở cho mọi authenticated. " +
"Gate company-wide đặt action-level (Reports.Read).");
attr.Roles.Should().BeNull("class-level KHÔNG được hardcode Roles");
}
[Fact]
public void ReportsController_Dashboard_GET_RequiresReportsReadPolicy()
{
// GET /reports/dashboard = báo cáo TOÀN CÔNG TY → phải gate Reports.Read (chỉ Admin, S118).
var attr = GetActionAuthorize(typeof(ReportsController), nameof(ReportsController.Dashboard));
attr.Should().NotBeNull("GET dashboard (báo cáo toàn công ty) phải có [Authorize(Policy = ...)]");
attr!.Policy.Should().Be("Reports.Read",
"dashboard company-wide (tổng giá trị HĐ, top NCC/dự án, xu hướng 12 tháng) chỉ Admin — " +
"gate Reports.Read (S118 vá lỗ hổng lộ tài chính). Gỡ Policy = tái lộ toàn công ty.");
}
[Fact]
public void ReportsController_ExportContracts_GET_RequiresReportsReadPolicy()
{
// GET /reports/contracts/export = xuất TOÀN BỘ HĐ ra Excel → gate Reports.Read (chỉ Admin, S118).
var attr = GetActionAuthorize(typeof(ReportsController), nameof(ReportsController.ExportContracts));
attr.Should().NotBeNull("GET contracts/export (xuất toàn bộ HĐ) phải có [Authorize(Policy = ...)]");
attr!.Policy.Should().Be("Reports.Read",
"export toàn bộ HĐ ra Excel = nhạy cảm → gate Reports.Read chỉ Admin (S118).");
}
[Fact]
public void ReportsController_MyDashboard_GET_StaysOpen_NoActionPolicy()
{
// GET /reports/my-dashboard CỐ Ý giữ mở (chỉ inherit class-level [Authorize] any-authenticated)
// — scope theo currentUser.UserId, mỗi user chỉ thấy số của CHÍNH MÌNH, không lộ toàn công ty.
// Nếu ai thêm [Authorize(Policy=...)] action-level → khóa nhầm dashboard CÁ NHÂN của mọi role
// không có Reports.Read (403 silent). Cho phép bare [Authorize] (vô hại) nhưng CHẶN mọi Policy.
var attr = GetActionAuthorize(typeof(ReportsController), nameof(ReportsController.MyDashboard));
(attr?.Policy).Should().BeNull(
"GET my-dashboard KHÔNG được gate Policy — mỗi user chỉ thấy số của chính mình. " +
"Thêm policy sẽ khóa dashboard cá nhân của role không có Reports.Read (S118 giữ mở có chủ đích).");
}
}