Permissions

List all 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.

get/api/roles

Query parameters

company_idinteger 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.

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-tokenstring nullable

Response

Successful Response

{"stackTrail":"paths:/api/roles:get:responses:200:content:application/json:schema","oasType":"schema","type":"unknown"}

Changes