Actions & permissions¶
Status: 📋 Planned / Design complete
The action and permission system is designed, not yet implemented. This page documents the agreed design so you can plan around it. Track it on the roadmap (P4).
An action (a spell, in Conjure's vocabulary) is something you do to rows beyond CRUD: export, issue a tax invoice, print a receipt, refund, send a push. The design lets you declare actions once, gate them per role, and enforce them on the server.
Four principles¶
- Permission = (action × model). Django permission codenames are bound to a model's
ContentType, soorder.print_receiptandsubscription.print_receiptare different permissions. "Allowed on Orders but not on Subscriptions" falls out naturally — no special casing. - Group = role. You don't manage N admins × M permissions directly. Define ~5–8 role groups with permission checklists and assign admins to groups. The Django admin Group editor is your model-permission matrix.
- Permissions are data, not migrations. Instead of
Meta.permissions(which needs a migration), async_admin_actionscommand idempotentlyget_or_creates the permission rows. Zero migrations. - The server enforces; the frontend only hides. The action endpoint checks
request.user.has_perm(...). Hiding a button is UX, not security.
The flow¶
graph TD
A["actions.py<br/>ADMIN_ACTIONS = [...]"] -->|sync_admin_actions| B["auth_permission rows<br/>(no migration)"]
B --> C["Django admin Group editor<br/>(define roles via checkboxes)"]
C --> D["Assign admins to groups"]
A --> E["schema sends per-(model,user)<br/>allowed_actions"]
E --> F["Frontend ActionBar<br/>(button + ⋯ menu; hides if not allowed)"]
F -->|POST /action/{key}/| G["Action endpoint<br/>has_perm enforced + audit log"]
D --> G
Declaring actions¶
A single source of truth on the backend:
ADMIN_ACTIONS = [
# model, codename, name, kind(client|server), scope(toolbar|bulk)
{"model": "user.User", "codename": "export", "name": "Export users", "kind": "server", "scope": "bulk"},
{"model": "order.Order", "codename": "print_receipt", "name": "Print receipt", "kind": "server", "scope": "bulk"},
{"model": "order.Order", "codename": "issue_tax_invoice", "name": "Issue tax invoice", "kind": "server", "scope": "bulk"},
{"model": "subscription.Subscription", "codename": "print_receipt", "name": "Print receipt", "kind": "server", "scope": "bulk"},
{"model": "notification.Notification", "codename": None, "name": "Send notification", "kind": "server", "scope": "toolbar"},
]
codename: Nonemeans no permission — available to every staff user (e.g. send notification).- Otherwise the action requires the
{app_label}.{codename}permission. - Same codename on different models → independent permissions (the Orders-vs-Subscriptions receipt case).
Creating the permissions (no migration)¶
The command walks ADMIN_ACTIONS and get_or_creates a Permission on each action's
model ContentType. Run it on deploy and whenever you add an action. The new permissions
appear automatically as checkboxes under their model in the Django admin Group editor.
The action endpoint¶
The endpoint requires is_staff, then checks the action's permission (has_perm, unless
codename is None); superusers always pass. It dispatches to the action handler,
returns a result with a partial-failure report, and writes an AdminAuditLog entry
(actor, action, targets).
Sensitive actions go through the endpoint
Export of PII and anything destructive must be a server action so the permission is truly enforced — a client-only action is bypassable by anyone with view access.
The frontend ActionBar¶
The schema sends per-(model, user) allowed_actions:
"actions": [
{ "key": "print_receipt", "label": "Print receipt", "scope": "bulk", "allowed": true },
{ "key": "issue_tax_invoice", "label": "Issue tax invoice", "scope": "bulk", "allowed": false }
]
A generic <ActionBar> uses allowed to filter visibility, renders the 1–2 common
actions as buttons and the rest under a ⋯ more dropdown (hybrid widget), routes
scope: "bulk" actions to the selection bar and scope: "toolbar" to the page header, and
handles confirm dialogs / toasts / refetch. A list page only needs one
<ActionBar model params selectedIds refetch />.
Worked example¶
The classic test case — "3 of 10 admins can export users; everyone can send notifications; receipts work on Orders but not Subscriptions" — is on the Scenarios page.