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+ enumpublic.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 migration20250302043537_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.
| cargo | nível | origem (perfis TEC-194) |
|---|---|---|
owner | 1 | dono do produto (Makerkit) |
admin | 2 | admin_abracaf — renomeado de admin_associacao pela migration 20260724013204_rename_associacao_roles.sql |
comunicacao | 3 | comunicacao_abracaf — renomeado de comunicacao_associacao |
montadora | 4 | fiat_stellantis — renomeado de montadora_associacao |
conc_grupo | 5 | conc_grupo |
conc_loja | 6 | conc_loja |
parceiro | 7 | parceiro |
member | 8 | base (Makerkit) |
consultor_fi | 9 | Belite (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 emapps/web/config/products.config.ts(registroPRODUCTScomfeatures). É 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.productde todas as contas do usuário (cacheado por request).config/products.config.ts—getProductConfig(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 oAppSidebar; rotula o seletor pelo nome do produto.app/[locale]/home/[account]/_components/app-sidebar.tsx— produto sem experiência Vona usa oGenericAccountSidebar(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 tiverfeatures.reports.