---
name: project-s118-procurement-master-access-review
description: S118 review of SeedProcurementMasterAccessAsync (DbInitializer) — PRO role exception to S92 revoke; menu-flag vs role-gated authz reality
metadata:
  type: project
---

# S118 (2026-07-14) Procurement master-access seeder review — DbInitializer.cs +85 LOC

**Verdict: PASS (conditional) — code correct/safe/scope-isolated + build 0/0; 2 MAJOR owner-decisions before commit.**

Change: new `SeedProcurementMasterAccessAsync` runs AFTER `RevokeTemporarilyHiddenModulesAsync` (line 2008→2014), grants role Procurement 11 keys (Suppliers R+C+U, rest Read).

## Clean PASS items (verified)
- Ordering 2008→2014 wins revoke. Scope isolation PERFECT — every query/insert `RoleId==role.Id` (Procurement only); S92 held for all other roles. `.ToDictionary(MenuKey)` safe — `IX_Permissions_RoleId_MenuKey` UNIQUE (Mig `20260421042236`). All 11 MenuKeys exist + in `MenuKeys.All`. Upgrade-only (false→true only), CanDelete never touched. Null-guard OK. Build clean.

## MAJOR (owner sign-off)
- **M1 Reports grant leaks contract financials to PRO:** `Reports.Read` surfaces "Báo cáo" menu → `GET /reports/dashboard` returns `TotalValueActive` + top suppliers/projects BY CONTRACT VALUE + 12-month value trend; `contracts/export` dumps contracts. `ReportsController` = `[Authorize]` ONLY (no policy) → already reachable by any authed user (pre-existing gap), but grant makes it first-class UI for PRO — contradicts S92 "hide contracts/financial from non-admin". Stated goal (suppliers) does NOT need Reports → recommend drop Reports (+maybe Dashboard).
- **M2 `Suppliers.Update` does NOT enable standard edit:** PUT/DELETE `/suppliers/{id}` = `[Authorize(Roles="Admin,CatalogManager")]` (SuppliersController :48/:57) — menu-flag irrelevant → PRO still 403 editing supplier fields. `Suppliers.Update` policy only gates Publish + bulk Import/confirm (:67-101). If "quản lý"=edit, grant insufficient. Blast radius includes mass Import.

## MINOR
- Idempotency claim FALSE: revoke re-hides any non-admin true-flag row every startup → grant re-sets true every startup → 22 writes/boot (not "0 change 2nd run"). End-state correct. Tiny no-access window between the 2 SaveChanges at boot (negligible).
- `Suppliers.Create` menu-flag = API no-op (POST /suppliers has NO policy, open to all authed per S59) — FE button visibility only.
- Recommend 1 scope-isolation regression test (SqliteDbFixture+IdentityFixture exist): DeptManager-only role stays Master/Suppliers=false after seed. Prod-verify acceptable per seeder precedent (Hrm/Off untested) → SHOULD not MUST.

**Lesson: menu-flag grant ≠ API capability — MANY master controllers gate GET with bare `[Authorize]` (open-to-all) + writes with `[Authorize(Roles=...)]`, NOT menu-key policy. So a "Read/Create/Update" permission-row grant is mostly FE-menu-visibility; real API delta = only the endpoints that actually carry `[Authorize(Policy="X.Y")]`. ALWAYS grep the target controllers' actual authz attributes before trusting a permission-grant's intended effect. Reports/Dashboard menu surfaces company financials via unguarded endpoint — "menu-only Read grant" can still leak sensitive aggregates.**
