Pular para o conteúdo

Segurança

Validação antes de executar, limites de profundidade e custo, rate limit, introspecção e observabilidade.

Um servidor de consulta tem uma superfície de ataque que uma API REST não tem: quem consulta escolhe a forma da consulta. Isso é o valor do modelo, e é também o risco.

Validar antes de executar#

dataforge
problemas := Lavra.validar(esq, texto)
given len(problemas) bigger 0:
    respond 200 json {"dados": void, "erros": problemas}

A validação percorre a árvore sem chamar resolvedor nenhum: nada é lido, nada é escrito. Lavra.executar já faz isso por padrão — validar_antes := no só existe para quem já validou e guardou a consulta.

Os três limites#

dataforge
Lavra.limites(esq,
    profundidade := 8,      // quantos níveis a consulta pode descer
    complexidade := 1000,   // o custo somado
    itens := 500)           // o teto de uma lista devolvida
LimiteO que ele impede
profundidadeusuario.pedidos.cliente.pedidos… num grafo com ciclo
complexidadeuma consulta curta que pede um milhão de itens
itensum resolvedor que devolve a tabela inteira num dia de pico

Os três são conferidos antes de resolver qualquer coisa. Descobrir isso resolvendo já é tarde.

Rate limit#

O limite por consulta não substitui o limite por cliente. Os dois são do Kiln:

dataforge
server api on 8080:
    middleware Kiln.rate_limit(60, 60)      // 60 pedidos por minuto, por IP

Lavra.montar(api, esq, "/lavra")

Sessenta consultas de custo 900 cada passam pelos dois limites e ainda derrubam o banco. Para isso, o limite tem de ser de custo por janela, e não de pedidos — o extensoes.complexidade de cada resposta é o número que se soma.

Introspecção em produção#

dataforge
Lavra.introspeccao(esq, no)

Um esquema exposto é um mapa do que existe para quem for procurar: nomes de campos internos, o tipo que só aparece no fluxo de pagamento, o argumento que ninguém deveria descobrir. Quem precisa do esquema é o time, e ele pode lê-lo do repositório.

Consultas guardadas#

Num cliente próprio — um app, um site que você escreve —, a consulta não precisa vir da rede. Guarde as consultas no servidor e aceite só o nome:

dataforge
consultas := {
    "painel": IO.read("consultas/painel.lavra"),
    "perfil": IO.read("consultas/perfil.lavra"),
}

route POST "/lavra":
    nome := body["nome"] ?? ""
    given nome not in consultas:
        respond 400 json {"erro": "consulta desconhecida"}
    respond json Lavra.executar(esq, consultas[nome],
        variaveis := body["variaveis"] ?? {})

Isso resolve os três problemas de uma vez: a superfície volta a ser fechada como a de uma API REST, o custo de cada consulta é conhecido, e o corpo do pedido fica pequeno.

O que nunca vai para a resposta#

  • A mensagem de erro do banco. Lavra.erro diz o que quem consulta precisa saber; o resto vai para o registro, com o rastro.
  • O campo que não está no esquema. É a garantia mais barata que existe.
  • O caminho do arquivo. Nenhuma mensagem do Lavra cita arquivo nem linha do servidor.

Observabilidade#

Toda resposta traz extensoes — tempo, campos resolvidos, profundidade, custo e o resumo dos lotes. É o que se registra:

dataforge
action registrar(r, ctx):
    Log.info("consulta", {
        "ms": r["extensoes"]["ms"],
        "custo": r["extensoes"]["complexidade"],
        "campos": r["extensoes"]["campos"],
        "erros": len(r["erros"]),
        "rastro": Malha.rastro(),
    })

O custo é a métrica que importa, e não o tempo: uma consulta cara que ficou rápida porque o cache estava quente volta a ser cara quando ele esfria.