Dossiê — Acesso indevido ao admin (/admin/users)
Falha crítica #1 da auditoria "Manus AI" (NO-GO). ✅ RESOLVIDO (2026-09-04): FINDING-1 fechado — validação ao vivo provou o claim
email=ianffcs@neotek.wgno token+userinfo (checklist [OPERADOR] executado); deploy limpo atestado. Veredito do audit: GO CONDICIONAL (03-findings.md§Validação ao vivo). Veredito: não é uma falha de código do small-pine — é o painel admin do Open WebUI, cujo mapeamento de role depende de um merge por email que quebra quando o claim
1. O que a auditoria apontou
"Admin access indevido" em /admin/users — um usuário não-admin consegue ver o painel de administração de usuários.
2. O que /admin/users realmente é
/admin/users não é uma rota do small-pine. É o painel administrativo do Open WebUI (o frontend de chat do Chatsubo). A superfície admin do small-pine é /v1/admin/* (após o refactor reitit 22675bc), gateada pelo interceptor admin-token-gate (token compartilhado, não role).
| Superfície | Rota | Gate |
|---|---|---|
| small-pine admin | /v1/admin/* | admin-token-gate (Bearer token compartilhado) |
| Open WebUI admin | /admin/users | role admin na tabela user do OWUI |
3. Mecanismo de mapeamento de role no OWUI
O role de cada usuário no Open WebUI é decidido por três env vars + uma linha pré-seedada (ansible/roles/openwebui-jail-prod/tasks/configure.yml:611–616):
| Env var | Valor | Efeito |
|---|---|---|
ENABLE_OAUTH_SIGNUP | true | primeiro login SSO cria a conta local |
OAUTH_MERGE_ACCOUNTS_BY_EMAIL | true | liga o login SSO a uma linha pré-seedada por email |
DEFAULT_USER_ROLE | user | role de uma pessoa nova (admins pré-seedados mantêm o deles via merge) |
A linha admin é pré-seedada por email (configure.yml:394–395):
INSERT INTO "user" (id, name, email, role, ...)
SELECT '<uuid>', 'ianffcs@neotek.wg', 'ianffcs@neotek.wg', 'admin', ...
WHERE NOT EXISTS (SELECT 1 FROM "user" WHERE email = 'ianffcs@neotek.wg');
openwebui_admin_email: "ianffcs@neotek.wg" (defaults/main.yml:205).
4. Causa raiz do F5 (admin vê roster vazio)
O modo de falha está documentado em scripts/operator/t3-keycloak-realm.sh:337–342:
A federação LDAP é criada via REST, que não auto-cria os user-attribute mappers que a UI criaria. Sem um mapper
email <- mail, o atributoianffcs@neotek.wg, e oOAUTH_MERGE_ACCOUNTS_BY_EMAILdo Open WebUI falha em ligar o login SSO à linha pré-seedadarole='admin'— criando uma segunda contarole='user'e deixando o admin sem roster (F5).
Ou seja: o admin não ganha acesso indevido — ele perde o acesso (vira user), e o painel /admin/users aparece vazio. A correção é o mapper idempotente email <- mail:
{ "name": "email",
"providerId": "user-attribute-ldap-mapper",
"config": { "user.model.attribute": ["email"],
"ldap.attribute": ["mail"],
"read.only": ["true"],
"always.read.value.from.ldap": ["true"] } }
5. Fatores agravantes (identidade estável)
uuidLDAPAttribute: uid(nãoentryUUID) — o diretório é re-renderizado
a cada deploy (I38 "re-render / prune unmanaged DNs"), então entryUUID muda e orfana o LDAP_ID do Keycloak, quebrando o login com invalid_user_credentials (t3-keycloak-realm.sh:291–315).
- Reconcile de role no OWUI (
scripts/operator/t4-neotek-users.sh): o
roster ianffcs|admin pcms|admin ctni.teste|user evandro|user é aplicado via /api/v1/users/update/role; qualquer conta fora do roster é "parked" como pending (nunca deletada).
6. Gap estrutural (invariante de deploy)
O mapper email <- mail e a federação LDAP são configurados por um script manual de operador (t3-keycloak-realm.sh), que o deploy ansible não executa. Isso é a mesma classe de falha do ctni.teste (2026-08-28/29): a correção existe no repo mas nunca chega ao Keycloak vivo. O requisito durável do dono é que o loop SSO funcione desde o deploy ansible, sem fixes manuais.
7. E2E admin test — correção do harness (2026-09-06)
Após o mapper email <- mail ser promovido ao deploy, o teste E2E a-normal-account-is-refused-the-admin-surface ainda retornava nil para ambos os usuários. Três bugs no harness de teste foram corrigidos (e2e_mvp_v1_admin.clj, commit d49605f no small-pine):
js-executenão aguarda Promises.e/js-executemapeia para
:execute :sync do WebDriver, que retorna imediatamente sem resolver fetch().then(). A sonda session-email recebia nil para todo usuário. Corrigido para e/js-async com callback — o único modo WebDriver que aguarda um Promise antes de retornar.
- Falta de barra final na rota FastAPI. A sonda fetched
/api/v1/auths
(sem / final). Rotas router do FastAPI exigem a barra final; sem ela o request cai no fallback SPA e retorna HTML, não JSON. Corrigido para /api/v1/auths/.
localStorage.tokenpersistiu o JWT admin entre trocas de usuário.
delete-cookies limpa cookies mas não localStorage. OWUI guarda o JWT em localStorage.token, então o segundo browser manteve a sessão admin e a metade negativa viu o roster mesmo após trocar para ctni.teste. Corrigido navegando à origem OWUI e chamando localStorage.clear() entre os logins admin e regular-user (localStorage é per-origin).
Após a correção o teste passou contra prod: 1/1 PASSED, 0/1 FAILED (run 20260906T022327Z-689a252, 108s). Evidência: docs/mvp-v1/evidence/20260906T022327Z-689a252/.
8. Conclusão
- Não há acesso indevido a
/admin/users— o painel é do OWUI e o gate é
por role na tabela user do OWUI.
- O risco real é o inverso: o admin perder o role por um merge de email que
falha quando o claim email do Keycloak não bate com ianffcs@neotek.wg.
- A correção (mapper
email <- mail+uuidLDAPAttribute: uid) existe no
script T3, mas precisa ser promovida a passo do deploy ansible para cumprir o invariante de deploy.