docs/database/database-guide.md: - Conventions (naming, data types, audit fields, soft delete) - Schema hien tai (Identity tables sau migration Init) + seed 12 role + admin - Schema planned: Phase 1 dot 2 (Supplier/Project/Department + Permission Matrix) - Schema planned: Phase 3 (Contract + Approval + Comment + Attachment + Template + Clause + CodeSequence) - Mermaid ERD cho tung phase - Migration workflow (create/apply/revert) - Index strategy + unique indexes - Backup/restore SQL - Common pitfalls + SQL cheatsheet docs/flows/ — 6 flow documentation: - README.md: index - auth-flow.md: login/refresh/me/logout (IMPLEMENTED, sequence + edge cases + security checklist) - permission-flow.md: Phase 1 dot 2 - Role x MenuKey x CRUD resolution + FE guard + BE policy - contract-creation-flow.md: Phase 2 - Drafter flow chon template -> fill -> preview -> save draft - contract-approval-flow.md: Phase 3 - state machine 9 phase chi tiet + reject flow + timeline UI - form-render-flow.md: Phase 2 - OpenXml + ClosedXML + LibreOffice PDF convert - sla-expiry-flow.md: Phase 3 - BackgroundService auto-approve qua SLA + warning notify Update references: - CLAUDE.md (root): them 2 row Tai lieu quan trong - docs/CLAUDE.md: update project layout voi flows/ + database/ - docs/STATUS.md: log docs addition - docs/changelog/migration-todos.md: tick Phase 0 docs items Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9.1 KiB
9.1 KiB
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)
3. Flow — user login → FE resolve menu
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<br/>JOIN Roles ON Permissions.RoleId = Roles.Id<br/>JOIN MenuItems ON Permissions.MenuKey = MenuItems.Key<br/>WHERE Roles.Id IN (user's roles)
DB-->>Q: raw rows
Q->>Q: Group by MenuKey<br/>UNION CRUD flags (OR)<br/>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:
[
{
"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
flowchart TD
Start([Component mount]) --> CheckMenu{usePermission<br/>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<br/>Authorize 'Contracts.Update'?}
BEGuard -->|no| Reject[403 Forbidden]
BEGuard -->|yes| Proceed[Update DB + return 200]
FE pattern (sẽ implement Phase 1 đợt 2):
// 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
<PermissionGuard menuKey="Contracts" action="Update">
<Button>Sửa</Button>
</PermissionGuard>
// Route guard
<Route
path="/admin/permissions"
element={
<PermissionGuard menuKey="Permissions" action="Read" fallback={<Forbidden />}>
<PermissionMatrixPage />
</PermissionGuard>
}
/>
5. Flow — Admin configure permission matrix
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<br/>{roleId, menuKey: "Contracts", canRead, canCreate, canUpdate, canDelete}
API->>M: Send(UpsertPermissionCommand)
M->>DB: SELECT existing Permission<br/>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<br/>sẽ thấy nút Update HĐ<br/>sau khi họ refresh/re-login<br/>(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
// Api/Controllers/ContractsController.cs
[HttpPut("{id}")]
[Authorize(Policy = "Contracts.Update")] // custom policy
public async Task<IActionResult> Update(Guid id, UpdateContractCommand cmd)
{
// ...
}
Custom policy registration (Program.cs):
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<IAuthorizationHandler, MenuPermissionHandler>();
Handler:
public class MenuPermissionHandler : AuthorizationHandler<MenuPermissionRequirement>
{
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:
-
Menu tree seed — từ
MenuKeys.csconst class (Phase 1 đợt 2):Dashboard Master ├── Suppliers ├── Projects └── Departments Contracts Forms Approvals Reports System ├── Users ├── Roles └── Permissions -
Default permissions:
Adminrole → full CRUD mọi menu- Các role khác → chỉ
Readmặ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
AuthContextsau login → không hit API mỗi navigate /api/menus/meresponse 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