Pular para conteúdo

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 role na 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 no pabast_users_screen), powersync (powersync.yaml).