[CLAUDE] Docs: S92 closeout — ẩn 5 nhóm menu chỉ Admin (Run #358 PASS prod-verified) + harvest

- Session log 2026-06-29-S92-admin-only-modules.md + STATUS/HANDOFF prepend S92 (demote S91→Prev).
- cicd Run #358 PASS: deploy shipped (dll-mtime in-window 08:00–08:01Z + w3wp recycle 08:04Z),
  smoke 4×200, prod sqlcmd verify (non-Admin CanRead 0 trên 5 nhóm ẩn / 0 leak Contracts·Master·Catalogs
  / Admin giữ 5 key / Pe_* non-Admin 120 untouched). Permission change LIVE.
- Bundle JS rotate admin xz8FdmLN / user CA8Rla2w (CSS frozen) — sửa nhận-định #69-REFINE
  (deploy.yml rebuild FE vô-điều-kiện + rolldown chunk-id non-deterministic → bundle-hash KHÔNG
  đáng-tin làm ship-signal cho BE-only; tín-hiệu thật = dll-mtime + w3wp-recycle).
- Harvest agent-memory: test-specialist (434) + reviewer (S92 topic file + L1 curate 12.9KB) +
  cicd-monitor (Run #358 + prod schema note: bảng Roles/UserRoles KHÔNG prefix AspNetRoles).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-06-29 15:10:43 +07:00
parent 1408d4eb03
commit 396266e5fd
7 changed files with 111 additions and 17 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,61 @@
# 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:0008: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).