@neetru/db-authz
v0.1.1
Published
Motor unico de autorizacao por usuario final para o mundo nosql-vm (MongoDB) do Neetru Core DB. Decide se um usuario final de um produto SaaS pode ler/escrever uma colecao e impoe o filtro de propriedade. Funcao pura, zero dependencias, fail-closed em tod
Readme
@neetru/db-authz
Motor unico de autorizacao por usuario final do mundo nosql-vm (MongoDB)
do Neetru Core DB. Decide se um usuario final de um produto SaaS pode
ler/escrever uma colecao e impoe o filtro de propriedade que limita cada
um aos proprios documentos.
A biblioteca mais critica em seguranca do projeto Neetru Core DB: um bug aqui faz dados vazarem entre usuarios finais distintos de um mesmo produto SaaS.
Por que existe
O engine firestore tem Security Rules declarativas — "este usuario so ve
os proprios documentos" e configuracao. O MongoDB do nosql-vm nao tem nada
equivalente. Entao essa autorizacao precisa ser codigo: este pacote.
Ele e importado por dois chamadores:
- o caminho CRUD do Core (
get/list/add/set/update/remove); - o gateway realtime (
subscribe).
Tem de existir exatamente uma implementacao. Se o CRUD e o realtime divergissem, abriria-se um buraco de seguranca por onde um lado vazaria o que o outro barra. Por isso e um pacote npm publicado, importado pelos dois lados — nunca copiado. Funcao pura, zero dependencias de runtime, deliberadamente para que seja exaustivamente testavel.
Principio fail-closed
A decisao cravada deste pacote: todo caminho que nao for positivamente concedido resolve em negacao. Nao ha caminho "neutro" — ou a operacao foi explicitamente autorizada, ou e negada.
authorizeAccessnunca lanca. Uma entrada lixo (policy mal-formada, contexto nulo, operacao desconhecida) resolve em{ allow: false }.compileMongoFilterlanca ao receber uma decisao negada — compilar um filtro para umdenye bug do caller. Jamais devolve{}silenciosamente: um{}numfind()do Mongo retorna a colecao inteira (vazamento total).assertWriteOwnershiplanca em qualquer situacao que nao seja uma posse genuina por string. UmownerFieldausente, nulo, nao-string ou de outro dono reprova.
API
import {
authorizeAccess,
compileMongoFilter,
assertWriteOwnership,
type AccessPolicy,
type AuthzContext,
type AuthzDecision,
} from '@neetru/db-authz';
const policy: AccessPolicy = {
ownerField: '_ownerId', // campo de dono no documento
publicCollections: ['catalog'], // colecoes de leitura aberta
};
// 1. Decidir
const decision = authorizeAccess(
{ productId: 'p1', env: 'production', endUserId: 'user-A',
collection: 'notes', operation: 'list' },
policy,
);
// → { allow: true, enforcedFilter: { field: '_ownerId', op: '==', value: 'user-A' } }
// 2. Compilar o filtro Mongo (so de uma decisao permitida)
const filter = compileMongoFilter(decision); // → { _ownerId: 'user-A' }
db.collection('notes').find(filter); // usuario A so ve o que e dele
// 3. Antes de gravar, confirmar que o doc pertence ao usuario
assertWriteOwnership({ _ownerId: 'user-A', text: '...' }, ctx, policy); // ok
assertWriteOwnership({ _ownerId: 'user-B', text: '...' }, ctx, policy); // THROWTipos
AccessPolicy tem shape cravado — nao adicionar campos.
| Tipo | Campos |
|---|---|
| AccessPolicy | ownerField: string · publicCollections: string[] |
| AuthzContext | productId · env · endUserId: string \| null · collection · operation |
| AuthzDecision | allow: boolean · enforcedFilter? · reason? |
As 7 operacoes validas: get, list, add, set, update, remove,
subscribe.
authorizeAccess(ctx, policy)
Avalia nesta ordem, primeiro match vence:
- policy ausente/invalida →
{ allow: false, reason: 'no_policy' } - operacao fora das 7 →
{ allow: false, reason: 'unknown_operation' } endUserId === null→{ allow: true, reason: 'server_to_server' }— sem filtro. Caminho explicito e auditado do backend confiavel do produto.endUserIdvazio/whitespace →{ allow: false, reason: 'invalid_end_user' }- usuario final valido:
- colecao publica +
get/list/subscribe→{ allow: true }(sem filtro) - colecao publica + escrita →
{ allow: false, reason: 'public_collection_end_user_write_denied' } - colecao por dono +
add→{ allow: true }(create — o caller carimba oownerField) - colecao por dono + demais →
{ allow: true, enforcedFilter: { field: ownerField, op: '==', value: endUserId } }
- colecao publica +
compileMongoFilter(decision)
- decisao negada → throw
- decisao com
enforcedFilter→{ [field]: value } - decisao permitida sem filtro →
{}(leitura publica legitima)
assertWriteOwnership(doc, ctx, policy)
Lanca se policy for invalida, se doc nao for um objeto-registro, ou se o
ownerField do doc nao for uma string nao-vazia exatamente igual ao
endUserId. Le o ownerField como propriedade propria (hasOwnProperty),
de modo que um __proto__ poluido nao consiga forjar posse. Retorna sem erro
no caminho server-to-server (endUserId === null).
A mensagem do erro e generica de proposito — nao interpola o owner real nem o
endUserId, para nao vazar PII em logs.
Estado
Implementado (M0). Suite vitest adversarial — tenta ativamente vazar dados
entre usuarios — cobrindo policy ausente/invalida, operacao desconhecida,
server-to-server, usuario final invalido, colecoes publicas vs escopadas por
dono, coercion/prototype-pollution em assertWriteOwnership, e determinismo.
