---
title: "List all roles"
method: GET
path: "/api/roles"
tags: ["Permissions"]
---

# List all roles

`GET /api/roles`

The platform role catalogue — scopeable, and always LABELLED (F1, 2026-07-30).

This endpoint feeds the role dropdown of the system-admin user dialogs
(`RolesPermissionsPage.js`, via `useRolesPermissions.fetchRoles`), and it was
unscoped: **115 rows on the test DB**, because since 2026-07-29 every company
owns a private COPY of the catalogue, so the same six names repeat once per
tenant. The frontend renders them flat with
`getOptionLabel={(option) => option.name || ''}` — twenty identical entries
reading "socio", of which nineteen belong to somebody else's company. Picking
the wrong one was a coin flip, and `PUT /api/admin/users/{id}` then wrote it
straight into `Z_Users.idrole`, which `app/auth.py` resolves with no company
scoping at all. That is what made a cross-tenant `admin` grant a ONE-CLICK
mistake rather than a deliberate act.

The write path is now the real guard (`app/db/admin.py`: resolve against the
target row's company, refuse another tenant's role, refuse elevated content
without an explicit confirmation). This endpoint stops the mistake being
*offered*:

* **`company_id` scopes the list** to that company's own copies plus the
  system templates (templates are kept because `_resolve_role_for_company`
  maps a template to the company's copy, so they are legitimately pickable).
  This is what the user dialogs should pass.
* **Every row is labelled, unconditionally** — including the unscoped call,
  which the Roles tab genuinely needs (a `system_admin` managing the whole
  catalogue). `company_name` and a ready-made `label` are ADDED fields;
  `name` is left byte-identical on purpose, because the same list backs the
  Roles tab's edit form and decorating `name` would round-trip the decoration
  into `Z_Roles.name` on the next save.

`idcompany` was already in the payload and was already ignored by the client;
a name is what makes a foreign pick visible. See the report for the two
one-line frontend changes this enables.

**Also served since 2026-08-27, for the Roles tab clean-up** — all ADDED
fields, none replacing anything:

* `is_system` / `kind` — from `app/permissions/roles.py`, the SAME functions
  `GET /api/company-roles` uses. `is_system` alone conflates the two cases
  whose edit semantics differ (a template, whose rename cascades to every
  tenant's copy, versus one of those copies, whose name is the template's to
  set), so `kind` names all four.
* `holders` — how many people hold the row, from `role_holder_counts()`.
  In the list because the alternative is 115 round trips on mount, and
  because the delete confirmation must not be able to show a different
  number than the guard enforces.
* `copies` — for a template, how many company copies carry its
  `template_key`. This is the blast radius of a cascading rename.

The counts are two grouped queries merged in Python rather than a JOIN
inside `list_roles`, for the reason `admin_db.get_company_names` already
records: `list_roles`'s scoped form backs `list_assignable_roles`, a TENANT
route on the `admin` gate that fires on every invite-dialog open and does
not want the cost.

## Query parameters

- `company_id` integer, nullable — Empresa alvo. Quando indicada, devolve apenas os papéis dessa empresa mais os modelos de sistema — use-a sempre que a lista alimenta um seletor de papel para um utilizador.

## Headers

- `x-api-token` string, nullable

## Response `200`

Successful Response

- unknown

## Other responses

- `422` — Validation Error

---

[API](https://skmtc.dev/multigest/apis/multigest-protected-api.md) · [All operations](https://skmtc.dev/multigest/apis/multigest-protected-api/llms.txt) · [OpenAPI document](https://skmtc-service-production.skmtc.workers.dev/v1/apis/multigest/multigest-protected-api/revisions/4b44eb76b3ee/schema)
