Files
solution-erp/.claude/skills/permission-matrix/SKILL.md
pqhuy1987 7ca6c914fa
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m55s
[CLAUDE] Docs: chốt session 2 — PE skeleton + G-084 + skill audit
User feedback: "phần Duyệt NCC chưa xong đâu đấy nhé, còn chỉnh nhiều"
→ mark PE module skeleton (not feature-complete), liệt kê chi tiết chức
năng/UX/edge-case còn missing cho session tiếp.

Update 7 file:
 - STATUS.md — phase = "PE skeleton + refinement WIP", In Progress liệt
   kê 4 nhóm: A Chức năng MISSING (9 item), B UX/Polish (6 item),
   C Edge case (4 item), D Deploy/Ops (1 item). +G-084 row Recently Done.
 - HANDOFF.md — TL;DR "PE skeleton, còn chỉnh nhiều" + Priority 0 section
   cho session tiếp (9 task PE refinement) + cảnh báo runner + G-084.
 - migration-todos.md — Phase 7 checklist (A/B/C/D nhóm) trước Phase 8
   post-launch. Pending migrations: PaymentTermFields + DepartmentOpinions
   + CodeSequences.
 - architecture.md — Section 9 PurchaseEvaluation module (ERD + workflow
   A/B + kế thừa HĐ flow).
 - CLAUDE.md (root) — 5 file đọc đầu (thêm HANDOFF), Modules table, 12
   migration 46 bảng, +PurchaseEvaluation commit scope.
 - .claude/skills/ — 4 skill cross-ref Phase 6:
   * README: trạng thái updated với Phase 6 note
   * contract-workflow: note PE workflow tách table riêng
   * permission-matrix: +Pe_*/PeWf_* menu keys + TODO grant non-admin
   * ef-core-migration: 12 migration history + Phase 7 pending
 - docs/changelog/sessions/2026-04-23-2359-chot-session-pe-skeleton.md —
   session log full commits + MD files updated + session tiếp priorities
   + notes (PE là skeleton, runner check, G-084 rule, MaPhieu format).
2026-04-23 17:46:41 +07:00

153 lines
5.8 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. Seed 12 menu + admin full. FE PermissionGuard + usePermission. BE AuthorizationHandler + 48 policy. Dùng khi debug access denied, gán role, menu không hiện.
when-to-use:
- "permission denied"
- "access denied"
- "menu không hiện"
- "gán role cho user"
- "seed permission"
- "permission matrix edit"
---
# Permission Matrix Skill
> **Status:** Phase 1 đợt 2 IMPLEMENTED + extended qua các phase sau (28 Ct_* + 7 Wf_* + Phase 6 thêm 13 Pe_*/PeWf_*).
>
> **Phase 6 cross-ref (2026-04-23):** Thêm menu keys module Duyệt NCC:
> - `PurchaseEvaluations` (root group) + `PeWorkflows` (admin root, WIP)
> - `Pe_<Code>_<Action>` cho 2 type × 3 action = 6 leaf
> (`Pe_DuyetNcc_List/Create/Pending`, `Pe_DuyetNccPhuongAn_List/Create/Pending`)
> - `PeWf_<Code>` cho admin designer UI (FE chưa build)
> - Inheritance: `PurchaseEvaluations.Read` → auto grant Pe_* descendants (pattern Contracts → Ct_*)
> - FE resolver Layout.tsx: `Pe_<Code>_List` → `/purchase-evaluations?type=N` (1=NccOnly / 2=NccWithPlan)
> - **TODO session tiếp:** grant non-admin role `PurchaseEvaluations.Read` qua `/system/permissions` để user thấy menu Pe_*.
## 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)
12 menu trong `MenuKeys.All`:
```
Dashboard
Master
├── Suppliers
├── Projects
└── Departments
Contracts
Forms
Reports
System
├── Users
├── Roles
└── Permissions
```
Tree hierarchy qua `ParentKey` field. Seed trong `DbInitializer.SeedMenuTreeAsync`.
## 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.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)