327 lines
16 KiB
TypeScript
327 lines
16 KiB
TypeScript
import { X509Certificate } from "node:crypto";
|
|
import { execFileSync } from "node:child_process";
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import tls from "node:tls";
|
|
import { describe, expect, it } from "vitest";
|
|
import {
|
|
AUTHORIZED_HML_PROJECT,
|
|
POOLER_HOST_PATTERN,
|
|
DatabaseGuardError,
|
|
assertDeployedUrl,
|
|
assertLocalDatabaseUrl,
|
|
buildPostgresOptions,
|
|
expectedPoolerHost,
|
|
resolvePoolMax,
|
|
scrubDbError,
|
|
} from "@/infra/db/connection-policy";
|
|
import { readFileSync, readdirSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { assertInitAuthorized, projectIdentityDefinitionSql, projectIdentityRevertSql } from "@/infra/db/project-identity";
|
|
import { SUPABASE_ROOT_CA_2021 } from "@/infra/db/supabase-root-ca";
|
|
import { assertAdminTarget, evaluateAdminIdentity, evaluateProjectIdentity, evaluateRuntimeRole, type AdminAttestation } from "@/infra/db/target-guard";
|
|
import type { Db } from "@/infra/db/types";
|
|
|
|
const REF = "abcdefghijabcdefghij";
|
|
const REF_COM_DIGITO = "abcd1234abcd1234wxyz";
|
|
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: false,
|
|
rolbypassrls: true,
|
|
hasAuth: true,
|
|
hasStorage: true,
|
|
projectName: AUTHORIZED_HML_PROJECT,
|
|
projectRef: 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();
|
|
expect(() => evaluateAdminIdentity(attestation({ sessionUser: `postgres.${REF}` }), env)).not.toThrow();
|
|
});
|
|
|
|
it("reconhece o postgres do Supabase gerenciado sem tratar BYPASSRLS como autorização", () => {
|
|
expect(() => evaluateAdminIdentity(attestation({ rolsuper: false, rolbypassrls: true }), env)).not.toThrow();
|
|
expect(() =>
|
|
evaluateAdminIdentity(attestation({ projectName: null, projectRef: null, rolsuper: false, rolbypassrls: true }), env),
|
|
).toThrow(/marcador_ausente/);
|
|
expect(() =>
|
|
evaluateAdminIdentity(attestation({ sessionUser: "supabase_admin", rolsuper: false, rolbypassrls: true }), env),
|
|
).toThrow(/usuario_admin_nao_comprovado/);
|
|
expect(() => evaluateAdminIdentity(attestation({ rolsuper: null, rolbypassrls: true }), env)).toThrow(
|
|
/papel_nao_comprovado/,
|
|
);
|
|
});
|
|
|
|
it("aceita Reference ID com dígito e recusa GUC ou variável como única prova", () => {
|
|
const envComDigito = { ...env, ADAPTER_DB_PROJECT_REF: REF_COM_DIGITO };
|
|
expect(() =>
|
|
evaluateProjectIdentity({ projectName: AUTHORIZED_HML_PROJECT, projectRef: REF_COM_DIGITO }, envComDigito),
|
|
).not.toThrow();
|
|
expect(() => evaluateProjectIdentity({ projectName: null, projectRef: null }, envComDigito)).toThrow(/marcador_ausente/);
|
|
expect(() => evaluateProjectIdentity({ projectName: AUTHORIZED_HML_PROJECT, projectRef: REF }, envComDigito)).toThrow(
|
|
/marcador_diverge/,
|
|
);
|
|
expect(() => evaluateAdminIdentity(attestation(), { ...env, ADAPTER_DB_PROJECT_REF: "ABCD1234ABCD1234WXYZ" })).toThrow(
|
|
/ref_ausente/,
|
|
);
|
|
expect(() => assertInitAuthorized({ ADAPTER_ENV: "staging" })).toThrow(/init_nao_autorizada/);
|
|
expect(() => assertInitAuthorized({ ADAPTER_ENV: "development", ADAPTER_INIT_PROJECT_IDENTITY: "1" })).toThrow(
|
|
/init_fora_de_homologacao/,
|
|
);
|
|
expect(() => assertInitAuthorized({ ADAPTER_ENV: "staging", ADAPTER_INIT_PROJECT_IDENTITY: "1" })).not.toThrow();
|
|
const sql = projectIdentityDefinitionSql(REF_COM_DIGITO);
|
|
expect(sql).toContain(REF_COM_DIGITO);
|
|
expect(sql.split("do $identity$")).toHaveLength(2);
|
|
expect(sql).toContain("security definer");
|
|
expect(sql).toContain("set search_path = pg_catalog");
|
|
expect(sql).toContain("revoke all on table adaptador_guard.project_identity from service_role");
|
|
expect(sql.toLowerCase()).not.toContain("jwt_exp");
|
|
expect(sql.toLowerCase()).not.toContain("alter database");
|
|
expect(sql).not.toContain("auth.");
|
|
expect(sql).not.toContain("storage.");
|
|
const prepared = readFileSync(path.join(process.cwd(), "scripts/sql/init-project-identity.sql"), "utf8");
|
|
expect(prepared).toContain("<<PROJECT_REF>>");
|
|
expect(prepared).toContain("security definer");
|
|
expect(prepared.toLowerCase()).not.toContain("cascade");
|
|
const revert = projectIdentityRevertSql();
|
|
const revertFile = readFileSync(path.join(process.cwd(), "scripts/sql/revert-project-identity.sql"), "utf8");
|
|
expect(revert).toContain("drop schema adaptador_guard restrict;");
|
|
expect(revert.toLowerCase()).not.toContain("cascade");
|
|
expect(revertFile).toContain("drop schema adaptador_guard restrict;");
|
|
expect(revertFile.toLowerCase()).not.toContain("cascade");
|
|
const initScript = readFileSync(path.join(process.cwd(), "scripts/init-project-identity.ts"), "utf8");
|
|
expect(initScript).not.toContain("console.log(databaseUrl");
|
|
expect(initScript).not.toContain("console.log(process.env");
|
|
expect(initScript).not.toContain("bootstrapScript");
|
|
expect(initScript).not.toContain("exitOnInvalidConfig");
|
|
expect(initScript).not.toMatch(/migrate\(/);
|
|
expect(initScript).not.toMatch(/from ["'].*seed/);
|
|
});
|
|
|
|
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({ projectName: null, projectRef: null }), env)).toThrow(/marcador_ausente/);
|
|
expect(() => evaluateAdminIdentity(attestation({ projectRef: "zzzzzzzzzzzzzzzzzzzz" }), env)).toThrow(/marcador_diverge/);
|
|
expect(() => evaluateProjectIdentity({ projectName: "outro-projeto", projectRef: REF }, env)).toThrow(/marcador_diverge/);
|
|
const migrations = readdirSync(path.join(process.cwd(), "migrations"), { recursive: true }).filter(
|
|
(file) => typeof file === "string" && file.endsWith(".sql"),
|
|
);
|
|
for (const file of migrations) {
|
|
const sql = readFileSync(path.join(process.cwd(), "migrations", file), "utf8");
|
|
expect(sql, file).not.toContain("adaptador_guard");
|
|
}
|
|
for (const file of ["scripts/migrate.ts", "scripts/seed-staging.ts", "scripts/seed-dev.ts", "scripts/set-flag.ts"]) {
|
|
expect(readFileSync(path.join(process.cwd(), file), "utf8"), file).not.toContain("installProjectIdentity");
|
|
}
|
|
});
|
|
|
|
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);
|
|
const root = new X509Certificate(SUPABASE_ROOT_CA_2021);
|
|
expect(root.ca).toBe(true);
|
|
expect(root.fingerprint256).toBe("80:70:25:AD:50:D4:ED:21:9D:2C:9C:7D:29:9C:00:4F:82:4E:B0:0C:F7:F6:5A:FE:F6:07:D0:7B:72:E6:CA:FA");
|
|
expect(root.subject).toContain("CN=Supabase Root 2021 CA");
|
|
expect(SUPABASE_ROOT_CA_2021).not.toContain("PRIVATE KEY");
|
|
const deployed = buildPostgresOptions(adminUrl(), "admin", env);
|
|
const production = buildPostgresOptions(adminUrl(), "admin", { ...env, ADAPTER_ENV: "production" });
|
|
for (const options of [deployed, production]) {
|
|
expect(options.ssl).toEqual({ rejectUnauthorized: true, ca: SUPABASE_ROOT_CA_2021 });
|
|
expect(options).not.toHaveProperty("connection");
|
|
expect(JSON.stringify(options)).not.toContain("family");
|
|
expect(JSON.stringify(options)).not.toContain("rejectUnauthorized\":false");
|
|
}
|
|
const local = buildPostgresOptions("postgres://adaptador:local@127.0.0.1:5432/adaptador", "local", {
|
|
ADAPTER_ENV: "development",
|
|
});
|
|
expect(local).not.toHaveProperty("connection");
|
|
expect(local.ssl).toBeUndefined();
|
|
});
|
|
|
|
it("a CA pública não libera outro destino nem aceita certificado divergente", async () => {
|
|
expect(() =>
|
|
buildPostgresOptions(adminUrl(`postgres.${REF}`, "verify-full", "aws-0-us-west-2.pooler.supabase.com"), "admin", env),
|
|
).toThrow(/destino_incompativel/);
|
|
expect(() => assertInitAuthorized({ ADAPTER_ENV: "staging", ADAPTER_DB_PROJECT_REF: REF })).toThrow(/init_nao_autorizada/);
|
|
const options = buildPostgresOptions(adminUrl(), "admin", env);
|
|
expect(options.ssl).not.toHaveProperty("checkServerIdentity");
|
|
expect(options.ssl).not.toHaveProperty("servername");
|
|
expect(options.ssl?.rejectUnauthorized).toBe(true);
|
|
|
|
const dir = mkdtempSync(path.join(tmpdir(), "ca-divergente-"));
|
|
const keyPath = path.join(dir, "key.pem");
|
|
const certPath = path.join(dir, "cert.pem");
|
|
try {
|
|
execFileSync(
|
|
"openssl",
|
|
["req", "-x509", "-newkey", "ec", "-pkeyopt", "ec_paramgen_curve:prime256v1", "-keyout", keyPath, "-out", certPath, "-days", "1", "-nodes", "-subj", "/CN=divergente.example"],
|
|
{ stdio: "ignore" },
|
|
);
|
|
const server = tls.createServer({ key: readFileSync(keyPath), cert: readFileSync(certPath) }, (socket) => socket.end());
|
|
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
const address = server.address();
|
|
if (!address || typeof address === "string") throw new Error("porta_local_ausente");
|
|
try {
|
|
await new Promise<void>((resolve, reject) => {
|
|
const socket = tls.connect({
|
|
host: "127.0.0.1",
|
|
port: address.port,
|
|
servername: "divergente.example",
|
|
rejectUnauthorized: true,
|
|
ca: SUPABASE_ROOT_CA_2021,
|
|
});
|
|
socket.once("secureConnect", () => {
|
|
socket.end();
|
|
reject(new Error("certificado_divergente_aceito"));
|
|
});
|
|
socket.once("error", (error: NodeJS.ErrnoException) => {
|
|
expect(error.code).toMatch(/SELF_SIGNED_CERT_IN_CHAIN|DEPTH_ZERO_SELF_SIGNED_CERT|UNABLE_TO_VERIFY_LEAF_SIGNATURE/);
|
|
resolve();
|
|
});
|
|
});
|
|
} finally {
|
|
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
}
|
|
} finally {
|
|
rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("recusa a credencial administrativa na web e BYPASSRLS no worker", () => {
|
|
expect(() => evaluateRuntimeRole({ sessionUser: "postgres", rolsuper: false, rolbypassrls: true }, "web", env)).toThrow(
|
|
/web_com_credencial_admin/,
|
|
);
|
|
expect(() =>
|
|
evaluateRuntimeRole({ sessionUser: "adaptador_web", rolsuper: false, rolbypassrls: true }, "web", env),
|
|
).toThrow(/web_com_credencial_admin/);
|
|
expect(() =>
|
|
evaluateRuntimeRole({ sessionUser: "adaptador_worker", rolsuper: true, rolbypassrls: false }, "worker", env),
|
|
).toThrow(/worker_privilegiado/);
|
|
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(() => evaluateProjectIdentity({ projectName: null, projectRef: null }, env)).toThrow(/marcador_ausente/);
|
|
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("classifica a falha de conexão pelo código do driver e omite a mensagem", async () => {
|
|
const db = {
|
|
exec: async () => undefined,
|
|
close: async () => undefined,
|
|
withUser: async () => {
|
|
throw new Error("nao");
|
|
},
|
|
withService: async () => {
|
|
throw Object.assign(new Error("postgres://postgres.ref:segredo@host/db family"), { code: "42704" });
|
|
},
|
|
} as Db;
|
|
await expect(assertAdminTarget(db, env)).rejects.toMatchObject({
|
|
code: "identidade_indisponivel:42704",
|
|
});
|
|
await expect(assertAdminTarget(db, env)).rejects.not.toThrow(/segredo/);
|
|
|
|
const semCodigo = {
|
|
...db,
|
|
withService: async () => {
|
|
throw new Error("postgres://postgres.ref:segredo@host/db");
|
|
},
|
|
} as Db;
|
|
await expect(assertAdminTarget(semCodigo, env)).rejects.toMatchObject({ code: "identidade_indisponivel" });
|
|
await expect(assertAdminTarget(semCodigo, env)).rejects.not.toThrow(/segredo/);
|
|
|
|
const codigoInseguro = {
|
|
...db,
|
|
withService: async () => {
|
|
throw Object.assign(new Error("falha"), { code: "postgres://segredo" });
|
|
},
|
|
} as Db;
|
|
await expect(assertAdminTarget(codigoInseguro, env)).rejects.toMatchObject({ code: "identidade_indisponivel" });
|
|
await expect(assertAdminTarget(codigoInseguro, env)).rejects.not.toThrow(/segredo/);
|
|
});
|
|
|
|
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://***");
|
|
});
|
|
});
|