Compare commits
3 Commits
c2135bae2b
...
c808b9ad81
| Author | SHA1 | Date | |
|---|---|---|---|
| c808b9ad81 | |||
| 1491d29b10 | |||
| b7f7b56860 |
@ -38,7 +38,7 @@ cicd-spawn đôi khi trả memory-curation slice ("Compressing S112/S111") thay
|
||||
- **Stage 0 RAG infra:** `Get-Service Qdrant` Running + `http://localhost:6333/healthz`. Collection `proj_solution_erp`.
|
||||
- **Stage 1 Push+filter:** `git log -1 --format='%H %s'` + `git log origin/main..HEAD` empty + diff vs paths-ignore (docs-only → SKIPPED-DOCS return).
|
||||
- **Stage 2 Gitea poll** (max 10 iter × 60s): API `.../actions/tasks?limit=5` (NOT `/runs` 404). Match `head_sha` (NOT run_number — cancelled runs shift numbering). ⚠️ `updated_at` stale ~2min (gotcha #46) → cross-check VPS mtime. Foreground-sleep BLOCKED Windows → Monitor-style busy-wait-curl-spin ~30s/iter.
|
||||
- **Stage 3 Test gate:** baseline **501 PASS** (45 Domain + 456 Infra; S117 #387 `316a82f` — 495+6 PE approved-price-range tests [NEW PeApprovedPriceFinalizeTests + PeFinalizeProjectionTests mod]; Infra 450→456, Domain frozen 45; prior 495 [S116 #386 `88368fd` 486+9 submit-guard/finalize/budget], 486, 477, 458, 440, 436, 434, 421). CI runs both proj BEFORE build/deploy → status=success ⟹ test gate passed (`tasks` terminal=`status:success`, `conclusion` NOT populated — trust success NOT log-numeric). Local grep undercounts (Theory/InlineData). Phase 9 UAT skip OK.
|
||||
- **Stage 3 Test gate:** baseline **505 PASS** (45 Domain + 460 Infra; S118 #388 `c2135ba` — 501+4 `ProcurementMasterAccessSeedTests` scope-isolation; Infra 456→460, Domain frozen 45; prior 501 [S117 #387 `316a82f` PE approved-price-range], 495, 486, 477, 458, 440, 436, 434, 421). CI runs both proj BEFORE build/deploy → status=success ⟹ test gate passed (`tasks` terminal=`status:success`, `conclusion` NOT populated — trust success NOT log-numeric). Local grep undercounts (Theory/InlineData). Phase 9 UAT skip OK.
|
||||
- **Stage 4 Post-deploy** (if SUCCESS): auth login bearer (admin + nv.test gotcha #44; token=`accessToken` route `/api/auth/login`) → 3-5 endpoint smoke 2XX (incl new — NEW-endpoint bodyless-POST returns **411 IIS Length-Required pre-auth NOT 404**; re-probe WITH body → 401 confirms wired) → FE bundle hash 2 app (per #69 = real-size-vs-fake not hash-delta) → SignalR negotiate (gotcha #25) → EF mig prod==repo.
|
||||
- **Stage 4.6 (S29 CRITICAL):** sqlcmd seed sample verify post-deploy (NOT chỉ schema). Seed=runtime-row-insert (no history/tables advance) → verify via DB-query (e.g. Permissions count), NOT bundle-frozen-alone.
|
||||
- Discovery: ASP.NET 10 record enum needs numeric input unless `JsonStringEnumConverter` (SOL has NO converter → FE sends numeric). sqlcmd ssh Windows-auth `\\\\SQLEXPRESS` 4-backslash. INFRASTRUCTURE seed MUST run (NOT inside `if(!demoSeedDisabled)`); DEMO seed gated → gotcha #51.
|
||||
@ -60,6 +60,7 @@ cicd-spawn đôi khi trả memory-curation slice ("Compressing S112/S111") thay
|
||||
- **Mig latest repo: Mig 66 `AddPeApprovedPriceRange`** (`20260714035636`, S117 `316a82f`; single AddColumn `PurchaseEvaluations.ApprovedPriceMaxAmount decimal(18,2) nullable` — cột giá-chốt MAX của "duyệt theo khoảng Min–Max" src ProMinMax; VERIFIED-APPLIED-PROD `COL_LENGTH=9` + history-top `20260714035636_AddPeApprovedPriceRange` + sys.tables **89 giữ** [AddColumn no-new-table]). Prev Mig 65 `BackfillPeQuoteIsSelectedAndWinnerInvariant` / 64 `AddSupplierPublishState` / 62 `ExpandSupplierFields` (see docs/CLAUDE.md migration table). Path `src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/`. Prod check `sqlcmd __EFMigrationsHistory ORDER BY MigrationId DESC`. ⚠️ **`sys.tables`(is_ms_shipped=0)=89** — AddColumn/data-only don't change count; commit touching no new table ⟹ 89 correct, don't FAIL. enum-additive = int NO-mig (history NOT-advance + empty `git diff -- '*Migrations*'`).
|
||||
- **Bundle hash live:** current ship-baseline = S117 #387 (admin js `cFI7ih4a`/css `CfunQHbd` · user js `MxgooVZw`/css `DGqqHQXS`; size 1,656,696/1,563,044b; LM 04:18:21/04:19:16 GMT = 11:18/11:19+07 07-14) — JS-rotate ×2/CSS-frozen from S116 (Dd55jBpj/DVDr1UQ4). See S117 recent-run entry for markers. ⚠️ id≠run_number (head_sha-keyed). PER #69 TWO-MODE hash is SNAPSHOT-per-run NOT stable-baseline (rotate on real-src OR benign-0-src; freeze on 0-src) — ALWAYS re-snapshot LIVE post-success; ship-proof = LM-in-window + real-size (vs 919b SPA-trap) + marker-grep, NOT hash-delta. js-rotate/css-frozen = FE-logic-no-style fingerprint.
|
||||
- **Bearer:** admin `admin@solutions.com.vn/Admin@123456` (full) · UAT `nv.test@solutions.com.vn/TestUser@123456` (Drafter CCM, gotcha #44 check).
|
||||
- **API Serilog log (S118 correction):** real sink = `C:\inetpub\solution-erp\api\logs\solution-erp-YYYYMMDD.log` (lowercase `logs`, rolling daily). ⚠️ skill-runbook path `…\Logs\log-YYYYMMDD.txt` STALE → empty grep. MinimumLevel Default=Information (DbInitializer `LogInformation` seed/revoke lines DO persist) — Microsoft/System overridden Warning (no "Now listening" INFO). **Startup seed-race audit pattern (durability deploys):** grep this log for revoke-line THEN seeder-line same-second → "N added + M upgraded" proves seeder actively flipped rows revoke knocked down (0-upgraded = seeder no-op = FAIL flag).
|
||||
|
||||
## 🔑 Critical config (flag commit nếu tái xuất)
|
||||
Node CI `20.x` (`feedback_node_cicd`) · MediatR `12.4.1` (gotcha #1, flag `Version="14`) · Swashbuckle `6.9.0` (gotcha #2) · act_runner manual checkout (#39) · npm cache DISABLED (#40, flag `cache: npm`)
|
||||
|
||||
@ -15,7 +15,7 @@ WRITE specialist độc quyền `tests/**`. xUnit + FluentAssertions 7.2 + EF SQ
|
||||
- ❌ NOT: production code `src/Backend/**` + `fe-*/**` → test reveal bug → REPORT em main, KHÔNG fix
|
||||
- ❌ NOT: decide WHAT to test (test plan) → em main + reviewer chốt priority
|
||||
|
||||
## 📊 Baseline: 501 tests = 501 PASS (45 Domain + 456 Infra) as of S117 (was 495@S116; +5 PE ProMinMax [own] +1 endsBeforeCeo-pointer-aware [em-main-solo, PeFinalizeProjectionTests], filtered-run 15/15)
|
||||
## 📊 Baseline: 505 tests = 505 PASS (45 Domain + 460 Infra) as of S118 (was 501@S117; +4 Infra Procurement master-access seeder isolation regression, filtered-run 4/4)
|
||||
- Count history + verbose per-session test-design notes (S54→D2): [archive/baseline-history.md](archive/baseline-history.md) (L2, on-demand).
|
||||
- Latest (D2): +6 PE Mig 58 multi-winner — `CreateContractFromEvaluationMultiWinnerTests.cs` (5, 1→N HĐ per IsWinner, GiaTri per-winner=quote-total / single=legacy-budget-sum) + `PeListWinnerNamesProjectionTests.cs` (1, EF collection-projection translation-smoke). No prod bug.
|
||||
> Pattern S67: private-static seed/init → invoke qua REFLECTION (`GetMethod(name, NonPublic|Static)` + `Invoke(null, [db, roleManager, NullLogger.Instance])`); seed MenuItem rows TRƯỚC Permission (FK MenuKey→MenuItem.Key Cascade, SQLite Error 19 nếu thiếu). Cycle-guard test: SqliteDbFixture đủ (no User); rollup-count test cần IdentityFixture.
|
||||
@ -56,6 +56,7 @@ Test theo CODE (single source truth), document mismatch header comment + report.
|
||||
|
||||
## 📅 Recent activity (one-line FIFO — verbose in [archive/baseline-history.md](archive/baseline-history.md))
|
||||
|
||||
- **S118 (2026-07-14) Procurement master-access seeder SCOPE-ISOLATION regression (test-after SECURITY — anh Kiệt FDC) — 501→505 PASS (+4 Infra), no-red, NO prod bug:** new `ProcurementMasterAccessSeedTests.cs` (4 [Fact]) guard seeder MỚI `SeedProcurementMasterAccessAsync` (private static, DbInitializer) — NGOẠI-LỆ role Procurement mở lại Danh mục SAU S92 revoke ("chỉ Admin thấy"). Chain thật: Revoke→SeedProcurement. **T1 upgrade-path** Procurement sau revoke→seed đúng ma trận (Suppliers R+C+U D=false + 8 read-only chỉ R). **T2 ⭐SCOPE-ISOLATION (core)** DeptManager đứng RIÊNG (KHÔNG kèm Procurement): pre-grant→revoke→seed → 9 key VẪN all-false (row tồn tại giữ vết) = grant KHÔNG rò role khác; +CONTRAST assert Procurement THẬT được nâng (chống vacuous-pass). **T3 insert-path** DB mới no-revoke → INSERT 9 row, Delete=false + count==9. **T4 idempotent** seed ×2 → bất biến + count==9 no-dup + role khác vẫn ẩn. Reflection harness MIRROR `AdminOnlyModulesRevokeTests` (NonPublic|Static, sig (ApplicationDbContext,RoleManager<Role>,ILogger), FK seed MenuItem→Role→Permission). No user cần (perm role-level). **Reflection KHÔNG vướng — KHÔNG cần đổi visibility.** KHÔNG touch prod.
|
||||
- **S117 (2026-07-14) PE "duyệt theo KHOẢNG" ProMinMax range-price TEST-BEFORE-MERGE (financial-critical) — 495→500 PASS (+5 Infra, file 10→15), no-red, NO prod bug:** feature anh Kiệt: PRO có cả Min&Max → người duyệt cấp cuối chọn CẢ HAI (source="ProMinMax") → `ApprovedPriceAmount=Min`, `ApprovedPriceMaxAmount=Max`. Bất-biến `Max!=null ⟺ Source=="ProMinMax"`. Prod đã land: `ApplyApprovedPriceOnFinalize(...,decimal? maxAmount=null)` +`ValidApprovedPriceSources`+"ProMinMax"; `TransitionAsync`/`ApproveV2Async` +`approvedPriceMaxAmount` (SAU source). 5 new [Fact] qua reachable terminal path (mirror case-1 SeedSingleApproverWorkflow, KHÔNG gọi private): (a) happy Min100/Max130 set cả 2 · (a-boundary) **min==max OK** lock guard `>` vs `>=` (revert→RED) · (b) thiếu Max→Conflict "*phải có cả giá Min và giá Max*" not-finalized · (c) min>max→Conflict "*không được lớn hơn giá Max*" · (d) nguồn ĐƠN+Max→Conflict "*kèm giá Max*" wire-bug-guard. +regression assert `Max==null` vào Theory 3b (4 nguồn đơn). **⚠️ Gotcha (self-inflicted-avoided):** prod method 5-param nay → `InvokeApply` reflection helper phải pass 5 args (`{...,maxAmount}`); optional param KHÔNG auto-bind qua `MethodInfo.Invoke` → nếu giữ 4 args = `TargetParameterCountException` phá unit-test 4/4b/4c. Đã fix helper. KHÔNG touch prod.
|
||||
- **S116 (2026-07-13) PE báo giá SỐ ÂM regression-lock (test-before cho guard R2-revised — critical-algo/financial) — 486→495 PASS (+9 Infra), no-red, NO prod bug:** feature anh Kiệt cho ThanhTien ÂM (phát-sinh-giảm/hoàn tiền NCC = giá THẬT). BE ĐÃ đổi submit-guard `:202-208` `Sum()<=0`→`Count==0`(chưa chọn)/`else All(x==0)`(chưa có giá). 4 nhóm test khớp hành-vi MỚI: **T1** `PeWorkItemBudgetTests` (+1) quote ÂM −20tr KHÔNG chạm PeWorkItemBudget record (FullAmount ngân sách bất biến) NHƯNG CHẢY vào CurrentProposalTotal=−20tr (chứng tách-biệt cột đề-xuất vs ngân-sách, "0 code" kiến-trúc). **T2** `PeMultiWinnerTests` (+2) winnerQuoteTotal=SUM CÓ DẤU khác-winner(+50,−20=30) + cùng-winner-2-dòng(80,−30=50). **T3** `PeSubmitGuardAndBypassTests` (+4, +2 helper `SeedWinnerWithQuotesAsync` params + `BuildV2PeReadyToSubmitWithQuotesAsync`): all-zero[0,0]→BLOCKED "chưa có giá"; pure-neg[−500k]→ChoDuyet; mixed net>0→ChoDuyet; 🔒 **mixed net==0 từng-dòng≠0 [+500k,−500k]→ChoDuyet** = ranh giới phân-biệt All(==0) vs Sum()<=0 (revert→RED). **T4** `PeCcmThresholdFinalizeTests` (+2, +helper 2-signed-quote): NET threshold — [+1.2tỷ,−0.3tỷ]=0.9tỷ<ngưỡng→CCM finalize no-throw + CONTRAST [+1.2tỷ] alone=1.2tỷ≥ngưỡng→Conflict (dòng âm LOAD-BEARING, R-CEO giữ NET). KHÔNG touch prod.
|
||||
- **S113 (2026-07-12) Supplier import v2 dedup + publish-guard TEST-AFTER (critical-algo + security) — 458→477 PASS (+19 Infra), no-red, 🔴 1 BUG-FOUND (REPORTED em-main, NOT fixed):** 2 new files `Services/SupplierImportV2DedupTests.cs` (13) + `Application/SupplierPublishAndListTests.cs` (6). Existing S112 file UNTOUCHED (re-baked ExpectedHeaderTokens=30 \n-token → real tokens khớp → Case9 tự flip LayoutValid=true). **Dedup precedence** existing-MST→existing-Code→batch-MST→batch-Code→New: strip-ws-normalize MST-match · 2-blank-MST=2-insert (blank không index) · same-MST-batch=1-insert+fill-null-skip · **Code-backstop CI** (S112: dedup ở SERVICE OrdinalIgnoreCase, SQLite BINARY KHÔNG bắt "backstop"≈"BACKSTOP" → xanh=chứng không dựa DB) · **MST→A/Code→B = MST-precedence WINS, no silent-clobber** (FillNulls skip Code → A.Code giữ, B untouched). **Publish** `PublishSupplierCommand` (KHÔNG UpdateSupplier #73): gate D2 Publish+blank-Code→Conflict "Cần Mã NCC" · unique-exact→Conflict "đã tồn tại" (⚠️ `x.Code==newCode` DB-collation, SQLite bắt exact-case only — prod SQL CI bắt CI-dup; asymmetry vs import, observation không bug) · unpublish always. ListSuppliers Published t/f/null · GetSupplier DTO.IsPublic. **🔴 BUG:** `NewSupplier` `Code=r.Code!.Trim()` → NRE khi row.Code==NULL (path R4 nháp: parser sinh null ô rỗng + preview classify New no-error → confirm NRE → 500 CẢ batch dbCount=0). Empty ""=OK (T8/T14). Characterization test CHỐT NRE → fix null-safe thì flip NotThrow+draft.
|
||||
|
||||
@ -1348,6 +1348,14 @@ for h in resp.points: # ← .points không phải iterable trực tiếp
|
||||
|
||||
**Bài học:** derived-invariant phải re-establish ở **MỌI mutation đổi tập-nguồn — kể cả DELETE + cascade-delete**, không chỉ write/select. **Enumeration writer trong spec KHÔNG đáng tin** — grep độc-lập MỌI `.Remove(` chạm entity chứa field-nguồn (S87/S88 cardinality class mở rộng lên tầng mutation-path: writer-miss ≡ read-site-miss). Chỉ **verify adversarial-refute** bắt (invest/review/implement đều sót vì cùng theo enumeration). Liên-quan #72/#73 (cardinality) + `feedback_cardinality_change_grep_consumers`.
|
||||
|
||||
### 82. Menu-flag permission grant ≠ quyền API thật — controller gate GET bằng `[Authorize]` trần (Session 118)
|
||||
|
||||
**Triệu chứng:** S118 phân quyền role Procurement — grant menu `Suppliers` (R+C+U) tưởng "menu hiện = làm được mọi thứ". THỰC TẾ 2 chiều lệch: (1) `PUT/DELETE /suppliers` gate `[Authorize(Roles="Admin,CatalogManager")]` → PRO vẫn **403 khi Sửa/Xóa NCC** dù CanUpdate=1 (chỉ Publish/Import theo policy `Suppliers.Update` là ăn); nút Sửa hiện nhưng bấm lỗi. (2) Ngược lại `ReportsController` chỉ `[Authorize]` **trần (KHÔNG policy)** → menu bị S92 ẩn nhưng **MỌI user đăng nhập** vẫn gọi `GET /reports/dashboard` lấy tổng giá trị HĐ + top NCC/dự án theo giá trị (gõ URL/API trực tiếp, bỏ qua menu).
|
||||
|
||||
**Cơ chế:** Menu-tree (`GetMyMenuTree`) filter theo Permission `CanRead` → CHỈ đổi HIỂN THỊ menu FE. Hiệu-lực API THẬT chỉ ở endpoint mang `[Authorize(Policy="X.Y")]` (map qua `MenuPermissionHandler`). Nhiều controller master gate GET bằng `[Authorize]` trần (mở mọi authed user, chủ ý S59) + gate write bằng `[Authorize(Roles=...)]` (KHÔNG theo menu-key). Nên grant "R/C/U" phần lớn = FE-button-visibility; delta API thật ≪ mong đợi.
|
||||
|
||||
**Fix:** (1) Endpoint lộ data nhạy → gate `[Authorize(Policy="Reports.Read")]` per-method (S118 `dashboard` + `contracts/export`; `my-dashboard` giữ mở vì scope theo `currentUser`). (2) Quy tắc: TRƯỚC khi tin hiệu-lực 1 grant → **grep authz-attribute của CHÍNH controller đích** (`[Authorize(Policy=...)]` vs `[Authorize(Roles=...)]` vs `[Authorize]` trần). **Bài học:** grant permission = 2 tầng ĐỘC-LẬP: menu-display (Permission table) ⟂ API-authz (controller attribute) — verify CẢ HAI. reviewer (adversarial) bắt được vì grep authz thật; em-main-solo tin "menu-flag=quyền" thì sót. Liên-quan #44 (silent 403).
|
||||
|
||||
---
|
||||
|
||||
## Checklist debug bug mới
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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).");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user