Atribuição de papéis — API
Conceder, listar, atualizar a expiração e revogar a atribuição de um papel a um usuário — num tenant inteiro ou numa unidade específica — mais as rotas de leitura de permissões efetivas e o diretório que junta todas as atribuições visíveis de um usuário numa única resposta.
Funcionamento
- Conceder (
POST .../role-assignments): grava um vínculouserId↔roleIdno escopo informado (tenant-wide, viatenants/:tenantId/users/:userId/role-assignments, ou de unidade, viaunits/:unitId/users/:userId/role-assignments), comexpires_atopcional. Se já existir uma atribuição ativa e não expirada para o mesmo usuário/papel/escopo, a chamada é idempotente e devolve a atribuição existente sem criar uma nova. Se existir uma atribuição expirada ou inativa para o mesmo trio, ela é retirada e uma nova identidade de atribuição é criada (oidmuda) — não há reativação de um registro antigo. - Listar: quatro variações leem o mesmo tipo de registro em recortes diferentes — por unidade
e usuário, por unidade inteira, por tenant e usuário, por tenant inteiro — todas paginadas e
filtráveis por
role_id/search/user_id. - Diretório (
GET /users/:userId/role-assignments): junta, numa única leitura sem paginação, toda atribuição ativa do usuário — tenant-wide e por unidade — em todo tenant/unidade que o ator autenticado (não o usuário-alvo) tem permissão de enxergar. Nunca revela uma atribuição de um tenant ou unidade fora do escopo do ator: essas linhas são omitidas, não mascaradas. - Atualizar expiração (
PATCH .../role-assignments/:assignmentId): exige uma data no futuro; datas passadas ou inválidas são rejeitadas antes de tocar o banco. - Revogar (
DELETE .../role-assignments/:assignmentId): soft-delete lógico (isActive: false); não apaga o registro. - Permissões efetivas (
GET .../effective-permissionse.../effective-mask): calculam a união das permissões de todo papel ativo do usuário naquele escopo — a primeira devolve os nomes de permissão, a segunda o bitmask decimal equivalente.
Toda concessão/atualização/revogação passa antes por
IdentityRolePermissionAssignmentPolicy.resolveActiveScope (o tenant/unidade alvo precisa estar
ativo) e depois por assertActorCanAssignRole — o mesmo controle de não escalonamento de
privilégio usado em Papéis: o ator só pode atribuir um papel se possuir, ele mesmo,
cada permissão daquele papel no escopo (tenant ou unidade) da atribuição.
Endpoints
| Método | Rota | Descrição |
|---|---|---|
| POST | /v1/units/:unitId/users/:userId/role-assignments | Concede um papel a um usuário numa unidade |
| GET | /v1/units/:unitId/users/:userId/role-assignments | Lista atribuições ativas do usuário naquela unidade |
| PATCH | /v1/units/:unitId/users/:userId/role-assignments/:assignmentId | Atualiza a expiração de uma atribuição de unidade |
| DELETE | /v1/units/:unitId/users/:userId/role-assignments/:assignmentId | Revoga uma atribuição de unidade |
| GET | /v1/units/:unitId/users/:userId/effective-permissions | Permissões efetivas do usuário naquela unidade |
| GET | /v1/units/:unitId/users/:userId/effective-mask | Bitmask efetivo do usuário naquela unidade |
| GET | /v1/units/:unitId/role-assignments | Lista toda atribuição ativa numa unidade |
| POST | /v1/tenants/:tenantId/users/:userId/role-assignments | Concede um papel a um usuário no tenant (tenant-wide) |
| GET | /v1/tenants/:tenantId/users/:userId/role-assignments | Lista atribuições ativas do usuário naquele tenant |
| PATCH | /v1/tenants/:tenantId/users/:userId/role-assignments/:assignmentId | Atualiza a expiração de uma atribuição de tenant |
| DELETE | /v1/tenants/:tenantId/users/:userId/role-assignments/:assignmentId | Revoga uma atribuição de tenant |
| GET | /v1/tenants/:tenantId/users/:userId/effective-permissions | Permissões efetivas do usuário naquele tenant |
| GET | /v1/tenants/:tenantId/users/:userId/effective-mask | Bitmask efetivo do usuário naquele tenant |
| GET | /v1/tenants/:tenantId/role-assignments | Lista toda atribuição ativa num tenant |
| GET | /v1/users/:userId/role-assignments | Diretório: toda atribuição do usuário visível ao ator, sem paginação |
Versão: v1
Swagger: Identity — Role assignments · Identity — Tenant role assignments · Rota (Dev): http://localhost:3000/v1/units/{unitId}/users/{userId}/role-assignments
Permissões
| Rota | Guards | Permissão exigida |
|---|---|---|
POST, PATCH, DELETE .../role-assignments (unit ou tenant) | IdentityOAuthAccessTokenGuard, AuthorizationGuard | role:manage no escopo (unit ou tenant) |
GET .../role-assignments (por usuário, por unidade/tenant inteiro) | idem | role:read no escopo |
GET .../effective-permissions, .../effective-mask | idem | permission:read e user:read (match: all) no escopo |
GET /users/:userId/role-assignments (diretório) | idem | role:read, match: any, escopo organization-list (filter: policy) — o ator precisa enxergar pelo menos um tenant/unidade pela política de listagem |
Headers
| Header | Obrigatório | Descrição |
|---|---|---|
Authorization | Sim | Bearer <access_token> |
Path parameters
| Nome | Tipo | Obrigatório | Descrição |
|---|---|---|---|
tenantId / unitId | uuid | Sim, conforme a variante | Escopo da atribuição |
userId | uuid | Sim | Usuário-alvo |
assignmentId | uuid | Sim, nas rotas de item | Atribuição alvo |
Query parameters
Toda rota de listagem (IdentityRoleAssignmentListQueryRequest):
| Nome | Tipo | Obrigatório | Default | Descrição |
|---|---|---|---|---|
page | integer | Não | 1 | Página (mínimo 1) |
limit | integer | Não | 20 | Itens por página (1 a 100) |
search | string | Não | — | Busca textual (até 128 caracteres) |
role_id | uuid | Não | — | Filtra por papel |
user_id | uuid | Não | — | Filtra por usuário (só nas listagens por unidade/tenant inteiro) |
O diretório (GET /users/:userId/role-assignments) não aceita query — devolve tudo de uma vez.
Body
POST .../role-assignments — AssignIdentityUserRoleRequest:
json{ "role_id": "0198f1a2-...", "expires_at": "2027-01-01T00:00:00.000Z" }
| Campo | Tipo | Obrigatório | Validação |
|---|---|---|---|
role_id | uuid v7 | Sim | @IsUUID('7') |
expires_at | string (ISO 8601) | Não | formato estrito; se ausente, a atribuição não expira |
PATCH .../role-assignments/:assignmentId — UpdateIdentityUserRoleExpirationRequest:
json{ "expires_at": "2027-06-01T00:00:00.000Z" }
| Campo | Tipo | Obrigatório | Validação |
|---|---|---|---|
expires_at | string (ISO 8601) | Sim | formato estrito e deve ser uma data futura, senão IDENTITY_USER_ROLE_EXPIRATION_INVALID |
Response
IdentityUserRoleAssignmentResponse (base de toda rota de atribuição):
json{"id": "3c9e...uuid","role_id": "0198f1a2-...","user_id": "77aa...uuid","tenant_id": "1f2a...uuid","unit_id": null,"granted_by": "16ab...uuid","granted_at": "2026-09-01T12:00:00.000Z","expires_at": null,"is_active": true,"is_effective": true,"is_expired": false,"created_at": "2026-09-01T12:00:00.000Z","updated_at": "2026-09-01T12:00:00.000Z"}
O diretório e as listagens por unidade/tenant inteiro estendem esse formato com role_name,
role_display_name e role_type (IdentityUserRoleAssignmentWithRoleResponse), para a UI nunca
precisar de uma segunda chamada ao catálogo de papéis.
Mapa de campos
| Campo | Tipo | Valores possíveis | Descrição |
|---|---|---|---|
unit_id | uuid | null | — | null numa atribuição tenant-wide; preenchido numa atribuição de unidade |
expires_at | string (ISO) | null | — | null = sem expiração |
is_active | boolean | — | false após revogação (soft-delete); nunca volta a true no mesmo registro |
is_expired | boolean | calculado | true se expires_at já passou, mesmo que is_active ainda seja true |
is_effective | boolean | calculado | is_active && !is_expired — é este campo que decide se a atribuição concede permissão agora |
role_type | string (diretório/listagens agregadas) | system, tenant_custom | Tipo do papel atribuído |
Erros
| Classe de erro | errorCode | Status | Quando ocorre |
|---|---|---|---|
| (validação de payload/UUID) | BAD_REQUEST / INVALID_UUID | 400 | corpo ou identificadores malformados |
| — | IDENTITY_USER_ROLE_EXPIRATION_INVALID | 400 | expires_at ausente/no passado (PATCH) |
UnauthenticatedError | IDENTITY_INVALID_OAUTH_ACCESS_TOKEN | 401 | access token ausente/inválido |
ForbiddenAction | IDENTITY_ROLE_PERMISSION_ESCALATION_FORBIDDEN | 403 | ator não possui, ele mesmo, alguma permissão do papel que está atribuindo |
ForbiddenAction (guard) | — | 403 | ator sem role:manage/role:read/permission:read+user:read no escopo |
ForbiddenAction (guard) | AUTHORIZATION_SCOPE_UNAVAILABLE | 403 | tenant/unidade alvo inexistente, inativo, ou fora do escopo do tenant informado |
IdentityUserNotFoundError | — | 404 | userId inexistente/inativo |
IdentityRoleNotFoundError | IDENTITY_ROLE_NOT_FOUND | 404 | role_id inexistente ou não visível no escopo |
IdentityUserRoleAssignmentNotFoundError | IDENTITY_USER_ROLE_ASSIGNMENT_NOT_FOUND | 404 | assignmentId inexistente, já revogado, ou de outro usuário/escopo |
Regras de negócio
| ID | Regra | Comportamento esperado |
|---|---|---|
| RN-01 | Uma atribuição é tenant-wide ou de unidade, nunca as duas | criada pela rota correspondente; um grant de tenant aplica-se a toda unidade daquele tenant, mas isso é resolvido na leitura, não gravado por unidade |
| RN-02 | Concessão repetida é idempotente enquanto a atribuição existente está efetiva | mesmo trio usuário/papel/escopo com atribuição ativa e não expirada → devolve a existente, não cria duplicata |
| RN-03 | Uma atribuição expirada/revogada nunca é reaproveitada | uma nova concessão para o mesmo trio, após expiração ou revogação, sempre cria uma nova identidade de atribuição |
| RN-04 | Sem escalonamento de privilégio na atribuição | o ator só concede um papel se possuir, ele mesmo, cada permissão daquele papel no escopo da atribuição (platformAdmin isento) |
| RN-05 | Um grant de unidade só é válido se a unidade pertencer ao tenant esperado | se a persistência do vínculo unidade↔tenant divergir do cadastro atual da organização, o grant de unidade é ignorado no cálculo de efetivo |
| RN-06 | O diretório nunca revela atribuição fora do escopo do ator | tenants/unidades que o ator não pode ler são omitidos da resposta, não mascarados |
| RN-07 | Revogação é lógica | DELETE marca is_active: false; o registro permanece para auditoria |
| RN-08 | Toda mutação incrementa a authorization_version do usuário afetado | a claim cacheada do usuário é invalidada na próxima leitura |
Compliance
| Órgão / norma | Exigência | Como a rota atende |
|---|---|---|
| LGPD / HIPAA | Least privilege / não escalonamento | RN-04 |
| HIPAA / ANVISA | Trilha de concessão/revogação de acesso | todo evento (identity.user_role.assigned/revoked/expiration_updated) grava outbox com autor, IP e antes/depois |
Variáveis de ambiente
Nenhuma variável de ambiente é consumida diretamente por estas rotas.
Tempo médio de resposta
A confirmar — responsável: time de Identity; data: 24/09/2026. Sem medição publicada.
Requisitos não funcionais
| Requisito | Definição |
|---|---|
| Idempotência | Sim, em POST (RN-02); PATCH/DELETE não são idempotentes no sentido de repetir sem novo estado, mas são seguros de repetir |
| Paginação | Sim, nas listagens por unidade/tenant/usuário (1–100, default 20); o diretório não pagina |
| Rate limit | Não identificado |
| Cache | Não identificado |
| Auditoria | Sim — outbox em toda concessão, atualização de expiração e revogação |
Relacionado
- 📂 Módulo: Authorization
- 🔁 Depende de: Papéis
- 🔁 Ver também: Minhas permissões (o próprio usuário consultando o resultado destas atribuições) e Inspeção de autorização (visão agregada com origem de cada permissão)