# S92 (2026-06-29) — Ẩn 5 nhóm menu → CHỈ Admin thấy trên eoffice

> **Loại:** Security / permission-seed · BE-only (no migration) · em-main-led + reviewer + cicd-monitor · HMW-mode ON · 1 commit prod-verified `1408d4e`.

## Bối cảnh

Anh gõ `/session-start`. Bootstrap phát hiện **WIP "S92" chưa commit** trên đĩa (dated hôm nay 2026-06-29): `DbInitializer.cs` (+32/−10) + 2 seed test sửa comment + `AdminOnlyModulesRevokeTests.cs` (mới) + `test-specialist/MEMORY.md` delta. Đây là việc làm dở của một phiên trước hôm nay, chưa build-verify / commit / deploy / closeout. Anh chốt qua AskUserQuestion: **"Verify + hoàn thiện + deploy"**.

## Nội dung S92 (anh chốt: 5 nhóm menu CHỈ Admin thấy trên user-portal eoffice)

| Nhóm | Menu key | Trạng thái |
|---|---|---|
| Nhân sự | `Hrm*` | S58 đã ẩn — giữ |
| Văn phòng số | `Off*` | S58 đã ẩn — giữ |
| Cá nhân | `Personal` | S58 đã ẩn — giữ |
| **Hợp đồng** | `Contracts` + `Ct_*` | **MỚI S92** |
| **Danh mục** | `Master` · `Suppliers` · `Projects` · `Departments` · `Catalog*` | **MỚI S92** |

**Cơ chế:** menu hiện ⟺ `Permission.CanRead=true` (`GetMyMenuTreeQuery` lọc CanRead, KHÔNG admin-bypass). Revoke set 4 cờ CRUD=false cho MỌI role non-Admin trên key match predicate → menu tự ẩn. Admin loại trừ khỏi revoke → vẫn thấy đủ.

**3 thay đổi `DbInitializer.cs`:**
1. `RevokeTemporarilyHiddenModulesAsync` (`:2125`) — predicate `.Where()` mở rộng `+Contracts +StartsWith("Ct_") +Master +Suppliers +Projects +Departments +StartsWith("Catalog")` (cộng dồn S58).
2. TẮT (comment) 2 re-grant public chạy-sau-revoke: `SeedAllRolesHrmProfileReadPermissionsAsync` (S65, `:2003`) + `SeedAllRolesOfficeModulePermissionsAsync` (S69, `:2012`). 2 method giữ `private static` cho reversibility.
3. Thu hẹp `InReviewScope` → **chỉ `Pe_*`** (bỏ Master/Catalog/Suppliers/Projects/Departments khỏi grant-all-roles; row cũ trên prod được revoke gỡ).

**Invariant bảo vệ tính năng đang dùng:** `Pe_*` (Duyệt NCC) KHÔNG match predicate → module PE giữ nguyên cho user thường. Master-data GET = `any-auth` (controller class-`[Authorize]`) → form PE vẫn chọn Dự án/NCC/Hạng mục dù menu Danh mục ẩn.

## Verify chain (security change → test-before-merge + independent review)

1. **Full suite: 434 PASS** (45 Domain + 389 Infra, 0 fail/skip) — `+3` = `AdminOnlyModulesRevokeTests` (2 seed test cũ chỉ +comment).
2. **Seed-chain order (load-bearing, gotcha #75):** mọi permission-write nằm trong `SeedAdminPermissionsAsync` (gọi tại `SeedAsync:110`, **ungated**). Trong đó: grants `:1975`→`:1986` → **revoke `:1993` = bước permission CUỐI** (2 re-grant sau đã comment). `SeedAsync` sau `:110` không đụng Permissions → revoke thắng toàn cục. Prod `DemoSeed:Disabled=true` nhưng `SeedAdminPermissionsAsync` ungated → revoke re-chạy mỗi restart → ẩn BỀN.
3. **Predicate coverage:** enumerate `MenuKeys.cs` + `SeedMenuTreeAsync` — 5 nhóm phủ đủ; `Forms`/`Reports`/`Dashboard` là sibling top-level (parent=null) → giữ hiện đúng (ngoài scope); không over-match; `PurchaseEvaluations` là top-level (parent=null) dù label "Duyệt NCC" → PE preserved.
4. **Reviewer PASS** (adversarial, 5 attack vector A-E độc-lập re-derive; 15/15 trên 3 class; category 1-5 PASS). 0 blocking.

## FYI — CatalogManager (intended-by-spec, 0 live impact)

`SeedCatalogManagerPermissionsAsync` (`:1980`) cấp Danh-mục CRUD cho role `CatalogManager`, chạy **TRƯỚC** revoke (`:1993`) → revoke (mới) cũng thu Danh-mục của CatalogManager. Role này hiện **0 user** (S89 xóa hết test user) → **không vỡ gì hôm nay**. Theo lệnh "Danh mục CHỈ Admin" = đúng ý. Nếu sau này anh muốn 1 user non-admin quản lý danh mục → cần carve-out predicate (gỡ prefix + uncomment, code comment đã ghi). Re-grant qua UI Permission-matrix chỉ giữ tới restart kế (revoke thắng).

## Deploy

- Commit `1408d4e` `[CLAUDE] Infra: ẩn 5 nhóm menu ... chỉ Admin thấy (S92)` — 4 file (DbInitializer + 3 test).
- Push `f98a743..1408d4e` → Gitea Actions (test gate 434 → build → deploy).
- **cicd Run #358 PASS** (~5m24s) — test gate (434) → build → deploy. Smoke 4×200 (api ready/live + admin + eoffice).
- **BE-only ship-proof (gotcha #69):** bundle hash KHÔNG phải tín-hiệu ship đáng-tin — JS **rotate** admin `DgQyuG7f`→`xz8FdmLN` / user `BmbQon23`→`CA8Rla2w` (CSS frozen) DÙ 0 fe-src đổi (`deploy.yml` rebuild FE vô-điều-kiện + rolldown chunk-id non-deterministic → **sửa nhận-định #69-REFINE "same-hash deterministic"**). Tín-hiệu ship THẬT = `Infrastructure.dll`/`Api.dll`/`Application.dll` mtime in-window (08:00–08:01Z) + w3wp PID recycle 08:04:14Z.
- **Hiệu lực permission PROD-VERIFIED (sqlcmd):** revoke đã flip row prod tại pool-recycle 08:04:14Z (KHÔNG cần restart tay) — non-Admin CanRead=1 trên 5 nhóm ẩn = **0** · leak S92-new (Contracts/Master/Catalogs) = **0** · Admin giữ 5 key · Pe_* non-Admin untouched = **120** (12 role). Permission change **LIVE** trên prod.

## State (sau S92)

**Mig 59 · 88 bảng · 434 test (45D+389I) · gotcha 76 · menu 54 · bundle admin `xz8FdmLN`/user `CA8Rla2w` (JS rotate #69, CSS frozen; Run #358) · prod 17 user/7 NCC.** KHÔNG migration, KHÔNG bảng/menu mới.

## Lessons

- **Seed-chain order = invariant load-bearing** (gotcha #75): thay đổi permission-visibility phải đảm bảo revoke là bước CUỐI + ungated (chạy mỗi restart prod) thì mới BỀN. S92 tắt 2 re-grant sau revoke + giữ revoke ungated → đúng pattern S91 seed-gate.
- **Cardinality/scope change → grep MỌI nhánh** (`feedback_cardinality_change_grep_consumers`): mở rộng predicate revoke = phải enumerate toàn bộ menu key của nhóm (MenuKeys + SeedMenuTree) để không under-revoke leak; reviewer xác nhận `Forms`/`Reports`/`PurchaseEvaluations` là sibling top-level (không lọt/không vỡ).
- **Reviewer lượng-hoá blast-radius**: thay vì chặn vì "CatalogManager có thể mất quyền", reviewer xác định **0 user** giữ role đó → biến lo-ngại thành FYI, deploy có cơ sở.

## NEXT

- **Anh:** UAT eoffice bằng 1 user non-admin (Ctrl+F5) → xác nhận KHÔNG còn thấy Hợp đồng / Danh mục / Nhân sự / Văn phòng số / Cá nhân (chỉ còn Duyệt NCC + Dashboard). ⚠️ restart CLI (carry: budget v3 + `/user-mark-*` + cadence chưa hot-reload).
- **Em:** monthly audit **2026-07-01** (2 ngày) — re-tier STATUS/HANDOFF mega-line (bloat: Read-truncate + detector mis-parse canonical) · flush ef-core SKILL/README Mig→59 · docs/CLAUDE Budget-removed · 3 vocab-fork alias-map.
- **Pending product (carry):** Section B cột Dự-án wire · Ngưỡng giá CEO (Mig 54) Designer UAT · "C" chuyển phiếu→dự án (chờ spec).
