Skip to content

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Ở đâuVai trò
Casbin model@venizia/ignis .../enforcers/models/rbac-domain.model.tsCASBIN_RBAC_DOMAIN_SCOPED_MODEL - định nghĩa request/policy/matcher
Policy adapter@venizia/ignis ScopedCasbinAdapterFiltered: 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}.tsalwaysAllowRoles, domainResolver, adapter + cached
Enforcer (framework)@venizia/ignis CasbinAuthorizationEnforcerChạy enforce() mỗi request, cache line set theo user

Identityissuer 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):

ini
[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ệCasbinEdge variantÝ nghĩa
g_, _, _assign_role + role_inheritsUser→Role / Role→Role, có domain (trục sub)
g2_, _join_domainUser là thành viên của một domain (trục dom)
g3_, _domain_inheritslồng domain, vd Merchant ⊂ Organizer
g4_, _resource_inheritslồng resource(obj), vd OrderItem ⊂ Order
g5_, _action_inheritslattice action: manage ⊃ read/write/execute
TokenÝ nghĩa
subSubject - User_<id> hoặc Role_<id>
domDomain request - Merchant_<id> (từ x-merchant-id) hoặc không có
objObject - permission/resource code, vd commerce.product.create
actAction - read / write / execute / manage / create / update / delete
eftEffect - allow (mặc định) / deny

Effect là default-DENY (allow-and-deny của casbin): request cần một allow khớp 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ý:

ScopeKhớpDùng bởi
SYSTEM_WIDEmọi domain, bỏ qua membershiponboarding guest
ANY_MEMBERmọ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_WIDEANY_MEMBER khác nhau. SYSTEM_WIDE thực sự merchant-agnostic - chỉ dùng cho tầng global guest. ANY_MEMBER scope theo tenant: chỉ áp dụng nơi user thực sự giữ membership join_domain. Đặt một role tenant lên SYSTEM_WIDE sẽ 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-admin 999, admin 900, operator 600) bỏ qua enforcement hoàn toàn - cấu hình trong verifier.ts / issuer.ts, không chạm logic domain.
  • domainResolver (getMerchantScopedDomainResolver) đọc header x-merchant-id và trả về { type: 'Merchant', id }, biến thành domain request Merchant_<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ạnhHợp đồng
Định dạngMột merchant id, vd d01b061a-46a8-4f35-9954-747efade2f3f.
Ai gửiClient (web/mobile) set theo merchant đang chọn; API gateway forward nguyên vẹn.
CORSPhả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 merchantTrướ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 bypasssuper-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.

VariantSubject → TargetCasbin line
grantRole|User → Permissionp, <Role|User>_<id>, <SYSTEM_WIDE|ANY_MEMBER|Type_id>, <objectCode>, <action>, <allow|deny>
assign_roleUser → Roleg, User_<id>, Role_<id>, <domain|*> (null ⇒ *)
join_domainUser → Merchant|Organizerg2, User_<id>, <Type>_<domainId>
role_inheritsRole → Roleg, Role_<child>, Role_<parent>, *
domain_inheritsdomain → domaing3, <Type>_<childId>, <Type>_<parentId>
resource_inheritsresource → resourceg4, <childCode>, <parentCode>
action_inheritsaction → actiong5, <childAction>, <parentAction>
merchant_roleRole ↔ Merchantkhô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:

  1. Cạnh per-user (khác nhau theo principal): grant, assign_role, join_domain. Các row join_domain bị giới hạn theo domainTypes đã cấu hình (Merchant, Organizer) và trở thành g2 line, vd g2, User_u1, Merchant_7.
  2. 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, allow

Cạ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

RoleIdentifierEnforcement
Super Admin / Admin / Operator999_* / 900_* / 600_*alwaysAllow bypass - bỏ qua Casbin
Owner500_organizer-ownergrant ANY_MEMBER + manage
Cashier110_cashiergrant ANY_MEMBER, action hẹp hơn theo module
Employee100_employeegrant ANY_MEMBER, action hẹp hơn theo module
Guest001_guestSYSTEM_WIDE - onboarding pre-merchant
Customer010_customerkhô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 trong verifier.ts / issuer.ts từ 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-000000000000 khi chưa chọn merchant. Chỉ grant SYSTEM_WIDE mới khớp ở đó - vì vậy các endpoint onboarding/tra-cứu cần role guest hoặc route authenticate-only.
  • Grant ANY_MEMBER không có membership. Một grant tenant (ANY_MEMBER) chỉ khớp nơi user giữ cạnh join_domain (g2) cho merchant đang active. Thiếu membership → không g2 → 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 *Permissions củ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_role khô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_at của adapter.
  • Grant lưu dạng coarse; read trả về node, không phải lá. manage trên module commerce là một row (grant → commerce), không phải mỗi operation một row. Nên GET …/roles/{id}/targets/permissions mặc định trả về đúng node đó - các op chi tiết mà nó cấp (qua g4 resource + g5 action 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ủa resolveGrants).

Liên quan

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.