Files
solution-erp/.claude/skills/permission-matrix/SKILL.md
pqhuy1987 a09dcae999 [CLAUDE] Skill: W4 positive-control - detector FIRE 5 -> va -> 0 (TOTAL 50->45, tut dung 5)
CHUOI BAT BUOC (v2 §3.3 + §2.4(4)): fire -> chung-kien -> ROI MOI va -> het fire.
CAM va permission-matrix TRUOC = mat chinh bang-chung detector co rang.

TRUOC (capture ra runs/<id>/W4-detector-fire-15-07-2026.md muc 1-5, fix#9c time-anchored):
  MED :3  writes 60 menu  but canonical=54
  MED :3  writes 240 policy but canonical=216
  MED :16 writes 60 menu  but canonical=54
  LOW :81 writes 48 policy but canonical=216
  LOW :16 title-stale anchor 2026-04-30 vs 2026-07-15 (76d)
  TOTAL FLAGS: 50  <- khop CHINH XAC du-bao W2 (WAL:17 "49->50, -1 cadence +2 policy")

SAU: permission-matrix 0 FLAG | TOTAL 45 | tut dung 5 | 0 flag moi (do bang `comm`, khong dem mat).

CACH VA = B1 bo so + tro canonical, KHONG doi so (doi so thi lan sau lai stale).
Dung dung loi khuyen detector tu in: "OR replace with pointer '-> docs/STATUS.md'".

CHUOI NHAN-QUA W1<->W2 chung duoc 3 detector doi trang-thai DUNG thiet-ke:
- h24_cadence: "missing => measuring NOTHING" -> "M = light_every = 6 (read from config)"
- spawn-model-audit: "expected constant missing" -> "[OK] claude-opus-4-8 = owner-ratified"
- GAP-3 policy: "policy=MISSING" -> "216" + BAT NGAY permission-matrix:3
  => truoc W2, nua policy KHONG THE fire vi STATUS khong co row Policies => detector IM,
     va su im-lang do trong y HET nhu "sach". Detector khong co nguon chuan = do NOTHING.

STALE NANG HON CON-SO (tim duoc luc va, detector count-token KHONG bat duoc):
  SKILL.md:20 liet "Budgets root + 3 Bg_*" nhu menu DANG SONG.
  Do dia: module Budget XOA tu S61 (Mig 50); 2 hit Bg_ con lai = COMMENT BIA-MO
  (MenuKeys.cs:70 + fe-{admin,user}/src/lib/menuKeys.ts:29). Code sach, chi skill con mo-ta.
  => gioi-han detector dem-so: thay "60 != 54" nhung khong thay "4 trong 60 do da bi xoa 60 phien truoc".

DUONG-GIA CAU-TRUC (khai, KHONG sua -- doi hanh-vi detector = quyen anh/W5):
  lead-view-auditor.md:45 FIRE title-stale vi bang vi-du cua no chua "2026-04-30" lam MAU
  minh-hoa cho class view-stale-header. Cung CO-CHE voi loi da lam acceptance FIX#6 bat-kha-thi
  (:47 chua "12 sub ... roster = 14" lam vi-du). 2 detector DOC-LAP, cung 1 file, cung 1 ly-do:
  VI-DU VE STALE TRONG Y HET STALE. File dinh-nghia anti-pattern tat-yeu chua mau anti-pattern.
  De-xuat (khong tu lam): detector can co-che mien-tru tuong-minh cho khoi vi-du.

LEAD TU BAT 2 LOI @W4:
(a) WAL ghi "permission-matrix 6 dong" -- that la 5 (dong :70 = context, khong phai flag).
    Loi dem lan thu 3 trong phien; lan nay bat TRUOC khi thanh claim gui hub.
(b) Suyt claim au canonical: grep tho MenuKeys.cs ra 64 chuoi -> SAI (dem MOI chuoi trong file,
    khong phai phan-tu mang All). W2 ground dung: |MenuKeys.All|=54 (:147) x |Actions|=4 = 216.
    Lead BO so cua minh, dung so W2. Bai hoc: do bang cong-cu sai con te hon khong do --
    vi no ra mot con-so TRONG CO VE do duoc.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 15:08:09 +07:00

7.7 KiB
Raw Blame History

name, description, when-to-use
name description when-to-use
permission-matrix Hệ thống phân quyền Role × MenuKey × CRUD (12 root + Ct_*/Wf_*/Pe_*/PeWf_*/Catalogs — số menu-key · policy canonical → docs/STATUS.md, KHÔNG chép số ở đây). FE PermissionGuard + usePermission. BE AuthorizationHandler + policy `{menu}.{action}`. Dùng khi debug access denied, gán role, menu không hiện, inheritance không work.
permission denied
access denied
menu không hiện
gán role cho user
seed permission
permission matrix edit
menu inheritance không work

Permission Matrix Skill

Status (cập-nhật 2026-07-15 — S122 W4): base Phase 1 đợt 2 + extended qua mọi phase. 🔴 Số menu-key · số policy = canonical ở docs/STATUS.md (row Menu keys / Policies) — KHÔNG chép số vào file này (B1). Policy = DERIVED: |MenuKeys.All| × |Actions| (Api/Program.cs) ⇒ đổi menu BUỘC đổi cả 2 row cùng lúc. 🔍 Đếm THẬT = đọc mã, đừng tin doc: MenuKeys.Allsrc/Backend/SolutionErp.Domain/Identity/MenuKeys.cs:147.

Nhóm menu (hình-dạng, KHÔNG phải con-số):

  • Core: Dashboard / Master+3 leaves / Forms / Reports / System+Users/Roles/Permissions
  • Contracts root + Ct_* (7 type × {Group/List/Create/Pending}) + Workflows root + Wf_*
  • PurchaseEvaluations root + Pe_* (2 type × 3 action) + PeWorkflows root + PeWf_*
  • Catalogs group + 4 leaves (Units/Materials/Services/WorkItems)
  • Office/HRM/… — xem MenuKeys.cs (mã là nguồn)
  • 🧊 Budgets root + Bg_*XOÁ S61 (Mig 50), module Budget cũ thay bằng PeWorkItemBudgets (ngân-sách per-gói-thầu). Bia-mộ: MenuKeys.cs:70 + fe-{admin,user}/src/lib/menuKeys.ts:29. (Skill này liệt Bg_* như menu ĐANG SỐNG suốt từ S61 → S122 — stale nặng hơn lệch con-số vì nó mô-tả thứ không tồn tại; vá @S122 W4.)

Inheritance roots (4 group, gotcha #35): ContractsCt_*, WorkflowsWf_*, PurchaseEvaluationsPe_*, PeWorkflowsPeWf_*. Thêm root mới có children → PHẢI extend 3 chỗ trong GetMyMenuTreeQuery (gotcha #35). (🧊 câu cũ "Budgets KHÔNG inherit (Bg_ phải grant tay)" — gỡ theo module.)*

Model

User ────< UserRoles ────< Role ────< Permissions ────< MenuItem
                                       (RoleId, MenuKey, CRUD flags)
  • 1 User có N Role (qua AspNetUserRoles rename → UserRoles)
  • 1 Role có N Permission (1 row per MenuKey × 4 CRUD flag)
  • Union (OR) nhiều role → user có quyền nếu bất kỳ role nào cho quyền đó
  • Admin role → bypass check (luôn pass mọi policy)

Menu tree (seed — ~60 key sau Phase 8)

Dashboard
Master
  ├── Suppliers
  ├── Projects
  ├── Departments
  └── Catalogs (group)
       ├── UnitsOfMeasure
       ├── MaterialItems
       ├── ServiceItems
       └── WorkItems
Contracts (root inherit)
  └── Ct_<Code>_<Group|List|Create|Pending>   × 7 type = 28 leaf
Forms
PurchaseEvaluations (root inherit)
  └── Pe_<Code>_<List|Create|Pending>          × 2 type = 6 leaf
Budgets (root, NO inherit — grant tay)
  ├── Bg_List
  ├── Bg_Create
  └── Bg_Pending
Reports
System
  ├── Users
  ├── Roles
  ├── Permissions
  ├── Workflows (root inherit)
  │    └── Wf_<Code>                           × 7 type = 7 leaf
  └── PeWorkflows (root inherit)
       └── PeWf_<Code>                         × 2 type = 2 leaf

Tree hierarchy qua ParentKey field. Seed trong DbInitializer.SeedMenuTreeAsync + Pe/Wf/Bg seeders riêng.

Code pointers

Backend:

  • Domain/Identity/MenuKeys.cs — const class, single source of truth
  • Domain/Identity/MenuItem.cs — entity (Key PK, Label, ParentKey, Order, Icon)
  • Domain/Identity/Permission.cs — entity (RoleId, MenuKey, 4 flag)
  • Application/Permissions/Queries/GetMyMenuTree/GetMyMenuTreeQuery.cs — resolve per-user, union OR, filter tree
  • Application/Permissions/PermissionFeatures.cs — list/upsert
  • Api/Authorization/MenuPermissionRequirement.cs + MenuPermissionHandler.cs — policy check
  • Api/Program.cs — register policy {menu}.{action} trong AddAuthorization (số = |MenuKeys.All| × |Actions| DERIVED; canonical → docs/STATUS.md row Policies — KHÔNG hardcode ở đây)
  • Infrastructure/Persistence/DbInitializer.csSeedMenuTreeAsync + SeedAdminPermissionsAsync
  • Api/Controllers/MenusController.cs, RolesController.cs, PermissionsController.cs

Frontend (fe-admin):

  • src/lib/menuKeys.ts — const mirror, cần đồng bộ tay với BE
  • src/types/menu.ts — MenuNode type
  • src/hooks/usePermission.tscan(menuKey, action) helper
  • src/components/PermissionGuard.tsx — wrap button/content
  • src/components/Layout.tsx — render sidebar động từ AuthContext.menu
  • src/pages/system/PermissionsPage.tsx — ma trận edit UI
  • src/contexts/AuthContext.tsxloadMenu() on login + localStorage cache

BE policy usage

Register trong Program.cs:

services.AddAuthorization(opts =>
{
    foreach (var menu in MenuKeys.All)
        foreach (var action in MenuKeys.Actions)
            opts.AddPolicy($"{menu}.{action}", p =>
                p.Requirements.Add(new MenuPermissionRequirement(menu, action)));
});
services.AddScoped<IAuthorizationHandler, MenuPermissionHandler>();

Apply ở controller:

[HttpPut("{id:guid}")]
[Authorize(Policy = "Contracts.Update")]
public async Task<IActionResult> Update(...) { }

FE guard usage

// Hook
const { can } = usePermission()
if (!can('Contracts', 'Update')) return null

// Component wrap
<PermissionGuard menuKey="Contracts" action="Update">
  <Button>Sửa</Button>
</PermissionGuard>

// Route guard
<Route
  path="/system/permissions"
  element={
    <PermissionGuard menuKey="Permissions" action="Read" fallback={<Forbidden />}>
      <PermissionsPage />
    </PermissionGuard>
  }
/>

Workflow — gán quyền cho role mới

  1. Admin login → /system/permissions
  2. Chọn role (vd "CostControl")
  3. Tick checkbox trên matrix grid — mỗi lần tick tự động PUT /api/permissions upsert
  4. User thuộc role đó logout/login lại → thấy permission mới (menu refresh từ /api/menus/me)

Guard rules đã implement

  • Admin bypass: role Admin luôn pass mọi policy (kể cả chưa seed row Permission)
  • Not user active: User.IsActive=false → AuthorizationHandler return fail
  • Self-demote protection: admin đang edit không thể giảm quyền role Admin (check trong UpsertPermissionCommandHandler)

Common pitfalls

  • Quên refresh menu sau update permission → user thấy menu cũ. Giải pháp: logout/login, hoặc Phase 3 thêm SignalR push.
  • MenuKey typo — TS không check vì menu.key là string. Luôn dùng MenuKeys.Contracts const, không hardcode "Contracts".
  • FE cache menu trong localStorage → sau user được assign role mới, FE thấy menu cũ. Login lại fix.
  • Hai role conflict (1 cho, 1 cấm): union OR → có ít nhất 1 role cho là được.
  • 403 ở API nhưng FE không hide button → FE guard chỉ UX, BE phải là source of truth. Phải apply [Authorize(Policy = "X.Y")] ở controller.

Phase tiếp theo

  • Phase 3: SignalR notify khi permission đổi → FE tự refetch /api/menus/me
  • Phase 4: Per-user override (ngoài role) — thêm bảng UserPermissionOverrides
  • Phase 4: Invalidate JWT khi role đổi (rare event, nhưng secure)