PRD: Per-merchant Casbin scoped authorization
| Module | Permissions (CORE-02) | PRD ID | PRD-EFF-001 |
| Status | Shipped | Owner | Permissions squad |
| Date | 2026-06-04 | Version | v1.0 |
| Packages | @nx/core · @nx/identity · @nx/commerce · @nx/finance · @nx/inventory | URD | EFF · GRANT · ROLE |
TL;DR
Centralizes authorization on a per-merchant Casbin engine so every verifier service enforces grants within the active merchant domain chosen per request, and resolves a user's reachable merchants/organizers from the Casbin policy repository instead of scattered request-context filtering. The result: "what can this user see and do here" becomes one consistent, policy-backed answer across commerce, finance, and inventory - with per-merchant (per-schema) isolation and soft-delete honoured.
1. Context & Problem
Authorization in KICKO is originally driven by request-context filtering: each service derives a user's reachable organizations and merchants from the request and hand-filters its queries. This makes "what can this user see" implicit, duplicated across services, and impossible to reason about in one place - and it has no single home to resolve a grant within the active merchant domain a request operates in.
A central Casbin scoped-RBAC engine in the VerifierApplication closes this gap: every verifier service enforces grants the same way, and merchant/organizer reach comes from the Casbin policy repository rather than per-request guesswork. Because the framework's adapter does not honour soft-delete and KICKO needs per-merchant (per-schema) isolation, an application-level Casbin adapter belongs in @nx/core so policy/grouping rows load under the project's own contract without editing IGNIS. Inventory is the pilot surface before broader enablement.
2. Goals & Non-Goals
Goals
- Enforce authorization through Casbin in the
VerifierApplication, resolving grants within the active merchant domain selected per request (x-merchant-id). - Resolve a user's reachable merchants/organizers from the Casbin policy repository instead of request-context filtering (commerce, finance, core).
- Provide an application-level scoped Casbin adapter in
@nx/corethat honours soft-delete and per-merchant (per-schema) isolation, requiring no IGNIS changes. - Pilot end-to-end enforcement on inventory: seed inventory role permissions, backfill owner roles, resolve allowed merchant IDs via policy.
- Keep the engine aligned with the framework's scoped Casbin model (wildcard/scoped model correctness, the scoped adapter, Casbin cache invalidation on token refresh).
Non-Goals
- Wildcard / regex permissions (
sales.*) - out of scope (URD §2, §7). - Permission categories / UI grouping, and role templates / bundles.
- Time- or shift-based permissions and permission audit logging.
- The resource / action / domain hierarchy and declaration toolkit - owned by PRD-HIER-001 (
HIER,DECL).
3. Success Metrics
| Metric | Target / signal |
|---|---|
| Central enforcement | 100% of verifier-service authorization decisions flow through the Casbin enforcer, not ad-hoc request filtering |
| Scope correctness | Merchant/organizer reach resolved from policy matches the user's actual grants; no foreign-merchant data leaks |
| Tenant isolation | Policy/grouping rows never cross a merchant (per-schema) boundary |
| Soft-delete fidelity | Revoked (soft-deleted) grants are never enforced as active |
| Pilot stability | Inventory operates fully under per-merchant authorization with no regression in allowed-merchant resolution |
4. Personas & Use Cases
| Persona | Goal in this feature |
|---|---|
| Owner | Reach only their own organizer and its merchants - resolved consistently from policy |
| Employee / Cashier | Act only within assigned merchants, enforced per active-merchant domain |
| Platform admin | Trust one central enforcement path across all verifier services |
| Service (verifier) | Resolve allowed merchant IDs from policy instead of bespoke per-request filtering |
Core scenarios: a request arrives with an active-merchant header → the verifier enforces the grant within that merchant domain via Casbin → reachable merchants/organizers are resolved from the policy repository → soft-deleted grants are excluded and tenant rows stay per-schema.
5. User Stories
- As a platform admin, I want authorization enforced centrally in the verifier, so every service answers "can this user do this here" the same way.
- As an owner, I want my reachable merchants resolved from policy, so I see exactly my organizer's merchants and nothing foreign.
- As an employee, I want grants enforced within the active merchant domain, so my access is scoped to the merchant I'm acting on.
- As a service developer, I want to resolve allowed merchant IDs from the Casbin policy, so I stop hand-filtering queries from request context.
- As a platform admin, I want revoked grants to stop applying immediately (no soft-deleted rows enforced), so access revocation is trustworthy.
- As a platform admin, I want a refreshed token to clear the Casbin cache, so role/permission changes take effect cleanly.
6. Functional Requirements
| # | Requirement | URD ref |
|---|---|---|
| FR-1 | Casbin RBAC enforcement is wired into the VerifierApplication; every verifier service enforces grants centrally | URD-EFF-004 |
| FR-2 | A grant resolves only within the active merchant domain selected per request (x-merchant-id) | URD-EFF-004 · URD-ROLE-005..006 |
| FR-3 | Effective permissions are the deduplicated union of direct + role-inherited grants, resolved from the policy repository | URD-EFF-001..002 |
| FR-4 | A user's reachable organizations and merchants are resolved from the Casbin policy repository, not request-context filtering | URD-EFF-003 |
| FR-5 | Grant / revoke is enforced through the policy repository (idempotent, privilege-escalation guarded) | URD-GRANT-001..007 |
| FR-6 | An application-level scoped Casbin adapter loads policy/grouping rows honouring soft-delete and per-merchant (per-schema) isolation | URD-EFF-004 |
| FR-7 | Tenant isolation is enforced by constraining the membership variant in the grouping query | URD-ROLE-007..008 |
| FR-8 | An Owner at a head-quarter merchant reaches every sibling merchant of that organizer | URD-ROLE-009 |
| FR-9 | Inventory resolves allowed merchant IDs via Casbin policy; inventory role permissions are seeded and owner roles backfilled | URD-EFF-003..004 |
| FR-10 | A token refresh invalidates the Casbin cache so policy changes take effect | URD-EFF-004 |
Full requirement text and acceptance criteria live in the Permissions URD. This PRD references them rather than restating them.
7. Non-Functional Requirements
| Area | Requirement |
|---|---|
| Central enforcement | All authorization decisions in verifier services go through one Casbin enforcer; no per-service ad-hoc filtering remains as the source of truth |
| Tenancy & authz | Every grant resolves within one active merchant domain per request; policy/grouping rows are isolated per merchant (per-schema) |
| Soft-delete fidelity | The scoped adapter excludes soft-deleted policy/grouping rows; revocation is honoured without physical deletes |
| Consistency | Cache invalidation on token refresh keeps enforcement aligned with current policy |
| Framework alignment | The application-level adapter requires no IGNIS edits and tracks the framework's scoped Casbin model |
| i18n | User-facing role/permission labels remain bilingual ({ en, vi }) |
8. UX & Flows
Key surfaces are backend: enforcement lives in @nx/core (application/verifier.ts, security/casbin-model.ts, security/application-casbin-adapter.ts, utilities/request.utility.ts); commerce/finance/core resolve scope from policy; inventory is the pilot service. There is no dedicated end-user screen - this increment is the authorization substrate other modules rely on.
9. Data & Domain
| Entity | Role |
|---|---|
Casbin policy (p) | A grant: subject, resource, action, scoped to a merchant domain |
Casbin grouping (g) | Membership - user→role, user→org/merchant, role→org/merchant; tenant-isolated by variant |
| Scoped Casbin adapter | Loads policy/grouping rows soft-delete-aware and per-merchant (per-schema) |
| Active merchant domain | The merchant a request operates in, selected via x-merchant-id |
| Casbin model | Scoped RBAC model defining how grants resolve within a domain |
Conceptual only - full policy schema and adapter contract in the developer Casbin Authorization docs and RBAC docs.
10. Dependencies & Assumptions
Depends on
- Fixed & custom roles + grants (URD-ROLE · URD-GRANT) - the policy rows the engine enforces.
- Commerce (
@nx/commerce) - organizations and merchants are the scopes grants attach to. - Identity (
@nx/identity) - sign-in token carries role context; token refresh triggers cache invalidation. - IGNIS framework - the scoped Casbin model the application-level adapter aligns with.
Assumptions
- Requests carry an active-merchant header (
x-merchant-id) selecting the domain. - A merchant's policy/grouping rows exist in its own schema (per-merchant isolation).
- Owner roles are backfilled and inventory role permissions seeded for the pilot.
11. Risks & Open Questions
| Risk / question | Mitigation / status |
|---|---|
| Framework adapter ignores soft-delete | Application-level adapter in @nx/core honours soft-delete; no IGNIS edit |
| Cross-merchant policy leakage | Membership query constrains the variant; rows isolated per schema |
| Stale enforcement after role/permission change | Casbin cache cleared on token refresh |
| Framework Casbin model drifts from app expectations | Track scoped model; migrate to the new scoped adapter as upstream changes |
| Broad enablement beyond inventory pilot | Roll out per service after the inventory pilot validates end-to-end |
12. Release Plan & Launch Criteria
| Aspect | Plan |
|---|---|
| Phase | P2 (EFF + GRANT, Built) - see URD feature catalog |
| Rollout | Inventory pilot first, then broader verifier-service enablement |
| Migration | Seed inventory role permissions; backfill owner roles |
| Launch criteria | Central enforcement verified in the verifier; scope resolved from policy matches grants; soft-delete and per-schema isolation verified; inventory allowed-merchant resolution correct end-to-end |
| Monitoring | Authorization decision path coverage, foreign-merchant access attempts, cache-invalidation on token refresh, pilot error rate |
13. FAQ
Where is authorization enforced now? Centrally, in the VerifierApplication via the Casbin enforcer - every verifier service uses the same path instead of hand-filtering from request context.
How is "which merchant" decided? Per request, by the active-merchant header (x-merchant-id); a grant resolves only within that merchant domain.
Why an application-level adapter instead of the framework's? The framework adapter does not honour soft-delete and KICKO needs per-merchant (per-schema) isolation; the @nx/core adapter provides both with zero IGNIS edits.
Does revoking a grant take effect immediately? Revocation soft-deletes the policy row, which the scoped adapter excludes; a token refresh clears the Casbin cache so changes apply cleanly.
Why pilot on inventory first? To validate end-to-end per-merchant enforcement (seed + owner backfill + policy-resolved merchant IDs) on one service before broader rollout.
Is wildcard / hierarchy permission part of this? No - coarse grants via the resource/action/domain hierarchy are owned by PRD-HIER-001.
References
- URD: Permissions - Effective Permissions & Scope · Grant / Revoke · Fixed Roles
- Related PRD: Resource, Action & Domain Hierarchy
- Module: Permissions - overview + traceability
- Developer: @nx/core · @nx/identity · Casbin Authorization