Files
solution-erp/.claude/skills/permission-matrix/SKILL.md
2026-07-31 21:47:23 +07:00

179 lines
9.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
name: permission-matrix
description: 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.
when-to-use:
- "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`](../../../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.All` ở `src/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):** `Contracts` → `Ct_*`, `Workflows` → `Wf_*`, `PurchaseEvaluations` → `Pe_*`, `PeWorkflows` → `PeWf_*`. Thêm root mới có children → **PHẢI extend 3 chỗ** trong `GetMyMenuTreeQuery` (gotcha #35). 🔴 **`KeHoachKyKet` (KHKK, +S161) CỐ Ý KHÔNG inherit** — switch `GetMyMenuTreeQuery.cs:71-74` vẫn đúng 4 case; 6 leaf `Khkk_*` grant TỪNG DÒNG riêng (seeder `DbInitializer.cs:2107` grant root + 6 leaf explicit, chạy SAU revoke để THẮNG). Đừng đọc "root mới" thành "tự inherit như `Ct_*`". *(🧊 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 — số key SỐNG → `docs/STATUS.md` "Menu keys", B1 KHÔNG chép số ở đây; nguồn mã = `MenuKeys.cs` + `DbInitializer.cs`)
```
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
KeHoachKyKet (root — const VÀO `All` @S161 W1 ⇒ sinh 4 policy KeHoachKyKet.{R,C,U,D}; Mig 69 KHKK)
└── Khkk_G1 "Kế hoạch ký kết HĐ (NCC-TP)" (DbInitializer.cs:1780-1787)
├── Khkk_WfView Luồng duyệt
├── Khkk_List Danh sách
├── Khkk_Create Thao tác
├── Khkk_Pending Đang duyệt
├── Khkk_Approved Đã duyệt
└── Khkk_Deleted Đã xóa
Reports
System
├── Users
├── Roles
├── Permissions
├── Workflows (root inherit)
│ └── Wf_<Code> × 7 type = 7 leaf
└── PeWorkflows (root inherit)
└── PeWf_<Code> × 2 type = 2 leaf
```
> 🧊 ~~`Budgets (root, NO inherit)` + `Bg_List/Bg_Create/Bg_Pending`~~ — **XOÁ S61 (Mig 50)** cùng module Budget; cây trên từng liệt nó như ĐANG SỐNG tới S164 (cùng bệnh header :26 đã vá @S122 — vá header quên vá CÂY = dư-lượng bất-đối-xứng).
> 🔸 `HopDongCung` (GĐ4 bản-cứng): const CÓ trong `MenuKeys.cs:43` nhưng **NGOÀI `All`** ⇒ **0 policy · 0 menu-row** tới khi có module (1-mốc theo owner). ĐỪNG liệt nó vào cây key sống.
> 🔴 Gate FE cho KHKK: dùng **đúng key policy của endpoint** (`KeHoachKyKet`) — KHÔNG OR `Khkk_*` key con (gotcha **#85**: menu-hiện rộng hơn API-cho ⇒ 403 rải UI; `MenuPermissionHandler.cs:40` so khớp CHÍNH XÁC 1 key, không kế-thừa).
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.cs``SeedMenuTreeAsync` + `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.ts``can(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.tsx``loadMenu()` on login + localStorage cache
## BE policy usage
Register trong Program.cs:
```csharp
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:
```csharp
[HttpPut("{id:guid}")]
[Authorize(Policy = "Contracts.Update")]
public async Task<IActionResult> Update(...) { }
```
## FE guard usage
```tsx
// 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)