Logosulfite.app
rafagazani/sulfite 999999

Grupos de relatórios

Como organizar relatórios em grupos tenant/global no sulfite_server

Grupos de relatórios#

Grupos organizam relatórios sem alterar o modelo de permissão. Eles funcionam como coleções nomeadas para filtros, menus e catálogos: o usuário ainda precisa ter permissão para listar, editar ou gerar os relatórios que aparecem no grupo.

O recurso está disponível para os backends memory, sqlite e postgres.

Modelo#

Um grupo tem:

CampoTipoDescrição
idstringIdentificador do grupo
namestringNome exibido para usuários
namespace tenant ou global Escopo do grupo
descriptionstringDescrição opcional
colorHex string Cor opcional do grupo em hexadecimal RGB (RRGGBB)
createdAtISO-8601Data de criação
updatedAtISO-8601Última atualização

namespace: tenant cria um grupo privado do tenant da request. namespace: global cria um grupo visível para todos os tenants e exige can_edit_global para criar, editar ou excluir.

Adicionar um relatório a um grupo não torna esse relatório visível para quem não teria acesso a ele. O grupo só organiza relatórios que já são visíveis no escopo da request.

Escopo das associações#

A associação entre grupo e relatório também tem escopo:

AssociaçãoQuem criaVisibilidade
Global Operador com can_edit_global em grupo global Todos os tenants
Tenant Usuário do tenant com can_edit Apenas o tenant da request

Isso permite um catálogo global com duas camadas: o operador publica grupos e relatórios globais para todos, enquanto cada tenant pode acrescentar seus próprios relatórios privados a um grupo global sem expor esses relatórios para outros tenants.

Listar grupos — GET /api/v1/groups#

Permissão: canList

Retorna grupos do tenant atual mais grupos globais. Sem paginação, a resposta é um array. Com page ou limit, a resposta usa o envelope paginado.

ParamTipoDescrição
pageintPágina, começando em 1
limitintItens por página, até 200
curl http://localhost:8080/api/v1/groups \
  -H "Authorization: Bearer $TOKEN"
[
  {
    "id": "grp-finance",
    "namespace": "tenant",
    "name": "Financeiro",
    "description": "Relatórios financeiros",
    "colorHex": "60A5FA",
    "createdAt": "2026-04-28T12:00:00.000Z",
    "updatedAt": "2026-04-28T12:00:00.000Z"
  }
]

Criar grupo — POST /api/v1/groups#

Permissão: canInsert para grupos de tenant; can_edit_global para grupos globais.

{
  "name": "Financeiro",
  "namespace": "tenant",
  "description": "Relatórios financeiros",
  "colorHex": "#60A5FA"
}
CampoObrigatórioDescrição
nameSimNome do grupo
namespace Não tenant por padrão; use global para catálogo compartilhado
descriptionNãoTexto livre
colorHex Não Cor livre do usuário. Aceita #RGB , RGB , #RRGGBB , RRGGBB , 0xAARRGGBB ou AARRGGBB ; persiste como RRGGBB sem alpha
StatusErroQuando
201Grupo criado
400 validation_error name ausente ou namespace inválido
403forbiddenPermissão insuficiente

Detalhes — GET /api/v1/groups/:id#

Permissão: canList

Retorna o grupo e os relatórios visíveis para o tenant atual dentro dele.

{
  "id": "grp-finance",
    "namespace": "tenant",
    "name": "Financeiro",
    "description": "Relatórios financeiros",
    "colorHex": "60A5FA",
    "createdAt": "2026-04-28T12:00:00.000Z",
    "updatedAt": "2026-04-28T12:00:00.000Z",
  "reports": [
    {
      "id": "rpt-cashflow",
      "name": "Fluxo de caixa",
      "namespace": "tenant",
      "createdAt": "2026-04-28T12:00:00.000Z",
      "updatedAt": "2026-04-28T12:00:00.000Z",
      "storageKind": "inline"
    }
  ]
}
StatusErroQuando
404 not_found Grupo inexistente ou não visível para o tenant

Atualizar — PUT /api/v1/groups/:id#

Permissão: canEdit para grupos de tenant; can_edit_global para grupos globais.

{
  "name": "Financeiro e Fiscal",
  "description": "Relatórios financeiros e fiscais",
  "colorHex": "2DD4BF"
}

name é obrigatório no update. O namespace do grupo não é alterado por este endpoint.

Envie colorHex: null ou omita o campo para remover a cor customizada. A UI do manager usa uma cor automática baseada no tema quando colorHex está ausente.

Excluir — DELETE /api/v1/groups/:id#

Permissão: canDelete para grupos de tenant; can_edit_global para grupos globais.

Excluir um grupo remove suas associações, mas não exclui os relatórios.

curl -X DELETE http://localhost:8080/api/v1/groups/$GROUP_ID \
  -H "Authorization: Bearer $TOKEN"

Adicionar relatório — POST /api/v1/groups/:id/reports#

Permissão: canEdit no escopo do tenant, ou can_edit_global para criar associação global em grupo global.

{
  "reportId": "rpt-cashflow"
}

Regras principais:

  • o relatório precisa existir e ser visível para o chamador;
  • operador com can_edit_global só cria associação global com relatório namespace: global;
  • usuário de tenant pode associar relatórios visíveis ao tenant, inclusive em grupos globais, mas essa associação fica privada para o tenant;
  • repetir a mesma associação é idempotente.
StatusErroQuando
204Associação criada ou já existente
400 validation_error reportId ausente
404not_foundGrupo ou relatório não encontrado
422 namespace_mismatch Operador tentou criar associação global com relatório não global

Remover relatório — DELETE /api/v1/groups/:id/reports/:reportId#

Permissão: canEdit no escopo do tenant, ou can_edit_global para remover associação global em grupo global.

curl -X DELETE http://localhost:8080/api/v1/groups/$GROUP_ID/reports/$REPORT_ID \
  -H "Authorization: Bearer $TOKEN"

Remover uma associação inexistente retorna 204.

Filtrar relatórios por grupo#

GET /api/v1/reports?group=<groupId> retorna os relatórios visíveis para o tenant atual dentro do grupo informado.

curl "http://localhost:8080/api/v1/reports?group=$GROUP_ID" \
  -H "Authorization: Bearer $TOKEN"

Quando group é usado, ele define o conjunto de resultados. Use GET /groups ou GET /groups/:id para descobrir os grupos disponíveis antes de filtrar.

Permissões por grupo — C3#

Além das permissões do token e da autorização por tenant, o Sulfite pode aplicar uma terceira camada de restrição por grupo. Essa camada é lida da view sulfite_group_permissions no banco do tenant.

A permissão efetiva é sempre `token ∩ tenantAuth ∩ groupPermission`. A view do grupo nunca aumenta permissões, apenas reduz o teto já concedido pelas camadas anteriores.

A view deve expor estas colunas:

ColunaTipo esperadoDescrição
emailtextEmail do usuário autenticado
group_id text Id do grupo em report_groups.id
can_list boolean Permite ver o grupo e usar GET /reports?group=
can_insert boolean Teto para criação no escopo do grupo
can_edit boolean Permite editar grupo tenant e alterar associações
can_deletebooleanPermite excluir grupo tenant

Exemplo:

CREATE VIEW public.sulfite_group_permissions AS
SELECT
  u.email,
  gp.group_id,
  gp.can_list,
  gp.can_insert,
  gp.can_edit,
  gp.can_delete
FROM app_users u
JOIN user_group_permissions gp ON gp.role_id = u.role_id
WHERE u.active = true;

Comportamento operacional:

SituaçãoResultado
View não existeFallback para permissões anteriores
View existe, mas não há linha para (email, group_id) Fallback para permissões anteriores
View existe e retorna linhaInterseção com as permissões anteriores
Token não tem email e a view existePermissões de grupo negadas
Erro de conexão ou queryRequest falha fechado com 503

can_edit_global não pertence à view. Ele vem apenas do token e continua sendo a autorização para criar, editar e excluir recursos globais.

A view `sulfite_group_permissions` mora no banco do tenant, portanto ela não controla grupos `namespace: global`. `GET /groups`, `GET /groups/:id` e `GET /reports?group=` ignoram C3 para grupos globais. Um desenho de permissões globais exigiria uma fonte central de autorização, fora do escopo deste recurso.

Persistência e migrações#

SQLite cria as tabelas automaticamente e usa chaves estrangeiras com cascade para remover associações quando um grupo ou relatório é excluído.

PostgreSQL em modo single-node também cria o schema automaticamente. Em cluster, o servidor não executa DDL no boot; aplique a migration antes de subir as réplicas:

psql "$DATABASE_URL" \
  -f packages/sulfite_server/migrations/005_report_groups.sql

Se SULFITE_CLUSTER_MODE=true e as tabelas report_groups, report_group_memberships ou a coluna report_groups.color_hex não existirem, o servidor falha no boot com uma mensagem pedindo a migration.

E2E com Postgres real#

A suíte inclui um teste opt-in para validar RFC-043 contra um Postgres real. Ele cria um schema temporário, sobe o server em processo com storage Postgres, cria a view sulfite_group_permissions, executa requests HTTP reais e remove o schema ao final.

cd packages/sulfite_server

RFC043_E2E_ENABLED=1 \
SULFITE_RFC043_E2E_DATABASE_URL="$DATABASE_URL" \
dart test test/rfc043_postgres_e2e_test.dart

Use uma URL de banco com permissão para CREATE SCHEMA, CREATE VIEW, CREATE TABLE e DROP SCHEMA. O teste isola tudo em um schema sulfite_rfc043_e2e_* para não tocar nos dados de demonstração.

Exemplo: grupo global com relatório global#

# Cria um grupo global.
curl -X POST http://localhost:8080/api/v1/groups \
  -H "Authorization: Bearer $OPERATOR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "Faturamento", "namespace": "global"}'

# Adiciona um relatório global ao grupo.
curl -X POST http://localhost:8080/api/v1/groups/$GROUP_ID/reports \
  -H "Authorization: Bearer $OPERATOR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"reportId": "rpt-invoice"}'

# Um tenant lista os relatórios visíveis dentro do grupo.
curl "http://localhost:8080/api/v1/reports?group=$GROUP_ID" \
  -H "Authorization: Bearer $TENANT_TOKEN"