Files
solution-erp/.claude/skills/permission-matrix/SKILL.md
pqhuy1987 2abbc1d867 [CLAUDE] Docs+Skill: chốt session 6 — 3 skill refresh + 2 rule audit định kỳ
Pure docs work — 0 thay đổi code/test. 77 test vẫn pass (Domain 54 + Infra 23).

3 skill refresh stale (audit định kỳ §6.4 + §9.4 phát hiện):
- form-engine: "Phase 2 MVP missing PDF + form builder" → "Tier 3 feature-complete"
  + bỏ section duplicate "Gen mã HĐ chưa implement" (đã DONE Phase 3+6)
- permission-matrix: 12 menu cũ → ~60 menu key (Bg_*/Pe_*/PeWf_*/Catalogs)
  + inheritance roots 4 group + Budgets KHÔNG inherit (gotcha #35)
- ef-core-migration: "24 DbSet" → "52 bảng (15 migration)"

2 rule mới chốt:
- rules.md §6.4 — Audit + compact MD định kỳ (cadence + checklist + anti-pattern)
  Triết lý: KHÔNG rewrite toàn bộ. Compact + patch drift.
  Cron solution-erp-skill-audit-monthly mở rộng scope (skill + doc drift combined)
- rules.md §9.4 mở rộng cross-ref §6.4

Update STATUS Session 7+ priority + HANDOFF cảnh báo session 7 + migration-todos
Phase 9 Session 6 done sub.

Cron 2026-05-01 fire mai → combined audit theo checklist §6.4 + §9.4.

Session log đầy đủ: docs/changelog/sessions/2026-04-30-chot-session-6-md-audit-compact.md

Commit MD-only → CI skip (path filter gotcha #41).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 01:18:51 +07:00

6.6 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. ~60 menu key (12 root + Ct_*/Wf_*/Pe_*/PeWf_*/Bg_*/Catalogs). FE PermissionGuard + usePermission. BE AuthorizationHandler + ~240 policy. 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 (post Session 6 — 2026-04-30): Phase 1 đợt 2 base + extended qua mọi phase. ~60 menu key total:

  • Core: Dashboard / Master+3 leaves / Forms / Reports / System+Users/Roles/Permissions (12 base)
  • Contracts root + 28 Ct_* (7 type × {Group/List/Create/Pending}) + Workflows root + 7 Wf_*
  • PurchaseEvaluations root + 6 Pe_* (2 type × 3 action) + PeWorkflows root + 2 PeWf_*
  • Budgets root + 3 Bg_* (List/Create/Pending)
  • Catalogs group + 4 leaves (Units/Materials/Services/WorkItems)

Inheritance roots (4 group, gotcha #35): Contracts → Ct_, Workflows → Wf_, PurchaseEvaluations → Pe_, PeWorkflows → PeWf_. Khi thêm root mới có children → PHẢI extend 3 chỗ trong GetMyMenuTreeQuery (xem gotcha #35). Budgets KHÔNG inherit (Bg_* phải grant tay).

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 48 policy {menu}.{action} trong AddAuthorization
  • 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)