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 adapter 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.
| # | Cạnh | variant | subject → target | Ý nghĩa domain |
|---|---|---|---|---|
| 1 | User → Role | group | User → Role | scope role theo merchant |
| 2 | User → Merchant | group | User → Merchant | (membership; không dùng domain) |
| 3 | Role → Permission | policy | Role → Permission | bỏ qua (luôn emit *) |
| 4 | User → Permission | policy | User → Permission | scope direct-grant theo merchant |
domain chứa merchant-id thô, NULL, hoặc * (adapter bọc merchant-id thành Merchant_<id>, giữ * raw). Cạnh Role → Merchant / Role → Organizer không được đọc — đừng dùng để scope.
Fixtures dùng bên dưới
Id tượng trưng (thay bằng UUID/snowflake thật):
| Token | Nghĩa |
|---|---|
U | một user id |
MA, MB, MC | merchant 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). 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 không liên quan với role grant.
| variant | subject_type | subject_id | target_type | target_id | domain | action | effect |
|---|---|---|---|---|---|---|---|
| policy | Role | R_OWNER | Permission | P_FIND | NULL | read | allow |
Emit ra (chỉ khi có user thực sự giữ R_OWNER ở ≥1 domain):
p, Role_R_OWNER, *, Product.find, read, allow1 row / (role, permission). 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 NULL-domain projection (Case 4) và cho data-scoping.
| variant | subject_type | subject_id | target_type | target_id | domain | action | effect |
|---|---|---|---|---|---|---|---|
| group | User | U | Merchant | MA | NULL | NULL | NULL |
→ merchantIds = [MA]. Tự thân không emit line; kết hợp với Case 4.
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 | action | effect |
|---|---|---|---|---|---|---|---|
| group | User | U | Role | R_OWNER | MA | NULL | NULL |
Emit (giả sử có grant Case 1):
g, User_U, Role_R_OWNER, Merchant_MA
p, Role_R_OWNER, *, 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ó g-line ở 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 NULL:
| variant | subject_type | subject_id | target_type | target_id | domain |
|---|---|---|---|---|---|
| group | User | U | Merchant | MA | NULL |
| group | User | U | Merchant | MB | NULL |
| group | User | U | Role | R_OWNER | NULL |
Emit:
g, User_U, Role_R_OWNER, Merchant_MA
g, User_U, Role_R_OWNER, Merchant_MB
p, Role_R_OWNER, *, Product.find, read, allowALLOW ở MA & MB; DENY ở MC. (NULL domain của role = "chiếu lên các membership của user".)
Case 5 — Role global (guest), pre-merchant
Mục tiêu: U onboard được khi chưa có merchant. Guest là role global (GLOBAL_ROLE_IDENTIFIERS) → adapter ép domain * bất kể domain của assignment (NULL OK).
| variant | subject_type | subject_id | target_type | target_id | domain |
|---|---|---|---|---|---|
| group | User | U | Role | R_GUEST | NULL |
| policy | Role | R_GUEST | Permission | (Organizer.onBoarding) | NULL |
Emit:
g, User_U, Role_R_GUEST, *
p, Role_R_GUEST, *, Organizer.onBoarding, create, allowALLOW Organizer.onBoarding ở MỌI domain — kể cả placeholder pre-merchant Merchant_00000000-0000-0000-0000-000000000000.
Case 6 — Direct permission cho user (bỏ qua role)
6a. Scope một merchant:
| variant | subject_type | subject_id | target_type | target_id | domain | action | effect |
|---|---|---|---|---|---|---|---|
| policy | User | U | Permission | P_FIND | MA | read | allow |
→ p, User_U, Merchant_MA, Product.find, read, allow — ALLOW chỉ ở MA (khớp qua reflexive | |||||||
g(User_U, User_U, dom) + keyMatch exact). |
6b. Mọi merchant của user: domain = NULL + membership (Case 2) → mỗi membership một p, User_U, Merchant_<m>, ….
6c. Direct global: domain = * → p, User_U, *, Product.find, read, allow — ALLOW mọi domain (dùng dè dặt).
Case 7 — 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 |
|---|---|---|---|---|---|---|---|
| policy | User | U | Permission | P_DELETE | MA | delete | deny |
→ p, User_U, Merchant_MA, Product.deleteById, delete, deny. Policy effect | |||||||
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ả |
|---|---|---|
| Role domain NULL, KHÔNG membership | row Case 4 nhưng thiếu Case 2 | role ra 0 domain → không g-line → vô hiệu. Lỗi hay gặp. |
* trên role thường (owner/employee) | group User→Role domain=* | role khớp mọi merchant toàn hệ thống → vỡ isolation. Tuyệt đối không. |
| domain trên row Role→Permission | policy Role→Permission domain=MA | domain bị bỏ qua — emit *. KHÔNG scope. Để NULL. |
| Cạnh Role→Merchant / Role→Organizer | group Role→Merchant | không được đọc → không tác dụng. |
| Direct perm domain NULL, không membership | Case 6 domain NULL, thiếu Case 2 | direct grant chiếu vào rỗng → vô hiệu. |
Lệch action | grant action=read cho permission *.deleteById (delete) | matcher cần r.act == p.act → không bao giờ khớp. Đặt action = action của permission. |
| Grant bị soft-delete | set deleted_at | bị loại (deletedAt IS NULL / INNER JOIN). |
| Role/Permission bị soft-delete | row trỏ tới role/perm đã xoá | INNER JOIN loại cạnh → grant biến mất. |
| Row trùng (cùng domain) | hai assignment 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ó quyền backend. |
INSERT nhanh (psql) ví dụ
-- Case 1: cấp Product.find cho role owner
INSERT INTO identity."PolicyDefinition"
(id, variant, subject_type, subject_id, target_type, target_id, action, effect, domain)
VALUES (gen_random_uuid()::text, 'policy', '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, 'group', 'User', '<U>', 'Merchant', '<MA>', NULL),
(gen_random_uuid()::text, 'group', '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