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: Casbin model, adapter domain theo merchant, 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ầnFileVai trò
Casbin modelcore/src/security/casbin-model.tsĐịnh nghĩa request/policy/matcher
Policy adaptercore/src/security/application-casbin-adapter.tsNạp policy của user từ PolicyDefinition vào Casbin
Active-merchant resolvercore/src/utilities/request.utility.tsĐọc x-merchant-id → domain của request
Wiring enforcercore/src/application/{verifier,issuer}.tsnormalizePayloadFn, kết nối cache, alwaysAllowRoles
Enforcer (framework)@venizia/ignis CasbinAuthorizationEnforcerChạy enforce() mỗi request, cache theo user
Cache rediscore/src/components/cache-redis.component.tsBackend cho cache policy của enforcer

Identityissuer JWKS (IssuerApplication); mọi service khác là verifier (VerifierApplication). Cả hai wire cùng một Casbin model + adapter.

Model

core/src/security/casbin-model.ts:

ini
[request_definition]
r = sub, dom, obj, act

[policy_definition]
p = sub, dom, obj, act, eft

[role_definition]
g = _, _, _

[policy_effect]
e = some(where (p.eft == allow)) && !some(where (p.eft == deny))

[matchers]
m = g(r.sub, p.sub, r.dom) && keyMatch(r.dom, p.dom) && r.obj == p.obj && r.act == p.act
TokenÝ nghĩa
subSubject — User_<id> hoặc Role_<id>
domDomain — Merchant_<id> trên membership; * (global) trên role-permission policy
objResource — permission code, vd Organizer.onBoarding
actAction — read / create / update / delete / execute
eftEffect — allow (mặc định) / deny

Thiết kế scale — domain nằm ở membership, không phải trên từng permission:

  1. g = _, _, _ có domain. Membership nghĩa là "user có role TRONG merchant d". Scope theo merchant nằm ở đây → adapter emit một g-line cho mỗi merchant user giữ role.
  2. Role permission domain-agnostic. Adapter emit p, Role, *, code, actmột dòng cho mỗi permission, không nhân theo domain. keyMatch(r.dom, "*") (matcher fn built-in) nhận mọi domain; các g-line ràng buộc merchant nào user thực sự giữ role. → kích thước policy/user là membership + permission (tuyến tính), không phải tích.
  3. Role global (vd 001_guest) dùng một membership g, User, Role, *. Một domain matching function (keyMatch) được đăng ký trên role-def g qua ICasbinEnforcerOptions.domainMatching (verifier/issuer), nên membership * khớp mọi domain.

Request được allow khi có ít nhất một policy allow khớp và không có deny khớp.

Luồng request → quyết định

  • alwaysAllowRoles (super-admin 999, admin 900, operator 600) bỏ qua enforcement hoàn toàn (xem verifier.ts / issuer.ts), không chạm logic domain. resolveActiveMerchant trả undefined cho họ.
  • resolveActiveMerchant (request.utility.ts) đọc header x-merchant-id và trả về làm active merchant. Validation membership hiện đang comment, nên dùng thẳng giá trị header.
  • normalizePayloadFn (trong verifier/issuer) dựng request Casbin: domain = activeMerchantId ? Merchant_<id> : undefined.

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 và chỉ grant domain * (global) mới khớp.

Khía cạnhHợp đồng
Định dạngUUID merchant, 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ỏ (đã thêm trong CORS của verifier.ts).
Placeholder trước khi chọn merchantTrước khi chọn merchant client gửi 00000000-0000-0000-0000-000000000000. Giá trị này không khớp domain merchant thật nào — chỉ grant global (*) áp dụng, vì vậy route onboarding/tra-cứu cần role guest.
Role bypasssuper-admin / admin / operator bỏ qua header — resolveActiveMerchant trả undefined, không enforce.
HQ ownerKhi header trỏ tới merchant HQ của một organizer, grant Owner được expand ra mọi merchant của organizer đó (xem HQ-expansion).

Luồng: resolveActiveMerchant (request.utility.ts) đọc header → normalizePayloadFn biến thành domain request Casbin Merchant_<id> (hoặc undefined khi thiếu/bypass). 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).

Ngữ nghĩa domain

Cột domain trên một row PolicyDefinition quyết định grant/assignment áp dụng ở đâu. Adapter dịch như sau:

Domain emit ra phụ thuộc role và cột domain của row:

Trường hợpÝ nghĩaDomain policy emit ra
Role ∈ GLOBAL_ROLE_IDENTIFIERS (vd 001_guest)Role global → áp dụng mọi domain, bất kể assignment domain*
Merchant_<id> (domain tường minh)Chỉ merchant đóMerchant_<id>
NULL domainGlobal assignment → chiếu lên mọi merchant user thuộc về (membership)một dòng/merchant thành viên

⚠️ "Role global" và "NULL domain" khác nhau. Role global (GLOBAL_ROLE_IDENTIFIERS trong AppFixedRoles) thực sự merchant-agnostic, emit *. NULL domain trên role thường (owner/employee) nghĩa là "mọi merchant của user" (scope tenant). Đánh dấu owner/employee là global sẽ vỡ isolation — nên globalness là allow-list tường minh trong code, không phải mặc định từ NULL.

PolicyDefinition → Casbin line

PolicyDefinition là bảng cạnh (edge) duy nhất. Adapter đọc các row của user và emit Casbin line.

VariantSubject → TargetCasbin line
groupUser → Merchant(membership; cấp cho projection domain NULL)
groupUser → Roleg, User_<u>, Role_<r>
policyRole → Permissionp, Role_<r>, <domain>, <code>, <act>, <eft>
policyUser → Permissionp, User_<u>, <domain>, <code>, <act>, <eft> (direct)

Adapter

ApplicationCasbinAdapter.loadFilteredPolicy(model, filter) chạy một lần cho mỗi user (rồi enforcer cache ~5 phút). Các bước:

  1. queryUserPolicies — 3 query đơn mục đích, chạy song song (Promise.all):
    • queryMerchantMemberships — row group tới MerchantmerchantIds[].
    • queryRoleAssignments — row group tới Role, INNER JOIN Role (loại soft-deleted) → { roleId, domain, identifier }[].
    • queryDirectPolicies — row policy tới Permission, INNER JOINPermission (loại soft-deleted) → grant trực tiếp cho user.
  2. queryRolePermissions — permission cấp cho các role của user.
  3. HQ-owner expansion (queryHqOwnerOrgMerchants) — nếu user sở hữu merchant headquarter của một organizer, mở rộng role owner sang mọi merchant của organizer đó (kể cả merchant mới tạo).
  4. buildGroupPolicyLines — resolve domain của từng role: role global (AppFixedRoles.isGlobalRole) → *; domain tường minh → merchant đó; NULL → membership của user. Emit một g line không-domain cho mỗi role, và một p line cho mỗi (role-perm × domain). Wildcard * giữ raw qua toDomainVerb.
  5. buildDirectPolicyLines — cùng ngữ nghĩa domain cho permission trực tiếp của user.

Ví dụ line emit ra

Owner ở merchant A (assignment domain = A), được grant Material.find:

g, User_u1, Role_owner
p, Role_owner, Merchant_A, Material.find, read, allow

→ request (User_u1, Merchant_A, Material.find, read) = allow; request (User_u1, Merchant_B, …) = deny (không có line Merchant_B, không *).

Guest (role global 001_guest, assignment domain bất kỳ), được grant Organizer.onBoarding:

g, User_u1, Role_guest
p, Role_guest, *, Organizer.onBoarding, create, allow

→ request (User_u1, Merchant_<bất kỳ>, Organizer.onBoarding, create) = allow (chạy được cả với placeholder x-merchant-id lúc chưa có merchant). * đến từ việc role nằm trong GLOBAL_ROLE_IDENTIFIERS, không phải từ cột domain của assignment.

Role & bypass

RoleIdentifierEnforcement
Super Admin / Admin / Operator999_* / 900_* / 600_*alwaysAllow bypass — bỏ qua Casbin
Owner500_organizer-ownerdomain theo merchant (+ HQ expansion)
Cashier110_cashierdomain theo merchant
Employee100_employeedomain theo merchant
Guest001_guestglobal (*) — onboarding pre-merchant
Customer010_customerkhông có quyền backend

alwaysAllowRoles cấu hình trong verifier.ts / issuer.ts qua AuthorizeComponent. Fixed role nằm ở AppFixedRoles (core/src/models/schemas/identity/role/constants.ts).

Cache enforcer

Enforcer cache policy đã nạp của mỗi user (TTL mặc định ~5 phút, key casbin:<principalType>:<userId>). Dùng Redis nếu có, không thì in-memory.

  • useCacheRedis({ bindingKey }) (base application) tạo + bind eager helper redis, vì configureAuthorization() wire enforcer ngay trong preConfiguretrước khi component boot. Client dùng lazyConnect; CacheRedisComponent làm ấm kết nối sau.
  • getAuthorizationRedisConnection() resolve helper đó cho enforcer; trả undefined (→ in-memory) nếu không có cache redis nào đăng ký.

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 domain * (global) mới khớp ở đó — vì vậy các endpoint onboarding/tra-cứu cần role guest hoặc route authenticate-only.
  • Permission phải được seed mới grant được. Nếu một route enforce code không có trong catalog *Permissions của module, sẽ không có row Permission, nên không tạo grant được và route trả 403 cho mọi người (trừ bypass). Hai dạng catalog gap hay gặp:
    • Sub-module chưa được aggregate. Có const *Permissions cho sub-feature (vd SalesReportPermissions, AllocationUsagePermissions, ReservationPermissions) nhưng không spread vào catalog module (SalePermissions). Seeder chỉ duyệt catalog đã aggregate nên các code đó không bao giờ được insert → 403.
    • Grant migration âm thầm no-op. Migration *-seed-role-permissions cần RoleRepository / PolicyDefinitionRepository / PermissionRepository; nếu application.ts của service không this.repository(...) đủ 3 repo này thì migration ném "RoleRepository is not bounded" và để lại 0 grant. Route 403 cho mọi role dù row permission đã tồn tại. (Gặp ở pricing — fix bằng cách bind 3 repo trong pricing/src/application.ts.)
  • Soft-delete bị loại bởi INNER JOIN (role/permission) và filter deletedAt IS NULL.

Liên quan

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