Permissions & Modules
The EMS + CAPA suite follows ianaiERP's existing two-tier permission model:
- Module entitlements (per tenant) — toggles a whole feature on or off for the tenant. Stored in
common_keyvalueunder theMODULE_PERMISSIONkey. - Granular permissions (per user) — fine-grained read/write bits. Stored on the user and merged with the tenant's enabled modules at request time.
A user can only exercise a granular permission if BOTH the user has the bit AND the tenant has the module enabled. This means a single user object stays the same when their tenant turns CAPA on or off — the merge happens server-side on every API call.
Module entitlements (tenant-level)
| Entitlement | What it turns on |
|---|---|
EQUIPMENT_PERMITTED | Equipment Hub, asset status board, runtime status updates, asset detail page, work-center CAPA fields |
CAPA_PERMITTED | Capacity Planner page, capacity heatmap on the hub, CapaSnapshot RPCs, recompute |
MAINTENANCE_PERMITTED | Maintenance Hub, PM schedules, maintenance events, spare-part links, low-stock alerts |
Each entitlement is independent. A tenant can enable any combination — the Equipment Hub renders graceful "Enable {Module}" empty states for widgets whose module is off.
How to enable an entitlement
Admin path (ianaiERP staff): edit the tenant's MODULE_PERMISSION JSON in common_keyvalue to add the relevant *_PERMITTED string to the enabled modules array.
User-facing path (for self-serve tenants on the Business plan): Settings → Subscription → enable the addon module.
What happens after enabling
- The tenant's
MODULE_PERMISSIONrow gets updated. - The next API call from any user in that tenant runs through the auth middleware, which reads the row and applies
ModuleToPermissionMapto fill in the corresponding granular bits. - The sidebar refreshes, exposing the new menu items.
- The Equipment Hub starts rendering the previously-gated widgets.
No restart needed; no per-user provisioning needed.
Granular permissions (user-level)
These are bit-level permissions stored on each user. The seven new ones added for the EMS suite:
| Permission key | Grants |
|---|---|
equipment_read | View Equipment Hub shell, KPIs, status board, alerts feed |
equipment_write | Save dashboard layouts, trigger on-demand recompute, edit hub widget settings |
equipmentstatus_write | Update an asset's runtime status (operator-level — does NOT grant accounting edits) |
capa_read | View Capacity Planner, heatmap, snapshots, ETAs |
capa_write | Recompute capacity on demand, run "what-if" scenarios, drag-reschedule operations |
maintenance_read | View PM schedules, maintenance events, spare-part links, downtime history |
maintenance_write | Create/edit PM plans, log/complete maintenance events, mark PM done, link spare parts |
Reuse of existing permissions:
- Spare-part item flags (
is_spare_part,safety_stock_qty) reuseitem_read/item_write. - Asset hierarchy edits (parent reassign, move subtree) reuse
asset_write. - Work Center CAPA fields reuse
support_write.
Module → Granular mapping
When a tenant enables an entitlement, the auth middleware automatically grants the corresponding granular bits to users who already have those granular permissions assigned:
| Entitlement | Auto-granted granulars |
|---|---|
EQUIPMENT_PERMITTED | equipment_read, equipment_write, equipmentstatus_write |
CAPA_PERMITTED | capa_read, capa_write |
MAINTENANCE_PERMITTED | maintenance_read, maintenance_write |
This is gating, not granting — the user still needs the granular bit on their user record. The module entitlement just unblocks it.
Typical role recipes
These aren't built-in roles (ianaiERP doesn't have role presets yet) — they're patterns for assigning granular perms to users.
Plant manager
equipment_read, capa_read, maintenance_read
asset_read, item_read, support_read
Read-only everything in the suite. Sees the hub, the planner, the maintenance history. Can't make changes; can call attention to issues.
Production planner
equipment_read, capa_read, capa_write
asset_read, item_read, support_read
Can use the Capacity Planner to recompute and rebalance. Can't change asset status or maintenance events.
Maintenance lead
equipment_read, maintenance_read, maintenance_write
equipmentstatus_write
asset_read, item_read, item_write (for spare parts)
support_read
Creates PM plans, logs events, links spare parts, marks PMs done. Can flip asset runtime status (e.g. take something to MAINTENANCE before starting an event manually).
Floor operator (tablet)
equipment_read
equipmentstatus_write
Sees the hub, can flip asset runtime status. Cannot edit accounting fields, cannot create maintenance events (the auto-created BREAKDOWN event from a DOWN action is fine — they don't "create" it, the system does).
Finance / asset accountant
asset_read, asset_write
(no equipment_*)
Doesn't need the EMS suite at all. Works on /asset for acquisitions, depreciation, disposal.
Page-level gating in the UI
Each page checks both module + granular before rendering:
| Page | Requires (module) | Requires (granular) |
|---|---|---|
/equipment (Hub) | EQUIPMENT_PERMITTED | equipment_read |
/equipment/:assetId (Detail) | EQUIPMENT_PERMITTED | asset_read |
/equipment/capacity (Planner) | CAPA_PERMITTED | capa_read |
/equipment/maintenance (Maintenance Hub) | MAINTENANCE_PERMITTED | maintenance_read |
/workcenter (WC list) | (no EMS module needed) | support_read |
/asset (Asset list) | (no EMS module needed) | asset_read |
If either check fails, the page renders a NoPermissionBlur with a hint about what's missing.
Cross-module widget gating on the Hub
When a tenant has only some modules enabled, the Equipment Hub shows what it can:
| Tenant has | Hub renders | Hub shows "Enable …" card for |
|---|---|---|
Only EQUIPMENT_PERMITTED | Shell, KPIs (partial), status board, alerts | Capacity Heatmap, Today's PM Schedule |
EQUIPMENT + CAPA | All of above + capacity heatmap | Today's PM Schedule |
EQUIPMENT + MAINTENANCE | All of above + PM schedule + maintenance alerts | Capacity Heatmap |
| All three | Full hub | — |
| None | Page returns a NoPermissionBlur | — |
This intentionally avoids hiding the cards — admins seeing the empty-state cards know what to enable.
Cross-permission action gating
For granular per-action checks (capa_write, maintenance_write, equipmentstatus_write), the UI reads from the merged-permission store:
const can = useUserState((state) => state.mergedPermission)
<Button disabled={!can?.capa_write}>Recompute</Button>
mergedPermission is the user's granular bits AND-ed with the tenant's enabled modules. Both must be true for the action to be enabled.
REST API auth
Every new HTTP path is registered in auth-ms's allowlist with the required granular perm:
| Path prefix | Verb | Required granular |
|---|---|---|
/equipment/hub | GET | equipment_read |
/asset/:id/status | PUT | equipmentstatus_write |
/asset/:id/tree | GET | asset_read |
/asset/move-subtree | POST | asset_write |
/workcenter (CRUD) | * | support_* |
/maintenance/schedule (CRUD) | * | maintenance_* |
/maintenance/event (CRUD + start/complete/markPmDone) | * | maintenance_* |
/sparepart (CRUD) | * | maintenance_* |
/capa/snapshot (list/get/getLatest) | GET | capa_read |
/capa/calculate | POST | capa_write |
Casbin enforces these per request. Calls without the bit return 403.
Troubleshooting
| Symptom | Likely cause |
|---|---|
| User can see the sidebar nav but the page returns NoPermissionBlur | The sidebar shows based on *_read; the page requires both the module entitlement and the read bit. Module is probably off. |
| Recompute button is greyed out | User has capa_read but not capa_write. |
| "Spare part" toggle doesn't save | User lacks item_write. |
| Quick Menu missing on chip | User lacks equipmentstatus_write. |
Module enabled in MODULE_PERMISSION but page still blocked | Cache. Force a re-login (the token holds permissions for the session lifetime). |
| New permission key isn't recognized | Library permission.go wasn't redeployed to the relevant service. Check permissionMap. |
Related
- Introduction → Module entitlements — how the modules slot into the broader platform
- Asset Management → Permissions split — the asset_write vs equipmentstatus_write distinction
- Runtime Status — the operator-tier permission in practice