# Permission Flow — Resolution Menu + CRUD > **Status:** 📝 Planned (Phase 1 đợt 2) > **Actors:** System (resolution) | Admin (configure matrix) | Any user (guard apply) ## 1. Mô hình 3 layer resolution: ``` User └─ có nhiều Role (UserRoles table) └─ có Permission (Permissions table, row per MenuKey × CRUD) └─ gắn với MenuItem (tree Key → ParentKey) ``` Quyết định cuối cùng: **union** của tất cả permission từ mọi role của user. Nếu bất kỳ role nào `CanRead=true` cho `MenuKey=Contracts` → user được đọc. ## 2. Ma trận Permission ``` MenuKey Role.Admin Role.Drafter Role.CCM Role.BOD ───────────────────────────────────────────────────────────── Dashboard R R R R Contracts CRUD CR (self) RU RU Suppliers CRUD R R R Projects CRUD R R R Users CRUD — — — Roles CRUD — — — Permissions CRUD — — — Reports R — R R ``` (Chi tiết đầy đủ role × menu mapping ở [`../workflow-contract.md §5`](../workflow-contract.md)) ## 3. Flow — user login → FE resolve menu ```mermaid sequenceDiagram actor U as User participant FE as Frontend participant API as MenusController.GetMyTree participant Q as GetMyMenuTreeQueryHandler participant CU as ICurrentUser participant DB U->>FE: Login thành công (có token) FE->>API: GET /api/menus/me API->>Q: Send(GetMyMenuTreeQuery) Q->>CU: UserId + Roles CU-->>Q: userId, roles[] Q->>DB: SELECT Permissions
JOIN Roles ON Permissions.RoleId = Roles.Id
JOIN MenuItems ON Permissions.MenuKey = MenuItems.Key
WHERE Roles.Id IN (user's roles) DB-->>Q: raw rows Q->>Q: Group by MenuKey
UNION CRUD flags (OR)
Build tree (Key → ParentKey) Q-->>API: MenuTreeDto[] with resolved CRUD per node API-->>FE: 200 FE->>FE: Cache in AuthContext + localStorage FE->>FE: Render sidebar (filter nodes có CanRead=true) ``` **Example response:** ```json [ { "key": "Dashboard", "label": "Tổng quan", "icon": "LayoutDashboard", "order": 1, "parentKey": null, "canRead": true, "canCreate": false, "canUpdate": false, "canDelete": false, "children": [] }, { "key": "Master", "label": "Danh mục", "icon": "Database", "order": 2, "parentKey": null, "children": [ { "key": "Suppliers", "label": "Nhà cung cấp", "parentKey": "Master", "canRead": true, "canCreate": true, "canUpdate": true, "canDelete": false, "children": [] } ] } ] ``` ## 4. Flow — FE guard render ```mermaid flowchart TD Start([Component mount]) --> CheckMenu{usePermission
can 'Contracts', 'Update'?} CheckMenu -->|no permission| Hide[Không render nút 'Sửa'] CheckMenu -->|has permission| Show[Render nút 'Sửa'] Show --> ClickEdit[User click 'Sửa'] ClickEdit --> CallAPI[PATCH /api/contracts/:id] CallAPI --> BEGuard{Controller
Authorize 'Contracts.Update'?} BEGuard -->|no| Reject[403 Forbidden] BEGuard -->|yes| Proceed[Update DB + return 200] ``` **FE pattern (sẽ implement Phase 1 đợt 2):** ```tsx // usePermission.ts export function usePermission() { const { menu } = useAuth() // menu cached từ login return { can: (menuKey: string, action: 'Read' | 'Create' | 'Update' | 'Delete') => { const node = findInTree(menu, menuKey) return node?.[`can${action}`] ?? false }, } } // PermissionGuard.tsx // Route guard }> } /> ``` ## 5. Flow — Admin configure permission matrix ```mermaid sequenceDiagram actor A as Admin participant FE as Permission Matrix Page participant API as PermissionsController participant M as UpsertPermissionCommandHandler participant DB A->>FE: Mở /admin/permissions FE->>API: GET /api/permissions?roleId={id} API-->>FE: Array permissions (row per menuKey) A->>FE: Tick checkbox "Contracts.CanUpdate = true" cho role Drafter FE->>FE: Optimistic update UI FE->>API: PUT /api/permissions
{roleId, menuKey: "Contracts", canRead, canCreate, canUpdate, canDelete} API->>M: Send(UpsertPermissionCommand) M->>DB: SELECT existing Permission
WHERE RoleId=? AND MenuKey=? alt Exists M->>DB: UPDATE flags else Not exists M->>DB: INSERT new row end M-->>API: 204 API-->>FE: 204 Note over FE,A: User với role Drafter
sẽ thấy nút Update HĐ
sau khi họ refresh/re-login
(hoặc SignalR notify Phase 3) ``` **Ghi chú quan trọng:** - Permission update **KHÔNG** real-time đến user đang online ở Phase 1 đợt 2 — họ phải logout/login để thấy. - Phase 3 có thể thêm SignalR notify "permission changed" → FE auto refetch `/api/menus/me`. - Phase 4 có thể invalidate JWT khi permission đổi (rare change, nhưng secure). ## 6. Backend guard ```csharp // Api/Controllers/ContractsController.cs [HttpPut("{id}")] [Authorize(Policy = "Contracts.Update")] // custom policy public async Task Update(Guid id, UpdateContractCommand cmd) { // ... } ``` **Custom policy registration (Program.cs):** ```csharp services.AddAuthorization(opts => { foreach (var menu in MenuKeys.All) { foreach (var action in new[] { "Read", "Create", "Update", "Delete" }) { opts.AddPolicy($"{menu}.{action}", p => p.Requirements.Add(new MenuPermissionRequirement(menu, action))); } } }); services.AddSingleton(); ``` **Handler:** ```csharp public class MenuPermissionHandler : AuthorizationHandler { private readonly IApplicationDbContext _db; // ... protected override async Task HandleRequirementAsync(...) { var userId = context.User.GetUserId(); var hasPermission = await _db.Permissions .Where(p => p.Role.Users.Any(u => u.Id == userId)) .Where(p => p.MenuKey == req.MenuKey) .AnyAsync(p => req.Action switch { "Read" => p.CanRead, "Create" => p.CanCreate, "Update" => p.CanUpdate, "Delete" => p.CanDelete, _ => false, }); if (hasPermission) context.Succeed(req); } } ``` ## 7. Seed mặc định (Phase 1 đợt 2) Seed trong `DbInitializer.InitializeAsync`: 1. **Menu tree seed** — từ `MenuKeys.cs` const class (Phase 1 đợt 2): ``` Dashboard Master ├── Suppliers ├── Projects └── Departments Contracts Forms Approvals Reports System ├── Users ├── Roles └── Permissions ``` 2. **Default permissions:** - `Admin` role → full CRUD mọi menu - Các role khác → chỉ `Read` mặc định, admin config thêm sau qua UI ## 8. Edge cases | Case | Xử lý | |---|---| | User có 0 role | Chỉ thấy `Dashboard` (nếu mở cho anonymous), không vào được menu khác | | Role bị xóa | `FK ON DELETE CASCADE` xóa permissions liên quan | | Menu bị remove khỏi `MenuKeys.cs` | Seed job cảnh báo + giữ orphan permissions (dev fix manual) | | Admin tự xóa quyền admin của mình | Check trong UpsertPermissionCommand — nếu target role là Admin + current user đang ở role Admin → `throw ForbiddenException("Không thể tự xóa quyền admin")` | | User có nhiều role conflict (1 role cho, 1 role cấm) | Union (OR) — có ít nhất 1 role cho là được | ## 9. Performance - Menu tree cache trong `AuthContext` sau login → không hit API mỗi navigate - `/api/menus/me` response size ~5KB gzipped (với ~30 menu nodes) - Authorization handler cache scope request (1 query / request) — EF Core auto cache - Phase 4 optimize: Redis distributed cache cho permission matrix (nếu >100 concurrent users) ## 10. Testing checklist (Phase 1 đợt 2) - [ ] Admin login → thấy tất cả menu - [ ] Tạo user role Drafter only → chỉ thấy menu Contracts + self HĐ - [ ] Tạo role tùy chỉnh "CCM Reviewer" chỉ read Contracts + read Reports → verify không thấy Master menu - [ ] User có 2 role (Drafter + Finance) → thấy union của cả 2 - [ ] Admin xóa quyền Update của role Drafter → user Drafter refresh → không thấy nút Sửa - [ ] Backend 403 khi FE bypass (dev tools unhide nút) → gọi API trực tiếp bị chặn