> For the complete documentation index, see [llms.txt](https://docs.layeronecloud.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.layeronecloud.com/platform/identity/staff-roles.md).

# Staff roles

Console access used to be one bit.

## 1.7 Staff roles: Ticketing only, Support and Super admin <a href="#id-17-staff-roles-ticketing-only-support-and-super-admin" id="id-17-staff-roles-ticketing-only-support-and-super-admin"></a>

Console access used to be one bit. Hiring one person to answer tickets meant handing over Stripe keys, the Proxmox cluster editor, the IP pools and the control that promotes anyone to admin.

| Role           | Stored as                                     | Can do                                                                                                                                               |
| -------------- | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| Client         | `access_level=Client`, `staff_role=""`        | The client portal. No console.                                                                                                                       |
| Ticketing only | `access_level=Admin`, `staff_role=Ticketing`  | The ticket workbench in full, plus their own account pages. Every other console route is a 403, and the client area is redirected back to the queue. |
| Support        | `access_level=Admin`, `staff_role=Support`    | Tickets in full. Clients, services, usage, receipts, incidents, monitoring read-only.                                                                |
| Super admin    | `access_level=Admin`, `staff_role=SuperAdmin` | Everything, including staff management and `/console/django-admin/`.                                                                                 |

**Ticketing only is an allowlist of one app, not Support with pages removed.** `TICKETING_ONLY_FULL_ACCESS_APPS` is `{"tickets"}` and is consulted *before* the Support table, so a read-only entry added to `SUPPORT_CONSOLE_ROUTES` later cannot widen this role by accident. It exists for outside helpdesk staff who answer tickets without being handed the platform around them, which is why they are the natural account for the dedicated ticketing hostname ([§8](/platform/support.md)).

That role has **one** area, so `access_mode_for_request` pins it to Admin and `record_access_mode` will not write Client for it — otherwise a session left in Client mode by a demotion would read back as the wrong area for the only app the account can open. It is also the one role `AccessModeRouteMiddleware` still redirects: a client-mode route is not somewhere this account can be, and a stale `/client/` link is a wrong turn rather than an attempt at something forbidden, which is why it is a redirect to the queue and not the 403 `StaffRoleRouteMiddleware` would give. For the same reason nothing may send it to `core:admin-dashboard` or `core:home`: sign-in, the already-signed-in redirect, the wrong-area redirect and the 403 page's exit all ask `accounts.access.admin_landing_url_name`, which answers `ticketing:admin-queue` here. Reversed, never hardcoded, so it resolves to `/` on the dedicated host and `TICKETING_PATH_PREFIX` on the main domain.

**Why two fields and not a third `AccessLevel`.** \~155 `@admin_required` views, `AccessModeRouteMiddleware`, the ticket and incident assignee pickers and the staff-reply path all ask `access_level == Admin`, and a Support agent is staff by every one of them. Widening the enum would have meant auditing every one of those call sites to let them back in, and missing one would look like a bug rather than a permission. So `access_level` answers "is this person staff" and `staff_role` answers "how much" — which is a **routing** question.

**Enforcement is a route allowlist, not decorators.** `apps/accounts/staff_access.py` lists the console routes Support may open and `StaffRoleRouteMiddleware` checks it once per request on the *resolved* route, before any view runs.

* **Default deny.** A new console area is invisible to Support until it is listed, because the opposite failure (forgetting to deny) is silent.
* Read-only entries admit `GET`/`HEAD` only, so visible never means writable.
* `/console/django-admin/` is refused **by name**: it is a *shared* path for the mode middleware, so it never reaches the admin-mode branch. (It still moves to the admin hostname with the rest of the console — `AdminSiteHostMiddleware` names it explicitly for exactly that reason.)
* A blank or unrecognised `staff_role` on an Admin row reads as **Support** — never Super admin, and never Ticketing only either. Every creation path sets it explicitly, so blank is an unknown; an unknown gets the smaller grant, but it must not silently *lose* access it already had.
* `User.is_support_staff` stays true for Ticketing only. Every `console_nav_is_support_staff` guard hides a control Support may not use, and a narrower role may not use it either; `is_ticketing_only` is the extra question asked on top.
* Roles are written only through `apps/accounts/roles.apply_console_role` (it keeps `is_staff` in step), by a super admin, and **never on their own row**. That self-rule is also what guarantees the last super admin cannot be demoted.
* `super_admin_required` on grant/revoke endpoints is deliberate belt-and-braces: the middleware decides from the URL, the decorator from the user, so a route later renamed or reached some way that skips the middleware still cannot promote anyone.

**Hiding nav entries and controls is cosmetic, not the check.** But **a link on a Support-readable page must point at a Support-readable route**, or be wrapped in `{% if not console_nav_is_support_staff %}` (and `console_nav_is_ticketing_only` for the narrower role — the ticket queue, preview and detail pages render the customer's name as text rather than a link into billing for it): a button that always 403s is still a bug, and it is invisible to whoever adds it because they are a super admin and the page looks right. `SupportVisibleLinkTests` renders every Support-reachable console page and resolves each `/console/` href back through the allowlist; its companion test asserts a super admin still sees the same controls, so the guards cannot decay into deletions. Client-facing templates never link admin console routes at all.

**Staff management** lives at `/console/account/staff/` (super admin only). Adding staff **sets a password rather than emailing an invite**: a staff row with no usable password is the one state the account system cannot repair, since password reset skips it and `is_claimable_placeholder_account` excludes anything privileged, so the address would be permanently dead. The add form *does* claim a passwordless placeholder (a new hire who once emailed support already has one); a real account with a usable password is refused with a pointer to the client page, so a customer's password can never be reset through it.

`accounts.0012_backfill_staff_role` set every existing Admin row to Super admin. Backfilling as Support would have been a silent mass demotion that nobody left could undo.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.layeronecloud.com/platform/identity/staff-roles.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
