Skip to content

URD: Permissions

ModuleVersionUpdated
CORE-02v0.82026-07-15

What the Permissions module does

Defines how access to features and data is controlled. Permissions lets administrators assign roles to users, grant permissions to those roles, scope a role to a specific organizer or merchant, and have every query automatically filtered to exactly what the requesting user is allowed to see.

All of it runs on a Casbin priority-based RBAC model, with a per-merchant domain scope.

Six points to understand first

This is the most important part of the document. Understanding the six points below is understanding 80% of the module.

Throughout this section we use one running example. Hoa is the Owner of the organizer Sau Beo Coffee, which has 3 merchants: Sau Beo District 1 (headquarters), Sau Beo Thu Duc, and Sau Beo Go Vap. Tuan is a cashier at District 1. Mai is floor staff at Thu Duc.

1. Permissions are granted in clusters, not button by button

The natural instinct is to grant one permission per button. The system does not work that way. The whole system has around 755 grantable operations, grouped into 14 business domains: Sale, Inventory, Commerce, Payment, Finance, Pricing, Invoice, Tax, Ledger, Licensing, Account, Customer Care, Alerts, Support. Granting them one by one would mean the Owner role alone would need to carry all 755 permission rows.

Instead the system grants one row for a whole cluster, then lets it expand automatically. It expands in two directions.

The first direction is the resource tree: a business domain contains subjects, and subjects contain operations. A grant on the Sale domain automatically covers Sale Order, Sale Order Item, Shift, and everything beneath them.

The second direction is the action lattice: a higher tier satisfies every tier below it.

Level grantedWhat it includes
Full access (manage)Read + Write + Execute. The highest tier, covers everything.
WriteCreate, Update, Delete
ReadView only, nothing can be changed
ExecuteRun a business operation (close a shift, issue an invoice) that isn't an ordinary write

Put both directions together: Hoa's Owner role carries only 14 grant rows - one per business domain, at Full access. Tuan's Cashier role carries far fewer:

Business domainCashier's levelMeaning
SaleFull accessOpen an order, edit an order, close an order, close a shift
PaymentWriteTake payment, record a transaction
InventoryReadSee how much stock is left, cannot receive or issue
CommerceReadView the item catalog, cannot change prices
InvoiceReadView the invoice status of the order just sold

Tuan has no row at all for Pricing, Tax, Ledger, or Account. No row means no permission - there's no need to spell out a "deny".

2. Effective permissions merge from multiple sources, and a deny always beats an allow

A person's permissions come from two sources, combined:

  1. Role-based permissions - Tuan is a Cashier, so he has everything the Cashier role has.
  2. Permissions granted directly to the individual - Hoa grants Tuan Read on Pricing directly, even though the Cashier role doesn't carry it.

The two sources merge and then deduplicate: a permission present in both sources only counts once. When a screen asks "what can Tuan actually do", the system returns this merged set. It can also be queried separately - "direct grants only" or "role-inherited only" - for auditing who granted what to whom.

But merging isn't a plain union. A grant row doesn't just carry a level - it also carries an effect: allow or deny. And the rule is: a deny beats an allow, no matter how much broader the allow is.

This is the most counter-intuitive part of the whole module. It exists so a merchant owner can punch a small hole in a large grant, instead of dropping the large grant and re-enumerating every small operation by hand.

Three examples running live in the system today:

WhoAllow row (broad)Deny row (narrow)Real result
Mai (Employee)Write on the whole Sale domainDeny Write on ShiftMai can open and edit orders, but cannot open or close a shift - view only
Tuan (Cashier)Full access on the Sale domainDeny Full access on Shift AdjustmentTuan closes shifts normally, but cannot edit the numbers on an already-closed shift - only the Owner can
Hoa (Owner)Full access on the Account domainDeny Write on Root Permission CatalogHoa can view the permission list to grant to employees, but cannot invent new permissions herself - that's the system's job

Notice the last example: even the Owner - the highest role within an organizer - is still denied in a few places. "Owner has full access" is a false statement.

The one exception: the three internal roles Super Admin, Admin, and Operator of the operations team skip this machinery entirely. They pass straight through - no permission check, no deny check, no data filtering. So they don't need a single grant row either.

3. Priority tier: no one can touch a role at or above their own level

Every role carries a priority number. That number is the staircase for the whole system.

RolePriority
Super Admin999
Admin900
Operator600
Owner500
Cashier110
Employee100
Customer10
Guest1

A self-created role can only sit in the range 100 to 499 - always lower than Owner, always higher than or equal to Employee. No one can create a custom role that stands level with Owner.

The blocking rule fits in one sentence: to create, edit, delete, or grant a role, that role's priority must be STRICTLY LOWER than the actor's own highest priority. Equal doesn't pass either.

Hoa (Owner, 500) wants to create a role "Merchant Manager" at priority 300 for the manager at Thu Duc: allowed. She tries to set that role's priority to 500: blocked. She wants to edit the Operator role (600) of the operations team: blocked. She wants to assign the Owner role to someone else in her own organizer: also blocked, because Owner sits level with her.

One easy-to-miss detail: the system takes the highest priority among the acting user's own roles. If someone is both Employee (100) and Merchant Manager (300), their ceiling is 300, not 100.

The priority tier also decides what's visible: when Hoa opens the role picker to assign a role to an employee, the list only shows roles below 500. Operator, Admin, and Super Admin don't even appear in the list.

4. Grant ceiling: you can't hand out what you don't have

The priority tier only answers "which role can I touch". It doesn't answer "what permission can I put into that role". These are two separate locks, and both have to open.

The second rule: the level you grant someone else can never be higher than the level you yourself hold on that same resource.

Level you holdLevel you're allowed to grant others
Full accessRead, Write, Execute, Full access (all of them)
WriteWrite only
ReadRead only
ExecuteExecute only

Back to Sau Beo. Hoa has Full access on the Inventory domain, so she can grant the "Merchant Manager" role any level on Inventory.

But suppose Hoa grants "Merchant Manager" only Read on Inventory. The Thu Duc manager holds that role, and wants to create a further role "Stock Assistant" (priority 200 - lower than his own 300, so it clears the priority lock) and grant it Write on Inventory. Blocked. He only holds Read, so he can only hand out Read. He can't upgrade himself by creating a stronger role and assigning himself into it.

This lock only reads the permissions he holds through a role. Permissions granted directly to the individual don't raise his ceiling - that's a deliberate choice, better to block wrongly than open wrongly. And the three internal operations roles skip this lock too, like everything else.

5. Standing at one merchant means you only see that merchant

This is the most commonly misunderstood point, and getting it wrong leaks data.

Hoa owns all 3 merchants. Intuition says: when she opens the Orders screen she should see orders from all 3. She doesn't.

Every request to the system carries the active merchant - exactly one merchant, chosen by whatever the screen currently has selected. The data filter only filters by that exact merchant. When Hoa is standing at District 1, the Orders screen only shows District 1's orders. To see Thu Duc she has to switch the active merchant in the merchant picker, and the screen reloads.

Her permissions and the merchant she's standing at are two separate things:

QuestionAnswered by
What can Hoa do?The Owner role's grant rows (point 1)
Where can Hoa do it?The list of merchants she's a member of
Which merchant is this click happening at?The active merchant, carried on every request

The grant rows for Owner, Cashier, and Employee are not pinned to any single merchant. They take effect at any merchant the person is a member of. Which means the real boundary doesn't sit in the grant row - it sits in the membership list.

So: Tuan is a member of District 1, not a member of Go Vap. His Cashier role lets him open orders - but only at District 1. If he switches the active merchant to Go Vap, his Cashier permission doesn't follow him there, because he isn't a member there.

One small but important point for testing: when someone calls a record directly by its id, outside their scope, the system returns "not found", not "no permission". This is deliberate - answering "no permission" would inadvertently confirm the record exists.

There's one door outside this rule: permissions meant for a guest (someone not signed in yet, with no merchant at all) are tagged system-wide. That's what lets the new-organizer signup screen, plan lookup, and tax-code lookup keep working at a moment when no merchant exists yet to stand at.

6. Owner reach extends to a new merchant by copying membership in, not by inferring it

Hoa opens a 4th merchant: Sau Beo Binh Thanh. She can manage it right away, with no one granting anything extra. The question is: how does the system pull that off?

Intuition says: the system "understands" that Binh Thanh sits under the Sau Beo Coffee organizer, and that Hoa is the organizer's Owner, so it infers she has permission at Binh Thanh. That's not what happens. The system doesn't infer anything.

What actually happens: the moment Binh Thanh is created, the system scans backward, finds everyone who both holds the Owner role and is a member of the Sau Beo Coffee organizer - which is Hoa - and writes a new membership row: "Hoa is a member of Binh Thanh." That row is real, sits in the data, and can be counted.

The consequence worth remembering, because it's the source of most of the confusing permission bugs support sees:

  • The copy only runs once, at merchant-creation time. Anyone promoted to Owner after the merchants already exist doesn't automatically show up at the old merchants - membership has to be added per merchant.
  • To revoke an Owner's access at one merchant, you have to remove exactly that merchant's membership row. Removing it at the organizer level doesn't make the merchant rows disappear.
  • The number of membership rows grows with the number of merchants. An organizer with 30 merchants means each Owner carries 30 rows.

The three rules in points 3, 4, 5, plus this copying mechanism, explain nearly every "why can't this person click that button" question the support team fields daily.

What the module has

Seven feature groups. Click the AREA code to jump to the Details tab and read that group's requirements.

AREANameStatus
ROLEFixed RolesPartial
CROLECustom RolesPartial
PERMPermission CatalogBuilt
GRANTGrant / RevokePartial
EFFEffective Permissions & ScopePartial
HIERResource, Action & Domain HierarchyPartial
DECLPermission DeclarationPartial

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