Casbin Authorization (chi tiết runtime) v1.1.0
Một request được phân quyền lúc chạy ra sao: scoped-RBAC Casbin model, adapter filtered theo từng request, luồng request → quyết định, role bypass, và cache enforcer.
Phạm vi. Trang này mô tả cách hiện thực. Mô hình dữ liệu policy (PolicyDefinition, role, API) xem RBAC & Policy Definitions; quyết định kiến trúc xem ADR-0002; ảnh chụp grant theo role hiện tại xem Ma trận Phân quyền.
Các thành phần
| Thành phần | Ở đâu | Vai trò |
|---|---|---|
| Casbin model | @venizia/ignis .../enforcers/models/rbac-domain.model.ts | CASBIN_RBAC_DOMAIN_SCOPED_MODEL - định nghĩa request/policy/matcher |
| Policy adapter | @venizia/ignis ScopedCasbinAdapter | Filtered: nạp cạnh PolicyDefinition của MỘT principal mỗi request |
| Adapter entities | @nx/core application/base.ts (getScopedCasbinEntities) | domainTypes = ['Merchant', 'Organizer'], soft-delete trên deleted_at |
| Active-merchant resolver | @nx/core application/base.ts (getMerchantScopedDomainResolver) | Đọc x-merchant-id → domain request Merchant_<id> |
| Wiring enforcer | @nx/core application/{verifier,issuer}.ts | alwaysAllowRoles, domainResolver, adapter + cached |
| Enforcer (framework) | @venizia/ignis CasbinAuthorizationEnforcer | Chạy enforce() mỗi request, cache line set theo user |
Identity là issuer JWKS (IssuerApplication); mọi service khác là verifier (VerifierApplication). Cả hai wire cùng một model + ScopedCasbinAdapter.
Model
CASBIN_RBAC_DOMAIN_SCOPED_MODEL (@venizia/ignis .../enforcers/models/rbac-domain.model.ts):
[request_definition]
r = sub, dom, obj, act
[policy_definition]
p = sub, dom, obj, act, eft
[role_definition]
g = _, _, _
g2 = _, _
g3 = _, _
g4 = _, _
g5 = _, _
[policy_effect]
e = some(where (p.eft == allow)) && !some(where (p.eft == deny))
[matchers]
m = g(r.sub, p.sub, r.dom) && (p.dom == "SYSTEM_WIDE" || (p.dom == "ANY_MEMBER" && g2(r.sub, r.dom)) || g3(r.dom, p.dom)) && (objectMatch(r.obj, p.obj) || g4(r.obj, p.obj)) && g5(r.act, p.act)| Quan hệ | Casbin | Edge variant | Ý nghĩa |
|---|---|---|---|
g | _, _, _ | assign_role + role_inherits | User→Role / Role→Role, có domain (trục sub) |
g2 | _, _ | join_domain | User là thành viên của một domain (trục dom) |
g3 | _, _ | domain_inherits | lồng domain, vd Merchant ⊂ Organizer |
g4 | _, _ | resource_inherits | lồng resource(obj), vd OrderItem ⊂ Order |
g5 | _, _ | action_inherits | lattice action: manage ⊃ read/write/execute |
| Token | Ý nghĩa |
|---|---|
sub | Subject - User_<id> hoặc Role_<id> |
dom | Domain request - Merchant_<id> (từ x-merchant-id) hoặc không có |
obj | Object - permission/resource code, vd commerce.product.create |
act | Action - read / write / execute / manage / create / update / delete |
eft | Effect - allow (mặc định) / deny |
Effect là default-DENY (allow-and-deny của casbin): request cần một allow khớp và không deny nào khớp, nên một deny tường minh override mọi allow.
Domain scope của một grant
Cột dom trên một dòng p (grant) quyết định grant áp dụng ở đâu. Nó là một trong ba dạng, đều do mệnh đề domain (p.dom == "SYSTEM_WIDE" || (p.dom == "ANY_MEMBER" && g2(...)) || g3(...)) xử lý:
| Scope | Khớp | Dùng bởi |
|---|---|---|
SYSTEM_WIDE | mọi domain, bỏ qua membership | onboarding guest |
ANY_MEMBER | mọi domain subject đã join qua g2 (join_domain) | owner / cashier / employee (role tenant) |
<Type>_<id> | domain đó, cộng mọi child lồng qua g3 (domain_inherits) | grant gắn vào một merchant hoặc một organizer |
⚠️
SYSTEM_WIDEvàANY_MEMBERkhác nhau.SYSTEM_WIDEthực sự merchant-agnostic - chỉ dùng cho tầng global guest.ANY_MEMBERscope theo tenant: chỉ áp dụng nơi user thực sự giữ membershipjoin_domain. Đặt một role tenant lênSYSTEM_WIDEsẽ vỡ isolation, vì vậy đó là lựa chọn tường minh trong code (seed-role-grants.ts), không phải mặc định.
Luồng request → quyết định
alwaysAllowRoles(super-admin999,admin900,operator600) bỏ qua enforcement hoàn toàn - cấu hình trongverifier.ts/issuer.ts, không chạm logic domain.domainResolver(getMerchantScopedDomainResolver) đọc headerx-merchant-idvà trả về{ type: 'Merchant', id }, biến thành domain requestMerchant_<id>; thiếu header →null.
Header x-merchant-id
Mọi request đã xác thực tới verifier service nên kèm x-merchant-id - nó chọn domain merchant đang active để enforce request. Thiếu header này thì request không có domain merchant, nên chỉ grant SYSTEM_WIDE mới khớp.
| Khía cạnh | Hợp đồng |
|---|---|
| Định dạng | Một merchant id, vd d01b061a-46a8-4f35-9954-747efade2f3f. |
| Ai gửi | Client (web/mobile) set theo merchant đang chọn; API gateway forward nguyên vẹn. |
| CORS | Phải nằm trong allow-list Access-Control-Allow-Headers, nếu không browser sẽ lược bỏ. |
| Placeholder trước khi chọn merchant | Trước khi chọn merchant client gửi SYSTEM_MERCHANT_ID (00000000-0000-0000-0000-000000000000). Giá trị này không khớp domain merchant thật - chỉ grant SYSTEM_WIDE áp dụng, vì vậy route onboarding/tra-cứu cần role guest. |
| Role bypass | super-admin / admin / operator bỏ qua header - không enforce. |
Merchant id sai hoặc lạ → không có grant nào khớp → 403 (đây là đảm bảo isolation, không phải bug).
PolicyDefinition → Casbin line
PolicyDefinition là bảng cạnh duy nhất. ScopedCasbinAdapter lọc các row của một principal và emit Casbin line tương ứng.
| Variant | Subject → Target | Casbin line |
|---|---|---|
grant | Role|User → Permission | p, <Role|User>_<id>, <SYSTEM_WIDE|ANY_MEMBER|Type_id>, <objectCode>, <action>, <allow|deny> |
assign_role | User → Role | g, User_<id>, Role_<id>, <domain|*> (null ⇒ *) |
join_domain | User → Merchant|Organizer | g2, User_<id>, <Type>_<domainId> |
role_inherits | Role → Role | g, Role_<child>, Role_<parent>, * |
domain_inherits | domain → domain | g3, <Type>_<childId>, <Type>_<parentId> |
resource_inherits | resource → resource | g4, <childCode>, <parentCode> |
action_inherits | action → action | g5, <childAction>, <parentAction> |
merchant_role | Role ↔ Merchant | không đọc - chỉ là metadata UI của @nx/core |
Adapter
ScopedCasbinAdapter.loadFilteredPolicy(model, filter) là filtered-load entry point của Casbin: nó dựng full line set cho một principal và đưa cho enforcer. Enforcer cache line set đó theo user trong Redis, nên hàm này chỉ chạy khi cache MISS.
Nó nạp, trong một đợt song song:
- Cạnh per-user (khác nhau theo principal):
grant,assign_role,join_domain. Các rowjoin_domainbị giới hạn theodomainTypesđã cấu hình (Merchant,Organizer) và trở thànhg2line, vdg2, User_u1, Merchant_7. - Cạnh cấu trúc toàn cục (dùng chung mọi principal):
role_inherits(g),domain_inherits(g3),resource_inherits(g4),action_inherits(g5).
Mặc định khi emit một grant: effect null thành allow; domain null thành ANY_MEMBER. Domain null của assign_role thành * (mọi domain). Row soft-deleted bị loại (filter deleted_at).
Ví dụ line emit ra
Owner (manage trên module commerce), thành viên của merchant A:
g, User_u1, Role_owner, *
g2, User_u1, Merchant_A
p, Role_owner, ANY_MEMBER, commerce, manage, allowCạnh cấu trúc toàn cục (seed một lần, dùng chung mọi principal):
g4, commerce.product, commerce (resource_inherits)
g5, read, manage (action_inherits)→ request (User_u1, Merchant_A, commerce.product, read) = allow (thành viên của A thoả ANY_MEMBER; read ⊂ manage; commerce.product ⊂ commerce); request (User_u1, Merchant_B, …) = deny (u1 chưa join B).
Guest (read trên licensing, SYSTEM_WIDE):
g, User_u2, Role_guest, *
p, Role_guest, SYSTEM_WIDE, licensing, read, allow→ request (User_u2, <bất kỳ merchant, kể cả placeholder pre-merchant>, licensing, read) = allow. SYSTEM_WIDE khớp mọi domain, không cần membership.
Role & bypass
| Role | Identifier | Enforcement |
|---|---|---|
| Super Admin / Admin / Operator | 999_* / 900_* / 600_* | alwaysAllow bypass - bỏ qua Casbin |
| Owner | 500_organizer-owner | grant ANY_MEMBER + manage |
| Cashier | 110_cashier | grant ANY_MEMBER, action hẹp hơn theo module |
| Employee | 100_employee | grant ANY_MEMBER, action hẹp hơn theo module |
| Guest | 001_guest | SYSTEM_WIDE - onboarding pre-merchant |
| Customer | 010_customer | không có grant backend |
alwaysAllowRoles cấu hình trong verifier.ts / issuer.ts. Fixed role nằm ở AppFixedRoles; map coarse role→module grant nằm ở seed-role-grants.ts (COARSE_MODULE_GRANTS).
Cache enforcer
Enforcer cache line set đã nạp của mỗi user bằng driver CasbinEnforcerCachedDrivers.REDIS (expiresIn 5 phút, key casbin:<principalType>:<userId>).
- Config
cachedđược wire trongverifier.ts/issuer.tstừgetAuthorizationRedisConnection(). Khi không có kết nối redis authorization nào đăng ký,cached: { use: false }- enforcer chạy không cache.
Hệ quả: đổi permission/role có hiệu lực ở lần cache hết hạn kế tiếp (~5 phút) hoặc lần sign-in kế tiếp - không tức thời.
Lưu ý (gotchas)
- Request pre-merchant. Client gửi
x-merchant-id: 00000000-0000-0000-0000-000000000000khi chưa chọn merchant. Chỉ grantSYSTEM_WIDEmới khớp ở đó - vì vậy các endpoint onboarding/tra-cứu cần role guest hoặc route authenticate-only. - Grant
ANY_MEMBERkhông có membership. Một grant tenant (ANY_MEMBER) chỉ khớp nơi user giữ cạnhjoin_domain(g2) cho merchant đang active. Thiếu membership → khôngg2→ không khớp → 403 dù grant đã tồn tại. Lỗi hay gặp. - Node permission phải được seed mới grant được. Coarse grant nhắm tới một node resource module/subject; nếu catalog
*Permissionscủa module chưa được aggregate, row node không bao giờ được insert, nên grant không resolve được và route 403 cho mọi người (trừ bypass). merchant_rolekhông enforce. Nó là metadata UI của@nx/core(role nào khả dụng trong một merchant) - adapter không bao giờ đọc, nên không tác động lên quyết định.- Soft-delete bị loại bởi filter
deleted_atcủa adapter. - Grant lưu dạng coarse; read trả về node, không phải lá.
managetrên modulecommercelà một row (grant → commerce), không phải mỗi operation một row. NênGET …/roles/{id}/targets/permissionsmặc định trả về đúng node đó - các op chi tiết mà nó cấp (quag4resource +g5action lattice) do enforcer resolve, không lưu trữ. Truyền?expand=trueđể API resolve node về danh sách quyền hiệu lực đầy đủ (PermissionService.expandGrants, nghịch đảo củaresolveGrants).
Liên quan
- RBAC & Policy Definitions - mô hình dữ liệu, role, API
- PolicyDefinition cookbook - row cụ thể theo từng case
- ADR-0002 Casbin via PolicyDefinition
- Ma trận Phân quyền - grant hiện tại theo role