# Roles and permissions

Three ranks, enforced server-side on every route. What each can read and write, which operations are privileged, and how a token narrows further.

Source: https://tokentriage.com/docs/build/dashboard-roles

---
import Callout from '../../../components/Callout.astro';
import Details from '../../../components/Details.astro';
import NextSteps from '../../../components/NextSteps.astro';

Access on the admin plane is three ranks, checked on the server for every route. This page is
the complete description: what each rank can do, which operations sit behind an extra gate,
and how a bearer token can be narrowed below its own rank.

[Sign in to the dashboard](/docs/build/dashboard-auth) covers creating the credentials these
roles attach to. This page covers what they then permit.

## The three ranks

Ranks are ordered, and a higher rank includes everything below it.

| Role | Can | Cannot |
|---|---|---|
| `viewer` | every read, exports included | change anything |
| `operator` | viewer, plus writes that emit configuration | manage users, tokens, or delete data |
| `admin` | operator, plus user management, token management, retention deletion | — |

**Viewer means read parity, and the emphasis is on parity.** An export is a read, so a viewer
can export. A role that could see a figure but not export it would push people to screenshot
tables, which is worse for everyone.

**Operator is the rank that changes the running system.** Applying config, moving a ladder
rung, activating a calibration, running a replay, submitting feedback, silencing an alert,
writing a saved view or a schedule or a reconciliation, and writing a pricing override.

**Admin is the rank that changes who can do the above**, plus the one destructive capability
in the product: retention deletion.

## What each role can do, by area

| Area | viewer | operator | admin |
|---|---|---|---|
| Read any surface, query, export | y | y | y |
| Apply config through validate → diff → apply | — | y | y |
| Set the ladder mode | — | y | y |
| Activate a calibration | — | y | y |
| Run a replay or a policy preview | — | y | y |
| Submit feedback on a request | — | y | y |
| Silence an alert rule | — | y | y |
| Create, edit or schedule a saved view | — | y | y |
| Import an invoice for reconciliation | — | y | y |
| Write a pricing override | — | y | y |
| Add, remove or re-role a user | — | — | y |
| Mint or revoke a bearer token | — | — | y |
| Mint or revoke a share token | — | — | y |
| Delete data under a retention policy | — | — | y |

Assign the rank someone needs for the job in front of them. Most people who look at a
dashboard are viewers; the operator rank is for whoever is changing routing.

<Callout type="key">
Roles are enforced on the server, on every route, independently of what the interface offers.
A request that exceeds its rank is refused with a `403` naming the role that was required,
rather than a `404` that would pretend the route does not exist.
</Callout>

## Privileged operations

Seven operations sit behind an extra gate, because each one either changes who can act or
exposes something a read normally cannot reach.

| Scope | Operation |
|---|---|
| `config:apply` | apply a configuration change |
| `content:read` | read a captured request or response body |
| `keys:mint` | mint a virtual key |
| `share:admin` | mint or revoke a share token |
| `tokens:mint` | mint a bearer token |
| `tokens:write` | modify or revoke a bearer token |
| `users:admin` | manage users |

`content:read` is on that list for a reason worth stating: it is the only scope that returns
request or response *text*, and it applies only where [content
capture](/docs/build/dashboard-capture) has been turned on. Everything else on the admin plane
deals in metadata and money.

## Narrowing a token below its role

A bearer token carries a role, and it can also carry an explicit list of scopes narrower than
that role would grant. This is how you give a Grafana panel or a CI job exactly the access it
needs and nothing more.

The instance publishes its whole scope vocabulary, so you never have to guess a name:

```console
$ curl -s http://127.0.0.1:9090/tt/api/v1/capabilities | \
    python -c "import json,sys; print(json.load(sys.stdin)['management_api']['scopes'])"
['alerts:read', 'alerts:write', 'analytics:read', 'audit:read', 'auth:read', 'budgets:read',
 'cache:read', 'cache:write', 'config:apply', 'config:read', 'content:read', 'evals:read',
 'evals:write', 'events:read', 'evidence:read', 'evidence:write', 'exports:read',
 'feedback:write', 'fleet:read', 'fleet:write', 'governance:read', 'governance:write',
 'guardrails:read', 'instances:read', 'keys:mint', 'pricing:read', 'pricing:write',
 'providers:read', 'providers:write', 'quality:read', 'quality:write', 'reconcile:read',
 'reconcile:write', 'schedules:read', 'schedules:write', 'share:admin', 'tokens:mint',
 'tokens:read', 'tokens:write', 'users:admin', 'views:read', 'views:write', 'webhooks:read',
 'webhooks:write']
```

That output was line-wrapped to fit; the endpoint returns it as one JSON array.

A denial for a missing scope names the scope in a `TT-Required-Scope` response header as well
as in the problem body, so a caller can tell "I need a bigger role" apart from "I need this
one extra scope".

<Details summary="Two confinement rules that apply to tokens and not to sessions">

**A token can be bound to a subtree.** A management token may name a governance node, after
which it reaches only that node and everything under it. The root of the tree answers; any
node outside the binding gets a `403`.

**A token reaches only registered operations.** A session can reach anything its role permits;
a management token is confined to operations the server has registered, so a route added
without its registry entry is unreachable by token rather than reachable by accident. That
asymmetry is intentional and it fails closed.

Sessions carry one more requirement in the other direction: a session-authenticated write must
present the CSRF header. Token principals never do, because a token is not sent automatically
by a browser.

</Details>

## How this relates to virtual keys

Two access systems exist in this product and they govern different planes.

**Roles govern the admin plane** — who may read and change the dashboard and the management
API. That is this page.

**Virtual keys govern the data plane** — which callers may send traffic through the proxy,
under which limits, attributed to which tenant. That is
[Governance](/docs/build/governance-keys).

They meet in one place: minting a virtual key is an admin-plane operation, so it needs the
`keys:mint` scope and the admin role. Nothing else about them is shared, and a virtual key is
not a dashboard credential.

## What can go wrong

**Everyone is an admin.** With `auth.mode: off` every request resolves to a local admin,
which is correct on loopback and wrong anywhere else. The mode is the control, not the roles.

**An operator cannot mint a token for a colleague.** That is admin work. It is on the admin
side of the line because minting a credential grants access, and granting access is the thing
the top rank exists to hold.

**A viewer reports that a page is empty rather than refused.** A surface whose read is
declined says so on the surface. A navigation entry promises the screen exists, not that every
read inside it is permitted; [why your sidebar is
shorter](/docs/build/dashboard-navigation) covers the other reason a screen may be missing.

<NextSteps items={[
  {title: "Sign in to the dashboard", href: "/docs/build/dashboard-auth", description: "The three modes and how to create a credential in each."},
  {title: "Share a view without giving access", href: "/docs/build/dashboard-sharing", description: "The fourth option: a read-only link with no role at all."},
  {title: "Virtual keys", href: "/docs/build/governance-keys", description: "The other access system, governing who may send traffic through the proxy."},
  {title: "Request and response capture", href: "/docs/build/dashboard-capture", description: "What the content:read scope reaches, and why it is off by default."}
]} />
