Appearance
Contrato de leitura de tenants — trilho escopado (D11)
A leitura de tenants passa a ter dois trilhos, espelhando o padrão de users (Épico 889, F7):
| Trilho | Endpoints | Quem | Escopo |
|---|---|---|---|
| Tenant (escopado) | GET /api/v1/tenants, GET /api/v1/tenants/{id} | qualquer read:tenants | só os tenants que o caller pode ver |
| Admin (global) | GET /api/v1/admin/tenants, GET /api/v1/admin/tenants/{id} | admin:system (F9: manage:all-tenants) | todos os tenants |
read:tenants significa "ler os tenants que você pode ver", nunca a plataforma inteira. Um admin:system que chame o trilho tenant recebe o próprio escopo — a visão global vive só no trilho admin (simetria com o trilho de users).
Escopo do caller
O escopo é a lista de vínculos do caller (não o tenant do token):
- todos os tenants onde o caller tem um
UserTenantativo; - mais os filhos ativos de qualquer grupo onde o caller é GroupAdmin (enforcement real de
read:group-tenants).
Caller sem vínculos ⇒ lista vazia (não é erro).
GET /tenants — lista escopada
- Item:
TenantDto(contrato existente, inalterado). Filtrostype,isActive,hierarchyScope(+parentTenantId),search, ordenação e paginação continuam valendo dentro do escopo — inclusivehierarchyScope=groupede o resumoGET /tenants/summary(admin-list-summaries-contract). - Nenhum tenant fora do escopo aparece em nenhuma combinação de filtros.
GET /tenants/{id} — detalhe escopado (contrato summary)
Fora do escopo ⇒ 404 anti-disclosure (indistinguível de um id inexistente).
Payload:
TenantSummaryDto— apenas identidade e hierarquia:json{ "id": "…", "name": "…", "type": "Company", "isActive": true, "parentTenantId": "…", "parentTenantName": "…", "isGroupTenant": false }Sem campos operacionais/sensíveis: nada de
settings(política de MFA etc.),maxUsers,activeUserCount,domain,descriptionou timestamps. O contrato completo (TenantDto, comsettings) é exclusivo do trilho admin.
⚠️ type como string no summary
TenantSummaryDto.type serializa como string ("Company" / "Group"), padrão v5 — igual ao type do tenant na resposta de login. Cuidado com a assimetria: o TenantDto da lista ainda serializa type como int (compatibilidade). Ao consumir os dois endpoints, trate type do detalhe como string e o da lista como int (ou normalize no cliente).
Trilho admin (global) — GET /admin/tenants
GET /api/v1/admin/tenants: lista global paginada, com paridade de filtros com o trilho tenant (type,isActive,hierarchyScope+parentTenantId,search, ordenação, paginação). Item:TenantDtocompleto.GET /api/v1/admin/tenants/{id}: detalhe global comTenantDtocompleto (inclui os campos operacionais que o summary omite —maxUsers,activeUserCount,domain,description, timestamps).- Gate:
admin:system(F9 passa a aceitar tambémmanage:all-tenants). Sem ele ⇒ 403; sem token ⇒ 401. A query éICrossTenantRequest(acesso cross-tenant exigido pelo pipeline).
Campo de referência — GET /tenants/lookup e GET /admin/tenants/lookup
Para selecionar um tenant ou mostrar o nome de ids já gravados (campo type: 'reference'), use o lookup do trilho, não a listagem: item { id, name, description, isActive? }, busca por nome ou domínio, até 50 por página e hidratação em lote com ?ids=. Mesma permissão e mesmo escopo da listagem do trilho. Contrato em tenant-lookup-contract.
Escrita permanece no TenantsController
As rotas de escrita de tenants — criar/atualizar/excluir e as operações de grupo — continuam nos endpoints atuais do TenantsController (POST/PUT/DELETE /api/v1/tenants…) com os gates atuais (create:tenants / update:tenants / delete:tenants / manage:child-tenants). Não há espelho de escrita em /admin/tenants — isso evita breaking change de rotas. Após a despromoção (F8), apenas o operador global mantém essas permissões de escrita.
UI: capability em vez de if de permissão
A UI de tenants deve decidir o trilho por capability (tenants.default), não por checagem de permissão espalhada: perfil escopado consome /tenants; perfil global consome /admin/tenants. O mesmo componente serve os dois perfis, trocando apenas a base do endpoint. Escrita usa sempre /tenants (gated por permissão).