feat(frontend): Platform > IA > Providers/Modelos/Uso/Custos + achado real de autorização
Providers/Modelos reaproveitam os endpoints /ai/providers e /ai/models já existentes (só filtram scope GLOBAL na tela); Modelos expõe custo unitário (inputCost/outputCost/audioCost) que a tela de tenant nunca mostra, porque só platform precisa cadastrar preço. GET /platform/ai-usage (novo) agrega uso bruto de AIUsageRecord de todos os tenants no mês corrente (Uso) e estima custo casando cada registro com o AIModel correspondente (Custos) — quando não dá pra casar, o registro fica de fora da soma e o tenant é marcado costIncomplete, nunca um número inventado. Achado real de autorização ao revisar AIModelsController antes de construir a tela de Modelos: POST/DELETE /ai/models não checava isPlatformUser quando o alvo era scope=GLOBAL — como a RLS híbrida (OR tenant_id IS NULL) deixa qualquer tenant ENXERGAR um provider/modelo GLOBAL, qualquer Tenant Admin com `ai.manage` (permission de escopo TENANT) conseguia injetar um modelo no catálogo global ou desabilitar um modelo GLOBAL só sabendo o id. O endpoint irmão (AIProvidersController) já tinha o check certo; corrigido com o mesmo padrão. Confirmado com um teste de ataque real: 403 depois do fix (era 201/sucesso antes), com regressão confirmando que BYOK do próprio tenant continua funcionando normalmente. Testado ponta a ponta: provider+modelo GLOBAL com custo real, uso de teste inserido direto no Postgres, /platform/ai-usage devolvendo o valor exato esperado (bate com a conta manual), tudo removido no final. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BFaBaBSQGhyXGEgtTYZGV8
This commit is contained in:
40
TODO.md
40
TODO.md
@@ -1617,6 +1617,46 @@ Usuários (agente.md secao 169)
|
||||
Puppeteer, mostrando a explicação por que falha aqui (mesmo texto
|
||||
já usado em Infraestrutura > Saúde)
|
||||
|
||||
## PHASE 45 — Platform > IA > Providers/Modelos/Uso/Custos (agente.md
|
||||
secao 96-103, 124, 169) + achado real de autorização em `/ai/models`
|
||||
- [x] achado real (achado revisando `AIModelsController` antes de
|
||||
escrever a tela de Modelos): `POST /ai/models` e `DELETE /ai/models/
|
||||
:id` não checavam `isPlatformUser` quando o provider/modelo era
|
||||
`scope=GLOBAL` — como a RLS híbrida de `ai_models`/`ai_providers`
|
||||
(secao 100, `OR tenant_id IS NULL`) deixa qualquer tenant ENXERGAR
|
||||
um provider GLOBAL, qualquer Tenant Admin com a permission
|
||||
`ai.manage` (escopo TENANT) conseguia **injetar um modelo no
|
||||
catálogo visível por todos os tenants**, ou **desabilitar** um
|
||||
modelo GLOBAL só sabendo o id — mesma classe de escalação já
|
||||
corrigida em `AgentsController` (PHASE 27) e já prevenida
|
||||
corretamente em `AIProvidersController` (o irmão deste controller,
|
||||
que já tinha o check certo). Corrigido com o mesmo padrão: 403
|
||||
explícito quando o alvo é GLOBAL e quem chama não é platform.
|
||||
Confirmado com um teste de ataque de verdade: tenant Acme tentando
|
||||
anexar um modelo a um provider GLOBAL (403 depois do fix, 201 antes)
|
||||
e apagar um modelo GLOBAL (403 depois, sucesso antes) — e um teste
|
||||
de regressão confirmando que Acme continua livre pra gerenciar o
|
||||
próprio BYOK
|
||||
- [x] Platform > IA > Providers/Modelos reaproveitam os mesmos endpoints
|
||||
`/ai/providers`/`/ai/models` já existentes (só filtram scope
|
||||
GLOBAL na tela, backend já filtra por RLS+isPlatformUser); Modelos
|
||||
expõe os campos de custo unitário (inputCost/outputCost/audioCost)
|
||||
que a tela de tenant nunca mostra, porque só platform precisa
|
||||
cadastrar preço
|
||||
- [x] `GET /platform/ai-usage` (novo) — uso bruto de `AIUsageRecord` de
|
||||
TODOS os tenants no mês corrente (Uso) + custo estimado casando
|
||||
cada registro com o `AIModel` correspondente por
|
||||
`providerId`+`externalModelId` (Custos). Quando não dá pra casar
|
||||
(provider apagado, custo nunca cadastrado), aquele registro fica
|
||||
de fora da soma e o tenant vem marcado `costIncomplete: true` —
|
||||
nunca um número inventado (secao 138/233)
|
||||
- [x] Testado ponta a ponta: criado provider GLOBAL + modelo com custo
|
||||
real (US$0,00015/tokenin, US$0,0006/tokenout), inserido uso de
|
||||
teste direto no Postgres (100k tokens in + 20k out), confirmado
|
||||
`/platform/ai-usage` devolvendo US$27,00 exatos (bate com a conta
|
||||
manual) e a tela de Custos mostrando o mesmo número — tudo removido
|
||||
no final
|
||||
|
||||
---
|
||||
|
||||
## Riscos conhecidos
|
||||
|
||||
Reference in New Issue
Block a user