0024 — Acesso unificado do usuário ao app (pAbast ∪ oauth), fail-closed¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-02 · Decidido em: 2026-09-02
Status: aceita · Data: 2026-09-02 · Contexto: o 0023
deu role (claim no JWT, gate da sync rule do PowerSync) só ao login social (oauth). Mas os apps
PowerSync (bi-comercial/Onpetro) são acessados também por staff pAbast (login ERP), cujo token
não tinha role → com a sync rule filtrando por auth.parameter('role'), todo usuário pAbast zeraria.
Linhagem .ia/016-*.
Decisão¶
1. Uma tabela de acesso para os dois tipos de login — app_user_access¶
A antiga oauth_access_requests evoluiu para app_user_access (migração V13): o acesso de
qualquer usuário ao app — subject_type ∈ {oauth, pabast} — numa tabela só, carregando status
(lifecycle) + role (papel). subject_key = firebase_uid (oauth) | id/ChaveUsu (pabast). email
virou nullable (pАbast não tem). Prod tinha 0 linhas → migração quase-schema.
Preterido: tabela pAbast dedicada (menos blast no 0023) — recusada em favor do modelo unificado
(um só lugar para a tela de Usuários, a notificação, o auto-lockout e o gate). Custo aceito: refactor
de ~6 classes do 0023 (OauthAccessRequest→AppUserAccess), com o contrato observável preservado.
2. pAbast vira "app-user" — app_id opcional no login¶
POST /api/auth/pabast/login aceita app_id opcional. Com ele o token carrega oauth_app_id/
oauth_cliente_id + role (de app_user_access(pabast, subject_key=id, cliente, app)),
simétrico com o oauth — e o pAbast passa a poder usar /api/apps/**. Sem app_id → token legado
(scope=full, sem role/app): não-quebra os consumidores atuais (vantroba etc.).
Chave canônica do usuário pAbast = id/ChaveUsu (não id_usuario): pabast_senhas não tem UNIQUE
em (cliente, id_usuario) — o mesmo id_usuario pode ter várias linhas (0008) — e o id é o mesmo
campo já usado como sub do token. Colisão de pessoa fica impossível.
3. Fail-closed + resolução por admin¶
Sem papel → sem claim role → a sync rule zera o bucket (login pАbast segue 200: a credencial
ERP é válida, papel ausente ≠ erro). O app mostra "Peça ao Admin definir seu acesso" e consome
GET /api/apps/admins-contato — os ADMIN ativos do (cliente,app) do claim ([{nome, email}],
email null quando o admin é pАbast; acessível a qualquer app-user, mesmo sem papel). Lista vazia
→ o cliente mostra "contate o suporte xadm". O 1º ADMIN é semeado por xadm (bootstrap) — não há
auto-bootstrap pelo tenant.
Preterido: default full-access (staff sem papel vê tudo) — recusado: o usuário escolheu fail-closed (papel ausente = sem dado, não "vê tudo").
4. Atribuição unificada — self-service (app-ADMIN) + paridade xadm¶
O app-ADMIN atribui papel a pАbast e oauth pela tela unificada de Usuários (/api/apps/usuarios,
pАbast LEFT JOIN app_user_access dedup por id): POST /api/apps/usuarios/pabast/{id}/role. O xadm
espelha em /api/admin/** (sem escopo no claim → cliente no path, app_id no body):
POST .../pabast/users/{clienteId}/{id}/role {app_id, role} (bootstrap), leitura
GET .../pabast/users?cliente=&app= (com app_role/app_status) e revoke
POST .../pabast/users/{clienteId}/{id}/access-status {app_id, status∈{ATIVO,INATIVO}} — sem
409 last_admin (o xadm é meta-admin/saída de emergência; o guard de auto-lockout fica só no
self-service app-ADMIN).
5. Contrato preservado para o central-ui¶
O response admin oauth continua emitindo a chave JSON firebase_uid (= subject_key das linhas
oauth) e email não-null — o painel faz parse hard-non-null (handoff central-ui, travado em teste).
Consequências¶
- Precede o gating por
rolena sync rule do PowerSync (bi-comercial) — sequenciamento obrigatório: central-backend (este contrato) → bi-comercial-xls (segmento+ backfill) → powersync (sync rule, por último; coluna/role ausente derruba a rule). - Fora deste repo (follow-ups declarados): bi-comercial (login
app_id, tela "peça ao admin"), central-ui (ação atribuir/revogar papel pАbast nopabast_users_screen), powersync (powersync.yaml).