Bỏ qua để đến nội dung

Mô hình RBAC & Phân quyền

RBAC dựa trên mã quyền với định nghĩa vai trò theo từng tenant.

Nền tảng định nghĩa một danh mục cố định các mã quyền (ví dụ: accounting.journalentry.create). Quản trị viên tenant định nghĩa vai trò và gán mã quyền cho chúng. Người dùng được gán một hoặc nhiều vai trò trong tenant của họ. JWT được cấp khi đăng nhập mang các mã quyền đã được tổng hợp của người dùng. Phân quyền dựa trên policy của ASP.NET Core thực thi các mã này ở tầng controller/endpoint. SQL Server RLS (xem multitenancy.md) thực thi cô lập tenant độc lập ở tầng dữ liệu.


Mọi module trong hệ thống — Kế toán, Kho hàng, Mua hàng, Bán hàng — đều yêu cầu kiểm soát truy cập. Mô hình truy cập phải:

  • Được thực thi ở tầng API, không chỉ ở UI
  • Hỗ trợ tùy chỉnh theo tenant (một “Quản lý” ở Tenant A có thể có quyền khác với “Quản lý” ở Tenant B)
  • Phân biệt giữa thao tác cấp nền tảng (cấp phát tenant, cấu hình hệ thống) và thao tác cấp tenant
  • Có khả năng kiểm toán (ai làm gì, theo quyền nào)
  • Dễ hiểu để đội ngũ ≤5 kỹ sư duy trì

Chủ thểMô tả
Quản trị viên Hệ thốngNgười vận hành nền tảng. Quản lý tenant, thanh toán, cấu hình nền tảng. Không thuộc phạm vi bất kỳ tenant nào. Tách biệt với người dùng tenant.
Quản trị viên TenantNgười dùng quyền cao trong một tenant. Quản lý người dùng, vai trò, cấu hình tenant. Được gán vai trò tích hợp sẵn TenantAdministrator.
Người dùng TenantBất kỳ người dùng nào hoạt động trong một tenant. Có một hoặc nhiều vai trò do tenant định nghĩa.

Quản trị viên Hệ thống xác thực riêng biệt và sử dụng PlatformAdminDbContext (xem multitenancy.md). Họ không thể truy cập dữ liệu tenant qua các API ứng dụng thông thường.


Mã quyền theo quy ước đặt tên: {module}.{resource}.{action}

Module: core, accounting, inventory, procurement, sales, reporting Hành động: view, create, edit, delete, approve, void, export, manage

Mô tả
core.users.viewXem danh sách và hồ sơ người dùng
core.users.manageTạo, sửa, vô hiệu hóa người dùng
core.roles.viewXem định nghĩa vai trò
core.roles.manageTạo và sửa vai trò, gán mã quyền
core.tenantconfig.viewXem cấu hình tenant
core.tenantconfig.manageSửa cấu hình tenant, cài đặt hóa đơn điện tử
core.auditlog.viewXem nhật ký kiểm toán
Mô tả
accounting.journalentry.viewXem bút toán và sổ cái tổng hợp
accounting.journalentry.createTạo bút toán thủ công
accounting.journalentry.voidHủy bút toán đã ghi sổ (tạo bút toán đảo)
accounting.period.viewXem các kỳ tài chính
accounting.period.manageMở và đóng kỳ tài chính
accounting.chartofaccounts.viewXem hệ thống tài khoản
accounting.chartofaccounts.manageThêm và sửa tài khoản
accounting.ap.viewXem hóa đơn nhà cung cấp và thanh toán
accounting.ap.createGhi nhận hóa đơn nhà cung cấp và thanh toán
accounting.ap.approveDuyệt hóa đơn nhà cung cấp để thanh toán
accounting.ar.viewXem hóa đơn khách hàng và biên lai
accounting.ar.createXuất hóa đơn khách hàng và ghi nhận biên lai
accounting.ar.voidHủy hóa đơn khách hàng
accounting.einvoice.issueGửi hóa đơn đến nhà cung cấp hóa đơn điện tử
accounting.einvoice.cancelHủy hóa đơn điện tử đã gửi
accounting.tax.viewXem tờ khai VAT, TNDN, TNCN
accounting.tax.manageLập và nộp tờ khai thuế
accounting.reports.viewXem báo cáo tài chính (IFRS và VAS)
accounting.reports.exportXuất báo cáo tài chính ra PDF/Excel
Mô tả
inventory.products.viewXem danh mục sản phẩm
inventory.products.manageTạo và sửa sản phẩm
inventory.stock.viewXem mức tồn kho và biến động
inventory.stockadjustment.createTạo phiếu điều chỉnh tồn kho
inventory.stockadjustment.approveDuyệt điều chỉnh tồn kho vượt ngưỡng
inventory.stocktake.manageKhởi tạo và hoàn tất kiểm kê
Mô tả
procurement.suppliers.viewXem danh sách nhà cung cấp
procurement.suppliers.manageTạo và sửa nhà cung cấp
procurement.purchaserequisition.createLập phiếu yêu cầu mua hàng
procurement.purchaserequisition.approveDuyệt phiếu yêu cầu mua hàng
procurement.purchaseorder.createTạo đơn đặt mua hàng
procurement.purchaseorder.approveDuyệt đơn đặt mua hàng
procurement.goodsreceipt.createGhi nhận nhập kho
procurement.goodsreceipt.approveDuyệt phiếu nhập kho
Mô tả
sales.customers.viewXem danh sách khách hàng
sales.customers.manageTạo và sửa khách hàng
sales.quotation.createTạo báo giá
sales.salesorder.createTạo đơn bán hàng
sales.salesorder.approveDuyệt đơn bán hàng
sales.deliverynote.createTạo phiếu giao hàng
sales.deliverynote.confirmXác nhận giao hàng (kích hoạt xuất hóa đơn AR)
Mô tả
reporting.dashboards.viewXem dashboard có thể cấu hình
reporting.statutory.viewXem báo cáo pháp định VAS
reporting.statutory.exportXuất báo cáo pháp định VAS

Các vai trò này được seed cho mọi tenant mới. Quản trị viên tenant không thể xóa chúng nhưng có thể đổi tên và điều chỉnh mã quyền (ngoại trừ TenantAdministrator).

Vai tròQuyền mặc địnhGhi chú
TenantAdministratorTất cả mãKhông thể sửa hoặc xóa
AccountantTất cả mã accounting.* + reporting.*Có thể sửa bởi quản trị viên tenant
PurchasingTất cả mã procurement.* + inventory.stock.viewCó thể sửa
SalesTất cả mã sales.* + inventory.stock.viewCó thể sửa
ViewerTất cả mã *.view*.exportChỉ đọc toàn hệ thống; có thể sửa

Quản trị viên tenant có thể tạo thêm vai trò với bất kỳ tập con nào của danh mục.


PermissionCodes (seed khi triển khai; không thể sửa qua UI)
────────────────────────────────────────────────────────
Code NVARCHAR(100) PK -- ví dụ: 'accounting.journalentry.create'
Module NVARCHAR(50) NOT NULL
Resource NVARCHAR(50) NOT NULL
Action NVARCHAR(50) NOT NULL
DisplayName NVARCHAR(200) NOT NULL
Description NVARCHAR(500) NULL
TenantRoles
────────────────────────────────────────────────────────
RoleId UNIQUEIDENTIFIER PK
TenantId UNIQUEIDENTIFIER NOT NULL (RLS)
RoleName NVARCHAR(100) NOT NULL
Description NVARCHAR(500) NULL
IsSystem BIT NOT NULL -- TRUE = seed, không thể xóa
UNIQUE (TenantId, RoleName)
TenantRolePermissions
────────────────────────────────────────────────────────
RoleId UNIQUEIDENTIFIER NOT NULL FK → TenantRoles
PermissionCode NVARCHAR(100) NOT NULL FK → PermissionCodes
PRIMARY KEY (RoleId, PermissionCode)
TenantUsers
────────────────────────────────────────────────────────
TenantUserId UNIQUEIDENTIFIER PK
TenantId UNIQUEIDENTIFIER NOT NULL (RLS)
UserId UNIQUEIDENTIFIER NOT NULL FK → AspNetUsers
DisplayName NVARCHAR(200) NOT NULL
IsActive BIT NOT NULL
LastLoginAt DATETIMEOFFSET NULL
UNIQUE (TenantId, UserId)
TenantUserRoles
────────────────────────────────────────────────────────
TenantUserId UNIQUEIDENTIFIER NOT NULL FK → TenantUsers
RoleId UNIQUEIDENTIFIER NOT NULL FK → TenantRoles
PRIMARY KEY (TenantUserId, RoleId)

AspNetUsers là bảng ASP.NET Core Identity tiêu chuẩn (cấp nền tảng, không áp dụng RLS). Một người dùng có thể thuộc nhiều tenant qua các hàng TenantUsers riêng biệt.


Tập claims giống nhau bất kể phương thức vận chuyển xác thực:

{
"sub": "user-guid",
"tenant_id": "tenant-guid",
"tenant_user_id": "tenant-user-guid",
"permissions": [
"accounting.journalentry.view",
"accounting.journalentry.create",
"accounting.ap.view",
"accounting.ap.create",
"reporting.dashboards.view"
]
}

Tại sao dùng quyền dưới dạng claims thay vì vai trò:

  • Cho phép middleware phân quyền đưa ra quyết định mà không cần truy vấn cơ sở dữ liệu ở mỗi request.
  • Tên vai trò do tenant định nghĩa và có thể trùng giữa các tenant. Mã quyền ổn định và được định nghĩa bởi nền tảng.
  • Danh sách quyền của người dùng thông thường nhỏ (10–30 mã); kích thước claim không phải vấn đề trong thực tế.

Frontend React được host cùng với API trên cùng origin. Cookie auth của ASP.NET Core Identity là mặc định. Claims được lưu trong cookie HttpOnly, Secure, SameSite=Strict được mã hóa do ASP.NET Core Data Protection quản lý. Không cần lưu token trong trình duyệt.

Endpoint Bearer JWT (/auth/token) có sẵn cho các client không thể dùng cookie: ứng dụng mobile native Phase 3 và các tích hợp API bên thứ ba. Phân quyền dựa trên policy của ASP.NET Core thực thi cùng mã quyền bất kể scheme xác thực nào.


Phân quyền dựa trên policy của ASP.NET Core. Mỗi policy tương ứng với một mã quyền:

// Program.cs — đăng ký khi khởi động từ danh mục PermissionCodes
builder.Services.AddAuthorization(options =>
{
foreach (var code in permissionCatalog)
options.AddPolicy(code, p => p.RequireClaim("permissions", code));
});
// Controller
[Authorize(Policy = "accounting.journalentry.create")]
[HttpPost("journal-entries")]
public async Task<IActionResult> CreateJournalEntry(...) { ... }

SQL Server RLS thực thi cô lập tenant độc lập. Người dùng có accounting.journalentry.view chỉ thấy các bút toán thuộc tenant của họ — RLS đảm bảo điều này bất kể mã ứng dụng có lọc đúng hay không.

Pipeline request phải:

  1. Xác thực chữ ký và thời hạn JWT
  2. Set SESSION_CONTEXT(N'TenantId') trên kết nối cơ sở dữ liệu từ claim tenant_id
  3. Điền vào service ITenantContext với TenantIdTenantUserId để dùng trong domain service

Các bước này xảy ra trong auth/tenant middleware trước khi bất kỳ controller nào thực thi. Không controller hay domain service nào lấy TenantId từ dữ liệu người dùng nhập vào.


Mọi hành động được bảo vệ bởi quyền mà làm thay đổi dữ liệu phải được ghi vào nhật ký kiểm toán. Nhật ký ghi lại TenantUserId, hành động đã thực hiện, tài nguyên bị ảnh hưởng, và PermissionCode đã được sử dụng. Xem core-platform.md để biết schema nhật ký kiểm toán.


  1. Quyền cấp trường (field-level) — Một số client có thể yêu cầu ẩn các trường cụ thể (ví dụ: giá vốn với nhân viên bán hàng). Quyền cấp trường được hoãn sang đánh giá customization engine Phase 2. Mã Phase 1 không được giả định cứng về việc ẩn trường.

  2. Ủy quyền phê duyệt — Một số quy trình yêu cầu người dùng ủy quyền phê duyệt tạm thời (ví dụ: khi nghỉ phép). Cơ chế cho việc này chưa được thiết kế; sẽ được giải quyết khi thiết kế workflow engine (Phase 2).

  3. Người dùng đa tenant — Một tư vấn hoặc kế toán có thể cần truy cập nhiều tenant. Mô hình dữ liệu hỗ trợ điều này (một UserId → nhiều hàng TenantUsers), nhưng UI/UX để chuyển đổi context tenant cần được thiết kế.