fix(db): protege conexoes e migrations de homologacao
This commit is contained in:
1 parent
4779d6b2a3
commit
8c22597420
21 files changed
+839
-45
No files matched your search
@@ -0,0 +1,62 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createReadyDb } from "../helpers/world";
|
||||
|
||||
describe("papéis de manutenção", () => {
|
||||
it("cria web e worker sem superuser, sem bypass e sem herança", async () => {
|
||||
const db = await createReadyDb();
|
||||
const rows = await db.withService((query) =>
|
||||
query.query<{ rolname: string; rolsuper: boolean; rolbypassrls: boolean; rolinherit: boolean; rolcanlogin: boolean }>(
|
||||
`select rolname, rolsuper, rolbypassrls, rolinherit, rolcanlogin
|
||||
from pg_roles
|
||||
where rolname in ('adaptador_web', 'adaptador_worker', 'app_user')
|
||||
order by rolname`,
|
||||
),
|
||||
);
|
||||
expect(rows.map((row) => row.rolname)).toEqual(["adaptador_web", "adaptador_worker", "app_user"]);
|
||||
for (const row of rows) {
|
||||
expect(row.rolsuper, row.rolname).toBe(false);
|
||||
expect(row.rolbypassrls, row.rolname).toBe(false);
|
||||
expect(row.rolcanlogin, row.rolname).toBe(false);
|
||||
}
|
||||
expect(rows.find((row) => row.rolname === "adaptador_web")?.rolinherit).toBe(false);
|
||||
expect(rows.find((row) => row.rolname === "adaptador_worker")?.rolinherit).toBe(false);
|
||||
await db.close();
|
||||
});
|
||||
|
||||
it("impede a web e o app_user de varrer a fila, e deixa o worker só nas funções", async () => {
|
||||
const db = await createReadyDb();
|
||||
const denied = [
|
||||
`begin; set local role adaptador_web; select * from public.spreadsheet_jobs; commit;`,
|
||||
`begin; set local role app_user; select * from public.adapter_list_queued_job_ids(); commit;`,
|
||||
`begin; set local role adaptador_web; select * from public.adapter_worker_context('aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa', '11111111-1111-4111-8111-111111111111'); commit;`,
|
||||
];
|
||||
for (const statement of denied) {
|
||||
await expect(db.exec(statement)).rejects.toThrow(/permission denied/i);
|
||||
await db.exec("rollback");
|
||||
}
|
||||
await expect(
|
||||
db.exec(`begin; set local role adaptador_worker; select * from public.adapter_list_queued_job_ids(); commit;`),
|
||||
).resolves.toBeUndefined();
|
||||
const signature = await db.withService((query) =>
|
||||
query.query<{ result: string; prosecdef: boolean; proconfig: string[] | null }>(
|
||||
`select pg_get_function_result(p.oid) as result, p.prosecdef, p.proconfig
|
||||
from pg_proc p
|
||||
join pg_namespace n on n.oid = p.pronamespace
|
||||
where n.nspname = 'public' and p.proname = 'adapter_read_queued_job'`,
|
||||
),
|
||||
);
|
||||
expect(signature[0]?.prosecdef).toBe(true);
|
||||
expect(signature[0]?.proconfig?.join(" ")).toContain("search_path=pg_catalog");
|
||||
expect(signature[0]?.result).not.toMatch(/content|cells|columns_schema|storage_key/i);
|
||||
await db.close();
|
||||
});
|
||||
|
||||
it("mantém SET LOCAL ROLE dentro da transação do usuário", async () => {
|
||||
const db = await createReadyDb();
|
||||
const rows = await db.withUser("aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", (query) =>
|
||||
query.query<{ current_user: string }>(`select current_user`),
|
||||
);
|
||||
expect(rows[0]?.current_user).toBe("app_user");
|
||||
await db.close();
|
||||
});
|
||||
});
|
||||
@@ -138,6 +138,29 @@ describe.skipIf(!enabled)("PostgreSQL real", () => {
|
||||
expect(rows.every((row) => row.result === "success")).toBe(true);
|
||||
});
|
||||
|
||||
it("mantém web e worker sem bypass e sem varredura direta", async () => {
|
||||
const roles = await db.withService((query) =>
|
||||
query.query<{ rolname: string; rolsuper: boolean; rolbypassrls: boolean }>(
|
||||
`select rolname, rolsuper, rolbypassrls from pg_roles
|
||||
where rolname in ('adaptador_web', 'adaptador_worker', 'app_user')`,
|
||||
),
|
||||
);
|
||||
expect(roles).toHaveLength(3);
|
||||
for (const role of roles) {
|
||||
expect(role.rolsuper).toBe(false);
|
||||
expect(role.rolbypassrls).toBe(false);
|
||||
}
|
||||
await expect(
|
||||
db.exec(`do $$ begin set local role adaptador_web; perform 1 from public.spreadsheet_jobs; end $$;`),
|
||||
).rejects.toMatchObject({ code: "42501" });
|
||||
await expect(
|
||||
db.exec(`do $$ begin set local role app_user; perform * from public.adapter_list_queued_job_ids(); end $$;`),
|
||||
).rejects.toMatchObject({ code: "42501" });
|
||||
await db.exec(`do $$ begin set local role adaptador_worker; perform * from public.adapter_list_queued_job_ids(); end $$;`);
|
||||
const queued = await db.withUser(ANA, (query) => query.query<{ current_user: string }>(`select current_user`));
|
||||
expect(queued[0]?.current_user).toBe("app_user");
|
||||
});
|
||||
|
||||
it("pula o shim quando a decisão é não aplicar", async () => {
|
||||
const pending = await pendingMigrations(db, { applyIdentityShim: false });
|
||||
expect(pending).not.toContain("0001_platform_shim.sql");
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
AUTHORIZED_HML_PROJECT,
|
||||
POOLER_HOST_PATTERN,
|
||||
DatabaseGuardError,
|
||||
assertDeployedUrl,
|
||||
assertLocalDatabaseUrl,
|
||||
expectedPoolerHost,
|
||||
resolvePoolMax,
|
||||
scrubDbError,
|
||||
} from "@/infra/db/connection-policy";
|
||||
import { evaluateAdminIdentity, evaluateRuntimeRole, type AdminAttestation } from "@/infra/db/target-guard";
|
||||
|
||||
const REF = "abcdefghijabcdefghij";
|
||||
const DASHBOARD_HOST = "aws-1-us-west-2.pooler.supabase.com";
|
||||
|
||||
function adminUrl(user = `postgres.${REF}`, sslmode = "verify-full", host = DASHBOARD_HOST): string {
|
||||
return `postgres://${user}:segredo@${host}:5432/postgres?sslmode=${sslmode}`;
|
||||
}
|
||||
|
||||
function attestation(overrides: Partial<AdminAttestation> = {}): AdminAttestation {
|
||||
return {
|
||||
sessionUser: "postgres",
|
||||
databaseName: "postgres",
|
||||
rolsuper: true,
|
||||
rolbypassrls: true,
|
||||
hasAuth: true,
|
||||
hasStorage: true,
|
||||
databaseSettings: new Map([
|
||||
["app.hml_project", AUTHORIZED_HML_PROJECT],
|
||||
["app.hml_project_ref", REF],
|
||||
]),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
const env = { ADAPTER_ENV: "staging", ADAPTER_DB_PROJECT_REF: REF, ADAPTER_DB_POOLER_HOST: DASHBOARD_HOST };
|
||||
|
||||
describe("barreira do banco de homologação", () => {
|
||||
it("aceita o destino quando o catálogo, o papel e a URL concordam", () => {
|
||||
expect(assertDeployedUrl(adminUrl(), "admin", env).hostname).toBe(DASHBOARD_HOST);
|
||||
expect(() => evaluateAdminIdentity(attestation(), env)).not.toThrow();
|
||||
});
|
||||
|
||||
it("exige o host do Dashboard e recusa índice, região ou domínio diferentes", () => {
|
||||
expect(POOLER_HOST_PATTERN.test(DASHBOARD_HOST)).toBe(true);
|
||||
expect(expectedPoolerHost(env)).toBe(DASHBOARD_HOST);
|
||||
expect(() => expectedPoolerHost({ ADAPTER_DB_POOLER_HOST: "aws-0-us-west-2.pooler.supabase.com" })).not.toThrow();
|
||||
expect(() => assertDeployedUrl(adminUrl(`postgres.${REF}`, "verify-full", "aws-0-us-west-2.pooler.supabase.com"), "admin", env)).toThrow(
|
||||
/destino_incompativel/,
|
||||
);
|
||||
expect(() => expectedPoolerHost({})).toThrow(/pooler_host_ausente/);
|
||||
expect(() => expectedPoolerHost({ ADAPTER_DB_POOLER_HOST: "aws-1-eu-central-1.pooler.supabase.com" })).toThrow(
|
||||
/pooler_host_recusado/,
|
||||
);
|
||||
expect(() => expectedPoolerHost({ ADAPTER_DB_POOLER_HOST: "supabase.elopro.app" })).toThrow(/pooler_host_recusado/);
|
||||
expect(() => expectedPoolerHost({ ADAPTER_DB_POOLER_HOST: "db.abcdefghijabcdefghij.supabase.co" })).toThrow(
|
||||
/pooler_host_recusado/,
|
||||
);
|
||||
});
|
||||
|
||||
it("recusa identidade ausente, marcador ausente e marcador divergente", () => {
|
||||
expect(() => evaluateAdminIdentity(attestation(), {})).toThrow(DatabaseGuardError);
|
||||
expect(() => evaluateAdminIdentity(attestation({ databaseSettings: new Map() }), env)).toThrow(/marcador_ausente/);
|
||||
expect(() =>
|
||||
evaluateAdminIdentity(
|
||||
attestation({
|
||||
databaseSettings: new Map([
|
||||
["app.hml_project", AUTHORIZED_HML_PROJECT],
|
||||
["app.hml_project_ref", "zzzzzzzzzzzzzzzzzzzz"],
|
||||
]),
|
||||
}),
|
||||
env,
|
||||
),
|
||||
).toThrow(/marcador_diverge/);
|
||||
});
|
||||
|
||||
it("não aceita só o nome postgres, o host ou um usuário sem o ref", () => {
|
||||
expect(() => evaluateAdminIdentity(attestation({ sessionUser: "postgres", hasAuth: false }), env)).toThrow(
|
||||
/schema_supabase_ausente/,
|
||||
);
|
||||
expect(() => assertDeployedUrl(adminUrl("postgres"), "admin", env)).toThrow(/usuario_url_diverge/);
|
||||
expect(() =>
|
||||
assertDeployedUrl(
|
||||
`postgres://postgres.${REF}:segredo@supabase.elopro.app:5432/postgres?sslmode=verify-full`,
|
||||
"admin",
|
||||
env,
|
||||
),
|
||||
).toThrow(/host_elopro_preservado/);
|
||||
expect(() =>
|
||||
assertDeployedUrl(`postgres://postgres.${REF}:segredo@db.exemplo.supabase.co:5432/postgres?sslmode=verify-full`, "admin", env),
|
||||
).toThrow(/destino_incompativel/);
|
||||
});
|
||||
|
||||
it("recusa TLS fraco, parâmetro de sessão e pool acima do teto", () => {
|
||||
expect(() => assertDeployedUrl(adminUrl(`postgres.${REF}`, "require"), "admin", env)).toThrow(/tls_inseguro/);
|
||||
expect(() => assertDeployedUrl(adminUrl(`postgres.${REF}`, "disable"), "admin", env)).toThrow(/tls_inseguro/);
|
||||
expect(() =>
|
||||
assertDeployedUrl(`${adminUrl()}&options=-c%20app.hml_project%3Dadaptador-planilha-hml`, "admin", env),
|
||||
).toThrow(/parametro_de_sessao_recusado/);
|
||||
expect(() => resolvePoolMax("web", { ...env, ADAPTER_DB_POOL_MAX: "16" })).toThrow(/pool_recusado/);
|
||||
expect(resolvePoolMax("web", env)).toBe(2);
|
||||
expect(resolvePoolMax("worker", env)).toBe(2);
|
||||
expect(resolvePoolMax("admin", env)).toBe(1);
|
||||
});
|
||||
|
||||
it("recusa a credencial administrativa na web e BYPASSRLS no worker", () => {
|
||||
expect(() => evaluateRuntimeRole({ sessionUser: "postgres", rolsuper: true, rolbypassrls: true }, "web", env)).toThrow(
|
||||
/web_com_credencial_admin/,
|
||||
);
|
||||
expect(() =>
|
||||
evaluateRuntimeRole({ sessionUser: `postgres.${REF}`, rolsuper: true, rolbypassrls: false }, "web", env),
|
||||
).toThrow(/web_com_credencial_admin/);
|
||||
expect(() =>
|
||||
evaluateRuntimeRole({ sessionUser: "adaptador_worker", rolsuper: false, rolbypassrls: true }, "worker", env),
|
||||
).toThrow(/worker_privilegiado/);
|
||||
expect(() =>
|
||||
evaluateRuntimeRole({ sessionUser: "adaptador_web", rolsuper: false, rolbypassrls: false }, "web", env),
|
||||
).not.toThrow();
|
||||
expect(() =>
|
||||
evaluateRuntimeRole({ sessionUser: `adaptador_worker.${REF}`, rolsuper: false, rolbypassrls: false }, "worker", env),
|
||||
).not.toThrow();
|
||||
expect(() => evaluateRuntimeRole({ sessionUser: "adaptador_web", rolsuper: null, rolbypassrls: false }, "web", env)).toThrow(
|
||||
/papel_nao_comprovado/,
|
||||
);
|
||||
});
|
||||
|
||||
it("separa o development local do Supabase", () => {
|
||||
expect(() => assertLocalDatabaseUrl("postgres://adaptador:local@postgres:5432/adaptador")).not.toThrow();
|
||||
expect(() => assertLocalDatabaseUrl("postgres://adaptador:local@127.0.0.1:55432/adaptador")).not.toThrow();
|
||||
expect(() => assertLocalDatabaseUrl(adminUrl())).toThrow(/development_nao_acessa_supabase/);
|
||||
expect(() => assertLocalDatabaseUrl("postgres://postgres:local@supabase.elopro.app:5432/postgres")).toThrow(
|
||||
/host_elopro_preservado/,
|
||||
);
|
||||
});
|
||||
|
||||
it("não deixa senha nem URL no erro visível", () => {
|
||||
const scrubbed = scrubDbError(new Error("falha em postgres://postgres.abc:segredo@aws-0-us-west-2.pooler.supabase.com:5432/postgres"));
|
||||
expect(scrubbed).not.toContain("segredo");
|
||||
expect(scrubbed).not.toContain("postgres://postgres");
|
||||
expect(scrubbed).toContain("postgres://***");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user