PolicyDefinition cookbook - lưu grant theo Role / Merchant / direct Permission
Mẫu dữ liệu PolicyDefinition cụ thể cho mọi trường hợp (không chỉ happy case), kèm casbin line ScopedCasbinAdapter sinh ra và kết quả enforce. Mô hình runtime xem Casbin Authorization.
Enforcer đọc gì
Các cột quan trọng: variant, subject_type, subject_id, target_type, target_id, domain, action, effect, deleted_at.
| Variant | subject → target | Ý nghĩa domain |
|---|---|---|
grant | Role|User → Permission | grant scope: SYSTEM_WIDE / ANY_MEMBER / <Type>_<id> (null ⇒ ANY_MEMBER) |
assign_role | User → Role | scope role theo domain (null ⇒ *, mọi domain) |
join_domain | User → Merchant|Organizer | membership; cấp năng lượng cho grant ANY_MEMBER (không có cột domain) |
role_inherits | Role → Role | chung g, domain * |
domain_inherits | domain → domain | lồng g3 (Merchant ⊂ Organizer) |
resource_inherits | resource → resource | g4 (lồng obj) |
action_inherits | action → action | g5 (lattice action) |
merchant_role | Role ↔ Merchant | không được đọc bởi adapter (metadata UI của @nx/core) |
domain của grant là một trong ba dạng scope. Adapter bọc domain có kiểu thành <Type>_<id> và giữ nguyên các literal scope (SYSTEM_WIDE / ANY_MEMBER).
Fixtures dùng bên dưới
Id tượng trưng (thay bằng id thật):
| Token | Nghĩa |
|---|---|
U | một user id |
MA, MB, MC | merchant id |
O | một organizer id |
R_OWNER, R_EMP, R_GUEST | role id (500_organizer-owner, 100_employee, 001_guest) |
P_FIND, P_DELETE | permission id (Product.find read / Product.deleteById delete) |
action của grant phải bằng action của chính permission (Product.find → read, Product.deleteById → delete), trừ khi có cạnh action_inherits (g5) nối chúng. effect mặc định allow.
Case 1 - Cấp permission cho role (quản theo Role)
Mục tiêu: role R_OWNER được Product.find. domain null mặc định thành ANY_MEMBER.
| variant | subject_type | subject_id | target_type | target_id | domain | action | effect |
|---|---|---|---|---|---|---|---|
| grant | Role | R_OWNER | Permission | P_FIND | NULL ⇒ ANY_MEMBER | read | allow |
Emit (chỉ khi có user thực sự giữ R_OWNER):
p, Role_R_OWNER, ANY_MEMBER, Product.find, read, allowChỉ khớp ở domain user đã join (g2). Thu hồi = soft-delete row này.
Case 2 - Cho user là thành viên của merchant
Mục tiêu: U thuộc merchant MA. Cần cho grant ANY_MEMBER (Case 1/4) và cho data-scoping.
| variant | subject_type | subject_id | target_type | target_id | domain |
|---|---|---|---|---|---|
| join_domain | User | U | Merchant | MA | - |
Emit:
g2, User_U, Merchant_MA→ merchantIds = [MA] và thoả mệnh đề ANY_MEMBER g2(r.sub, r.dom) ở MA.
Case 3 - Gán role cho user, scope MỘT merchant (quản theo Merchant)
Mục tiêu: U là owner chỉ ở MA.
| variant | subject_type | subject_id | target_type | target_id | domain |
|---|---|---|---|---|---|
| assign_role | User | U | Role | R_OWNER | MA |
Emit (kèm grant Case 1 + membership Case 2):
g2, User_U, Merchant_MA
g, User_U, Role_R_OWNER, Merchant_MA
p, Role_R_OWNER, ANY_MEMBER, Product.find, read, allow- enforce
(User_U, Merchant_MA, Product.find, read)→ ALLOW - enforce
(User_U, Merchant_MB, Product.find, read)→ DENY (không cógline ởMB)
Case 4 - Gán role cho TẤT CẢ merchant của user
Mục tiêu: U là owner ở mọi merchant mình thuộc về.
Rows: membership (Case 2) cho từng merchant + MỘT role assignment domain *:
| variant | subject_type | subject_id | target_type | target_id | domain |
|---|---|---|---|---|---|
| join_domain | User | U | Merchant | MA | - |
| join_domain | User | U | Merchant | MB | - |
| assign_role | User | U | Role | R_OWNER | NULL ⇒ * |
Emit:
g2, User_U, Merchant_MA
g2, User_U, Merchant_MB
g, User_U, Role_R_OWNER, *
p, Role_R_OWNER, ANY_MEMBER, Product.find, read, allowALLOW ở MA & MB; DENY ở MC. (Domain role * khớp mọi nơi; grant ANY_MEMBER sau đó thu hẹp về đúng các membership thực của user.)
Case 5 - Grant scope theo Organizer cascade xuống merchant (lồng domain)
Mục tiêu: U là owner trên các merchant của organizer O mà không cần liệt kê, qua một grant <Type>_<id> + domain_inherits.
| variant | subject_type | subject_id | target_type | target_id | domain |
|---|---|---|---|---|---|
| domain_inherits | Merchant | MA | Organizer | O | - |
| domain_inherits | Merchant | MB | Organizer | O | - |
| assign_role | User | U | Role | R_OWNER | NULL ⇒ * |
| grant | Role | R_OWNER | Permission | P_FIND | O |
Emit:
g3, Merchant_MA, Organizer_O
g3, Merchant_MB, Organizer_O
g, User_U, Role_R_OWNER, *
p, Role_R_OWNER, Organizer_O, Product.find, read, allowALLOW ở MA & MB (cả hai lồng dưới O, nên g3(r.dom, Organizer_O) đúng); DENY ở MC (không lồng dưới O). Không cần membership join_domain vì grant scope là <Type>_<id> cụ thể, không phải ANY_MEMBER.
Case 6 - Role global (guest), pre-merchant
Mục tiêu: U onboard được khi chưa có merchant. Grant guest dùng SYSTEM_WIDE nên khớp mọi domain bất kể membership (domain của assignment có thể NULL).
| variant | subject_type | subject_id | target_type | target_id | domain | action |
|---|---|---|---|---|---|---|
| assign_role | User | U | Role | R_GUEST | NULL ⇒ * | - |
| grant | Role | R_GUEST | Permission | (Organizer.onBoarding) | SYSTEM_WIDE | create |
Emit:
g, User_U, Role_R_GUEST, *
p, Role_R_GUEST, SYSTEM_WIDE, Organizer.onBoarding, create, allowALLOW Organizer.onBoarding ở MỌI domain - kể cả placeholder pre-merchant Merchant_00000000-0000-0000-0000-000000000000.
Case 7 - Direct permission cho user (bỏ qua role)
Một grant có subject là User (không phải Role). Self-link reflexive g(User_U, User_U, dom) của matcher giúp direct grant khớp.
7a. Scope một merchant (scope <Type>_<id>):
| variant | subject_type | subject_id | target_type | target_id | domain | action | effect |
|---|---|---|---|---|---|---|---|
| grant | User | U | Permission | P_FIND | MA | read | allow |
→ p, User_U, Merchant_MA, Product.find, read, allow - ALLOW chỉ ở MA (qua self-link g3 | |||||||
g3(Merchant_MA, Merchant_MA)). |
7b. Mọi merchant của user: domain = NULL ⇒ ANY_MEMBER + membership (Case 2) → p, User_U, ANY_MEMBER, Product.find, read, allow - ALLOW ở mọi merchant đã join.
7c. Direct system-wide: domain = SYSTEM_WIDE → p, User_U, SYSTEM_WIDE, Product.find, read, allow
- ALLOW mọi domain (dùng dè dặt).
Case 8 - DENY tường minh (override allow)
Mục tiêu: dù R_OWNER cho phép Product.deleteById, chặn nó với user U.
| variant | subject_type | subject_id | target_type | target_id | domain | action | effect |
|---|---|---|---|---|---|---|---|
| grant | User | U | Permission | P_DELETE | MA | delete | deny |
→ p, User_U, Merchant_MA, Product.deleteById, delete, deny. Effector default-DENY | |||||||
some(allow) && !some(deny) → bất kỳ deny khớp đều thắng → DENY ở MA. |
Pitfalls & các case không-happy
| Tình huống | Row(s) | Kết quả |
|---|---|---|
Grant ANY_MEMBER, KHÔNG membership | grant Case 1 nhưng thiếu join_domain Case 2 | grant ra không g2 line → vô hiệu. Lỗi hay gặp. |
SYSTEM_WIDE trên role tenant (owner/employee) | grant … domain=SYSTEM_WIDE | khớp mọi domain toàn hệ thống → vỡ isolation. Dùng ANY_MEMBER cho role tenant. |
merchant_role dùng để scope enforcement | merchant_role Role↔Merchant | không được đọc bởi adapter → không tác dụng (chỉ metadata UI). |
Lệch action | grant action=read cho permission *.deleteById (delete), không có cạnh g5 | matcher cần g5(r.act, p.act) → không bao giờ khớp. Đặt action = action của permission (hoặc thêm cạnh action_inherits). |
| Grant bị soft-delete | set deleted_at | bị loại (filter deleted_at). |
| Role/Permission bị soft-delete | row trỏ tới role/perm đã xoá | bị loại → grant biến mất. |
| Row trùng | hai cạnh giống hệt | dedup in-memory → vô hại. |
| super-admin / admin / operator | (không cần) | always-allow bypass - mọi PolicyDefinition của họ bị bỏ qua khi enforce. |
| customer | (không) | không có grant backend. |
INSERT nhanh (psql) ví dụ
-- Case 1: cấp Product.find cho role owner (ANY_MEMBER ⇒ domain NULL)
INSERT INTO identity."PolicyDefinition"
(id, variant, subject_type, subject_id, target_type, target_id, action, effect, domain)
VALUES (gen_random_uuid()::text, 'grant', 'Role', '<R_OWNER>', 'Permission', '<P_FIND>', 'read', 'allow', NULL);
-- Case 3: gán owner cho user U scope merchant MA (+ membership)
INSERT INTO identity."PolicyDefinition"
(id, variant, subject_type, subject_id, target_type, target_id, domain)
VALUES
(gen_random_uuid()::text, 'join_domain', 'User', '<U>', 'Merchant', '<MA>', NULL),
(gen_random_uuid()::text, 'assign_role', 'User', '<U>', 'Role', '<R_OWNER>', '<MA>');Trong luồng app nên dùng service/API policy-definition thay vì SQL thô - các INSERT này để hiểu/debug.
Xem thêm
- Casbin Authorization - model + adapter
- RBAC & Policy Definitions - role, API, quy tắc nghiệp vụ
- Ma trận Phân quyền - grant hiện tại theo role