Permissions
Permissions — Technical Spec
app/im2.py, template app/templates/perms.html. Reached from /settings ("Open Permissions"); there is no nav link.
Routes
| Method | Path | Auth | Notes | |
|---|---|---|---|---|
| GET | /perms | role == 'admin' (403 "Admins only") | Renders both matrices. | |
| POST | /api/perms | admin | kind (action\ | field), key, role, allowed (1/true/yes/on = on). Returns the resulting state. |
Tables
role_field_perms(role, field_name)— a row means "this role may edit this field". No row = not editable. Read byeditable_fields(role).role_action_perms(action_key, role)(scripts/schema.sql, PK(action_key, role), role CHECK against the four roles). No rows at all for anaction_keymeans "use the coded default",ACTION_DEFAULTSinim2.py. Read byaction_roles().- Audit:
audit_logrow per toggle,sku = 'PERM:{kind}:{key}',field_name = role,new_valueallowed/blocked,source='perms_admin'.
Code definitions
ROLES = ("admin","level1","level2","level3").EDITABLE_FIELD_LABELS— 15 field rows, in display order.ACTION_DEFS/ACTION_LABELS/ACTION_DEFAULTS— 12 action keys:item_create,item_qb_sync,item_upload,count_record,count_post,abc_run,abc_write,ro_view,ro_write,pick_do,pick_post,rush_priority_set.- Enforcement helpers:
require_action(user, key)→ 403"Your access level is not allowed to: {label}";can(user, key)for template gating;editable_fields(role)for the grid.
Hard-coded guards (save_perms)
kind='action'+role='admin'+ off → 400 "Admin cannot be removed from an action".kind='field'+role='level3'+ on → 400 "Level 3 is view-only by design".- Unknown
kind/key/role→ 400. - First-ever toggle of an action inserts the full
ACTION_DEFAULTSrow set for that key before applying the change, so the other roles' coded defaults don't silently vanish once the table starts overriding code. - Screens with a fixed
role == 'admin'test and therefore no row here at all:/users,/settings,/settings/rush-contacts,/permsitself. Also fixed-role (not data):ro_import.can_write/require_write(admin/level1/level2).
Caching
_PERM_CACHE and _ACTION_CACHE, PERM_TTL = 60 seconds, keyed by role / action key. save_perms clears both — in its own process only. Under more than one uvicorn worker or gunicorn child, other workers keep serving stale permissions for up to 60 s; that is the "within a minute" the operator page promises. The cache exists because editable_fields() is called once per field per row during a bulk upload.
Known limits / defects
- The
GET /permsrender for actions computes each cell as "any row exists for this key? then table membership, else coded default". Correct, but note the consequence: with rows present, an action with zero roles ticked other than admin is indistinguishable in storage from "defaults" only until the first toggle writes the defaults out. no_cost_expectedandno_cost_reasonare offered in the field matrix but are not inWH_FIELDS; ticking them has no effect on the Item Maintenance grid's editable set.- No CSRF token on
POST /api/perms(cookie auth +samesite=laxonly). - There is no "reset to defaults" action: once a key has rows, deleting the last non-admin row leaves an explicit table state, not a fall-back to
ACTION_DEFAULTS. - Permissions are per-role only. There is no per-user override anywhere in the build.
Permission gate (added 2026-09-06)
Every route in ro.py now depends on ro_user() instead of current_user directly. ro_user() calls require_action(user, "ro_view"), so revoking View Requirements Orders screens on the Field Permissions screen actually blocks the RO screens (403) — before this the key existed and nothing read it. All four roles hold ro_view by default, so nothing changed for anyone until someone revokes it. Pick and Rush screens live in ro_pick.py / rush.py and keep their own ro_write / rush_* checks.