Pular para o conteúdo principal

Associação Digital — Cargos, Permissões e RLS (TEC-194 / TEC-195)

Documento operacional do modelo de permissões implementado para a Associação Digital (Vona) sobre o Makerkit. Resume as decisões, o que foi entregue e o que ficou adiado (e por quê).

Modelo

  • Produto = team account (public.accounts, is_personal_account = false). Ex: abracaf, abcn, abrare, belite.
  • Usuário ↔ produto = public.accounts_memberships (user_id, account_id, account_role).
  • Cargos = catálogo global public.roles — definidos uma vez, servem em qualquer produto. As permissões ficam coladas no cargo (public.role_permissions + enum public.app_permissions).
  • Admin geral (MeResolve) = super-admin nativo do Makerkit (app_metadata.role = 'super-admin' + MFA/AAL2). NÃO é um cargo de produto — é global, por cima de todos os produtos. Ver migration 20250302043537_mfa-rls-super-admin.sql.
  • App teams-only: NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_ONLY=true. O usuário só vê produtos (times); o workspace pessoal fica oculto.

Catálogo de cargos (public.roles)

hierarchy_level é único; menor = mais poder.

cargonívelorigem (perfis TEC-194)
owner1dono do produto (Makerkit)
admin2admin_abracaf — renomeado de admin_associacao pela migration 20260724013204_rename_associacao_roles.sql
comunicacao3comunicacao_abracaf — renomeado de comunicacao_associacao
montadora4fiat_stellantis — renomeado de montadora_associacao
conc_grupo5conc_grupo
conc_loja6conc_loja
parceiro7parceiro
member8base (Makerkit)
consultor_fi9Belite (TEC-345)

Permissões adicionadas ao enum: content.manage (conteúdo editorial) e reports.read (relatórios). Mapeamento em apps/web/supabase/migrations/20260630171449_generic_roles_associacao.sql e no schema declarativo apps/web/supabase/schemas/17-roles-seed.sql.

TEC-557 adiciona 3 permissões específicas do Emplacamento/Intranet — reports.emplacamento.read (BI de emplacamento), reports.audit.read (auditoria de acesso) e reports.financial.read (dado financeiro sensível — CNPJ, faturamento individual) — sem redefinir reports.read, que continua o relatório genérico (Belite/comunicacao). member perde settings.manage/invites.manage (defesa em profundidade — nunca atribuído de fato); parceiro fica sem nenhuma permissão (área de parceiros ainda não existe). Ver a matriz completa em docs/matriz-de-acesso-emplacamento-intranet.md e o mapeamento nas migrations 20260731210000_add_emplacamento_permissions_enum.sql / 20260731220000_emplacamento_role_permissions.sql.

Padrão de RLS (a aplicar quando as tabelas de produto existirem)

As tabelas de produto da Associação Digital (events, partners, documents, posts, organizations, stores, partner_leads, emplacamento) ainda não existem no repositório. Quando forem criadas, aplicar RLS usando as helpers do kit — sem checagem manual de auth. Exemplo para uma tabela editorial:

-- conteúdo editorial: quem tem content.manage no produto pode escrever;
-- membros do produto podem ler.
create policy "events_read" on public.events for select to authenticated
using (public.has_role_on_account(account_id));

create policy "events_write" on public.events for all to authenticated
using (
public.has_permission(
(select auth.uid()), account_id, 'content.manage'::public.app_permissions
)
)
with check (
public.has_permission(
(select auth.uid()), account_id, 'content.manage'::public.app_permissions
)
);

O filtro de dado por escopo dos perfis conc_grupo (vê só o seu grupo) e conc_loja (vê só a sua loja) depende de colunas organization_id / store_id nessas tabelas e dos claims correspondentes no JWT (ver TEC-195 abaixo). Enquanto as tabelas não existem, fica só o cargo + a permissão (reports.read); o filtro entra junto com a modelagem das tabelas.

TEC-195 (Access Token Hook / claims no JWT) — ADIADO

No modelo escolhido o cargo é por produto e a autorização é resolvida em runtime por has_role_on_account / has_permission contra o [account] da URL — não há um role global no JWT. Os claims organization_id / store_id / partner_id dependem das tabelas de emplacamento/organização que ainda não existem. Portanto o hook não tem dado real para injetar hoje.

Implementar quando: (a) as tabelas de produto + o mapeamento usuário → organização/loja existirem, ou (b) uma API downstream (ex: API Placas / TEC-149) precisar consumir esses claims direto do JWT.

Produtos, clientes e isolamento (TEC-324)

O superapp tem dois eixos ortogonais:

  • Produto = layout/feature-set. Ex: emplacamento, belite, intranet, ganhaz. Definidos em apps/web/config/products.config.ts (registro PRODUCTS com features). É o produto que decide o layout.
  • Cliente = a org dentro de um produto (o "space"/team account). Ex: emplacamento → abracaf/abrare/abcn; belite → grupo-caminho/grupo-samam. O cliente é o escopo de dado (grupoId via association-config).

Cada team account (cliente) é marcada com seu produto em public_data.product. Uma org em vários produtos = várias contas (uma por produto). O seletor de spaces mostra o nome do produto (fallback para o nome da conta quando não marcada).

Resolução em runtime:

  • app/[locale]/home/[account]/_lib/server/load-account-product.ts — lê public_data.product de todas as contas do usuário (cacheado por request).
  • config/products.config.tsgetProductConfig(productId) / hasVonaExperience(productId). DEFAULT_PRODUCT = 'emplacamento': contas sem marca caem na experiência Vona (preserva o comportamento atual).
  • app/[locale]/home/[account]/layout.tsx — resolve o produto da conta atual e passa para o AppSidebar; rotula o seletor pelo nome do produto.
  • app/[locale]/home/[account]/_components/app-sidebar.tsx — produto sem experiência Vona usa o GenericAccountSidebar (Dashboard/Configurações/ Membros), sem relatórios/painel da abracaf.
  • app/[locale]/home/[account]/reports/layout.tsx — redireciona para /home/[account] se o produto não tiver features.reports.