fix(db): compatibiliza barreira com Supabase gerenciado
This commit is contained in:
1 parent
8c22597420
commit
2c8c5d9507
3 files changed
+69
-16
No files matched your search
@@ -3,7 +3,9 @@
|
|||||||
-- Não concede BYPASSRLS nem SUPERUSER.
|
-- Não concede BYPASSRLS nem SUPERUSER.
|
||||||
-- Os papéis nascem NOLOGIN. A senha e o LOGIN ficam para um passo autorizado no projeto de homologação.
|
-- Os papéis nascem NOLOGIN. A senha e o LOGIN ficam para um passo autorizado no projeto de homologação.
|
||||||
-- As funções são SECURITY DEFINER, com search_path travado, e EXECUTE só para adaptador_worker.
|
-- As funções são SECURITY DEFINER, com search_path travado, e EXECUTE só para adaptador_worker.
|
||||||
-- O dono é quem aplica a migration. No Supabase gerenciado esse dono é o postgres, que atravessa FORCE RLS.
|
-- O dono é quem aplica a migration. No Supabase gerenciado esse dono é o postgres,
|
||||||
|
-- com rolsuper = false e rolbypassrls = true. O BYPASSRLS do dono faz a função
|
||||||
|
-- atravessar FORCE RLS; SUPERUSER não é necessário para isso. BYPASSRLS não identifica o projeto.
|
||||||
-- adaptador_web e adaptador_worker são NOINHERIT: sem SET ROLE app_user não herdam os grants do papel da aplicação.
|
-- adaptador_web e adaptador_worker são NOINHERIT: sem SET ROLE app_user não herdam os grants do papel da aplicação.
|
||||||
|
|
||||||
do $$ begin
|
do $$ begin
|
||||||
|
|||||||
@@ -35,23 +35,32 @@ export function databaseSettings(items: string[]): Map<string, string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prova administrativa. O hostname, o nome `postgres`, um GUC de sessão e a
|
* Prova administrativa do Supabase gerenciado. O hostname, o nome `postgres`,
|
||||||
* variável de ambiente, cada um sozinho, não passam. Os fatos do servidor
|
* um GUC de sessão, a variável de ambiente e o atributo BYPASSRLS, cada um
|
||||||
* precisam concordar: schemas do Supabase, papel autenticado e marcador
|
* sozinho, não passam. Os fatos do servidor precisam concordar: schemas
|
||||||
* persistido com ALTER DATABASE.
|
* `auth` e `storage`, papel autenticado e marcador persistido com ALTER DATABASE.
|
||||||
|
*
|
||||||
|
* No projeto `adaptador-planilha-hml` o papel `postgres` tem `rolsuper = false`
|
||||||
|
* e `rolbypassrls = true`. SUPERUSER não é exigido. BYPASSRLS também não
|
||||||
|
* autoriza: o mesmo atributo existe em outros papéis e em outros projetos, e
|
||||||
|
* a leitura de `pg_roles` não diz qual projeto atendeu a sessão.
|
||||||
*
|
*
|
||||||
* Limitação do marcador: `app.hml_project` e `app.hml_project_ref` não são
|
* Limitação do marcador: `app.hml_project` e `app.hml_project_ref` não são
|
||||||
* identidade criptográfica do projeto Supabase. São parâmetros gravados no
|
* identidade criptográfica do projeto Supabase. São parâmetros gravados no
|
||||||
* catálogo `pg_db_role_setting` (setrole = 0). Um SET de sessão ou `options=-c`
|
* catálogo `pg_db_role_setting` (setrole = 0). Um SET de sessão ou `options=-c`
|
||||||
* na URL não escreve nesse catálogo, por isso `current_setting` não é aceito
|
* na URL não escreve nesse catálogo, por isso `current_setting` não é aceito
|
||||||
* como prova. Um superusuário de outro banco pode copiar os mesmos ALTER
|
* como prova. Outro parâmetro já gravado, como `app.settings.jwt_exp`, não
|
||||||
* DATABASE. A cópia só passaria se a URL também trouxesse o pooler declarado,
|
* identifica o projeto e não é alterado por esta verificação. Quem puder
|
||||||
* o usuário `postgres.<ref>` e o ref do marcador. Sem o marcador, ou com
|
* executar ALTER DATABASE em outro banco pode copiar os mesmos marcadores.
|
||||||
* divergência, a operação para.
|
* A cópia só passaria se a URL também trouxesse o pooler declarado, o usuário
|
||||||
|
* `postgres.<ref>` e o ref do marcador. Sem o marcador, ou com divergência,
|
||||||
|
* a operação para.
|
||||||
*
|
*
|
||||||
* O pooler pode devolver `session_user` só como `postgres`, sem o sufixo do
|
* Limitação do Session Pooler: o usuário da URL tem a forma `papel.<ref>`,
|
||||||
* ref. Nesse caso o ref autenticado está no usuário da URL, e o vínculo com
|
* mas o servidor pode devolver `session_user` só como `postgres`, sem o
|
||||||
* este projeto está no marcador. Os dois têm de ser o mesmo ref.
|
* sufixo. O ref autenticado fica então no usuário da URL, e o vínculo com
|
||||||
|
* este projeto fica no marcador. Os dois têm de ser o mesmo ref. O pooler
|
||||||
|
* também não torna exclusivo o nome do banco `postgres` nem o host da região.
|
||||||
*/
|
*/
|
||||||
export function evaluateAdminIdentity(attestation: AdminAttestation, env: EnvMap = process.env): void {
|
export function evaluateAdminIdentity(attestation: AdminAttestation, env: EnvMap = process.env): void {
|
||||||
const ref = env.ADAPTER_DB_PROJECT_REF?.trim() ?? "";
|
const ref = env.ADAPTER_DB_PROJECT_REF?.trim() ?? "";
|
||||||
@@ -62,7 +71,9 @@ export function evaluateAdminIdentity(attestation: AdminAttestation, env: EnvMap
|
|||||||
if (session !== "postgres" && session !== `postgres.${ref}`) {
|
if (session !== "postgres" && session !== `postgres.${ref}`) {
|
||||||
throw new DatabaseGuardError("usuario_admin_nao_comprovado");
|
throw new DatabaseGuardError("usuario_admin_nao_comprovado");
|
||||||
}
|
}
|
||||||
if (attestation.rolsuper !== true) throw new DatabaseGuardError("admin_sem_superuser");
|
if (attestation.rolsuper === null || attestation.rolbypassrls === null) {
|
||||||
|
throw new DatabaseGuardError("papel_nao_comprovado");
|
||||||
|
}
|
||||||
const project = attestation.databaseSettings.get("app.hml_project");
|
const project = attestation.databaseSettings.get("app.hml_project");
|
||||||
const markedRef = attestation.databaseSettings.get("app.hml_project_ref");
|
const markedRef = attestation.databaseSettings.get("app.hml_project_ref");
|
||||||
if (!project || !markedRef) throw new DatabaseGuardError("marcador_ausente");
|
if (!project || !markedRef) throw new DatabaseGuardError("marcador_ausente");
|
||||||
@@ -137,8 +148,10 @@ async function readDatabaseMarker(db: Db): Promise<Map<string, string>> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Barreira de destino. Em development só aceita host local. Em staging e
|
* Barreira de destino. Em development só aceita host local. Em staging e
|
||||||
* produção a web e o worker recusam superuser e BYPASSRLS; migrate, seed e
|
* produção a web e o worker recusam SUPERUSER e BYPASSRLS. A credencial
|
||||||
* flag só seguem com a identidade administrativa comprovada no catálogo.
|
* administrativa do Supabase gerenciado não é superusuária; migrate, seed e
|
||||||
|
* flag só seguem quando o catálogo comprova o projeto, sem usar BYPASSRLS
|
||||||
|
* como autorização.
|
||||||
*/
|
*/
|
||||||
export async function assertDatabaseGuard(db: Db, purpose: DbPurpose, connectionString: string | null, env: EnvMap = process.env): Promise<void> {
|
export async function assertDatabaseGuard(db: Db, purpose: DbPurpose, connectionString: string | null, env: EnvMap = process.env): Promise<void> {
|
||||||
if (!isDeployedEnv(env)) {
|
if (!isDeployedEnv(env)) {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ function attestation(overrides: Partial<AdminAttestation> = {}): AdminAttestatio
|
|||||||
return {
|
return {
|
||||||
sessionUser: "postgres",
|
sessionUser: "postgres",
|
||||||
databaseName: "postgres",
|
databaseName: "postgres",
|
||||||
rolsuper: true,
|
rolsuper: false,
|
||||||
rolbypassrls: true,
|
rolbypassrls: true,
|
||||||
hasAuth: true,
|
hasAuth: true,
|
||||||
hasStorage: true,
|
hasStorage: true,
|
||||||
@@ -40,6 +40,35 @@ describe("barreira do banco de homologação", () => {
|
|||||||
it("aceita o destino quando o catálogo, o papel e a URL concordam", () => {
|
it("aceita o destino quando o catálogo, o papel e a URL concordam", () => {
|
||||||
expect(assertDeployedUrl(adminUrl(), "admin", env).hostname).toBe(DASHBOARD_HOST);
|
expect(assertDeployedUrl(adminUrl(), "admin", env).hostname).toBe(DASHBOARD_HOST);
|
||||||
expect(() => evaluateAdminIdentity(attestation(), env)).not.toThrow();
|
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", () => {
|
||||||
|
const settings = new Map([
|
||||||
|
["app.settings.jwt_exp", "3600"],
|
||||||
|
["app.hml_project", AUTHORIZED_HML_PROJECT],
|
||||||
|
["app.hml_project_ref", REF],
|
||||||
|
]);
|
||||||
|
expect(() =>
|
||||||
|
evaluateAdminIdentity(attestation({ rolsuper: false, rolbypassrls: true, databaseSettings: settings }), env),
|
||||||
|
).not.toThrow();
|
||||||
|
expect(settings.get("app.settings.jwt_exp")).toBe("3600");
|
||||||
|
expect(() =>
|
||||||
|
evaluateAdminIdentity(
|
||||||
|
attestation({
|
||||||
|
rolsuper: false,
|
||||||
|
rolbypassrls: true,
|
||||||
|
databaseSettings: new Map([["app.settings.jwt_exp", "3600"]]),
|
||||||
|
}),
|
||||||
|
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("exige o host do Dashboard e recusa índice, região ou domínio diferentes", () => {
|
it("exige o host do Dashboard e recusa índice, região ou domínio diferentes", () => {
|
||||||
@@ -105,6 +134,15 @@ describe("barreira do banco de homologação", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("recusa a credencial administrativa na web e BYPASSRLS no worker", () => {
|
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(
|
expect(() => evaluateRuntimeRole({ sessionUser: "postgres", rolsuper: true, rolbypassrls: true }, "web", env)).toThrow(
|
||||||
/web_com_credencial_admin/,
|
/web_com_credencial_admin/,
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in new issue
Block a user