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#
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#
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| Limite | O que ele impede |
|---|---|
profundidade | usuario.pedidos.cliente.pedidos… num grafo com ciclo |
complexidade | uma consulta curta que pede um milhão de itens |
itens | um 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:
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#
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:
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.errodiz 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:
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.