A verificação de FORCE RLS lia pg_class sem filtrar relkind e pegava índices como spreadsheet_adapter_settings_pkey, que nunca têm RLS. Agora lê só tabelas (r/p) do schema public e compara com a lista exata das dez tabelas protegidas pela 0003_rls.sql. O teste de auditoria reutilizava $1 como uuid (workspace_id) e text (resource_id), e o Postgres recusava antes de testar a permissão. Cada coluna tem seu parâmetro e o teste exige o código 42501 em update e delete, confirmando que o registro continua intacto. Regressão do catálogo de RLS também roda na suíte padrão (PGlite). Nenhuma migration, política ou permissão foi alterada. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
88 lines
4.2 KiB
TypeScript
88 lines
4.2 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { createModel } from "@/server/spreadsheet-adapter/service";
|
|
import { ctxOf, createReadyDb, depsOf, IDS } from "../helpers/world";
|
|
import { readRlsCatalog, RLS_TABLES } from "../helpers/rls-catalog";
|
|
|
|
describe("RLS e permissões", () => {
|
|
it("impede leitura e escrita entre workspaces", async () => {
|
|
const db = await createReadyDb();
|
|
const deps = depsOf(db);
|
|
const ana = ctxOf("ana", "alpha", "owner", true);
|
|
const diego = ctxOf("diego", "beta", "admin", false);
|
|
await createModel(deps, ana, { name: "Somente Alpha", filename: "a.csv", bytes: Buffer.from("A\n1\n") });
|
|
const asDiego = await db.withUser(IDS.diego, (query) => query.query(`select name from public.spreadsheet_models`));
|
|
expect(asDiego).toHaveLength(0);
|
|
const asAna = await db.withUser(IDS.ana, (query) => query.query(`select name from public.spreadsheet_models where workspace_id = $1`, [IDS.beta]));
|
|
expect(asAna).toHaveLength(0);
|
|
await expect(
|
|
db.withUser(IDS.ana, (query) =>
|
|
query.query(
|
|
`insert into public.spreadsheet_models
|
|
(workspace_id, name, original_filename, format, storage_key, content_sha256, header_row, columns_schema, created_by)
|
|
values ($1,'Invasao','a.csv','csv','workspaces/x/models/y/original','abc',1,'[]'::jsonb,$2)`,
|
|
[IDS.beta, IDS.ana],
|
|
),
|
|
),
|
|
).rejects.toThrow(/row-level security/i);
|
|
await expect(createModel(deps, { ...diego, moduleEnabled: true }, { name: "Beta", filename: "a.csv", bytes: Buffer.from("A\n1\n") })).rejects.toMatchObject({
|
|
code: "FEATURE_DISABLED",
|
|
});
|
|
const eva = ctxOf("eva", "beta", "agent", true, true);
|
|
await expect(createModel(deps, eva, { name: "Super", filename: "a.csv", bytes: Buffer.from("A\n1\n") })).rejects.toThrow();
|
|
await db.close();
|
|
});
|
|
|
|
it("agent não altera configuração e owner altera", async () => {
|
|
const db = await createReadyDb();
|
|
const ana = ctxOf("ana", "alpha", "owner", true);
|
|
const bruno = ctxOf("bruno", "alpha", "agent", true);
|
|
await expect(
|
|
db.withUser(bruno.userId, (query) =>
|
|
query.query(`insert into public.spreadsheet_adapter_settings (workspace_id, allow_non_admin_model_upload) values ($1, true)`, [IDS.alpha]),
|
|
),
|
|
).rejects.toThrow(/row-level security/i);
|
|
await db.withUser(ana.userId, (query) =>
|
|
query.query(`insert into public.spreadsheet_adapter_settings (workspace_id, allow_non_admin_model_upload) values ($1, true)`, [IDS.alpha]),
|
|
);
|
|
const seen = await db.withUser(bruno.userId, (query) =>
|
|
query.query<{ allow_non_admin_model_upload: boolean }>(`select allow_non_admin_model_upload from public.spreadsheet_adapter_settings`),
|
|
);
|
|
expect(seen[0].allow_non_admin_model_upload).toBe(true);
|
|
await db.close();
|
|
});
|
|
|
|
it("não altera auditoria pelo papel da aplicação", async () => {
|
|
const db = await createReadyDb();
|
|
const ana = ctxOf("ana", "alpha", "owner", true);
|
|
await db.withUser(ana.userId, (query) =>
|
|
query.query(
|
|
`insert into public.audit_log (workspace_id, actor_id, action, resource_type, resource_id, result)
|
|
values ($1, $2, 'model.uploaded', 'model', 'm1', 'success')`,
|
|
[IDS.alpha, IDS.ana],
|
|
),
|
|
);
|
|
await expect(
|
|
db.withUser(ana.userId, (query) =>
|
|
query.query(`update public.audit_log set action = 'tamper' where workspace_id = $1`, [IDS.alpha]),
|
|
),
|
|
).rejects.toThrow(/permission denied/i);
|
|
const rows = await db.withService((query) =>
|
|
query.query<{ action: string }>(`select action from public.audit_log where workspace_id = $1`, [IDS.alpha]),
|
|
);
|
|
expect(rows.every((row) => row.action !== "tamper")).toBe(true);
|
|
await db.close();
|
|
});
|
|
|
|
it("verifica RLS e FORCE RLS só em tabelas, nunca em índices", async () => {
|
|
const db = await createReadyDb();
|
|
const rows = await db.withService(readRlsCatalog);
|
|
expect(rows.map((row) => row.relname)).toEqual([...RLS_TABLES]);
|
|
expect(rows.some((row) => row.relname.endsWith("_pkey"))).toBe(false);
|
|
for (const row of rows) {
|
|
expect(row.relrowsecurity, `${row.relname} sem RLS`).toBe(true);
|
|
expect(row.relforcerowsecurity, `${row.relname} sem FORCE RLS`).toBe(true);
|
|
}
|
|
await db.close();
|
|
});
|
|
});
|