fix(db): implementa identidade persistente segura para homologacao
ci / lint, typecheck, testes e build (push) Canceled after 0s
ci / Postgres e Redis reais (push) Canceled after 0s
ci / imagem Docker Linux (push) Canceled after 0s

This commit is contained in:
Ferraz Fagundes committed 2026-10-19 23:39:05 -03:00
1 parent 2c8c5d9507
commit a4f0a2f649
11 files changed
+1146 -106

No files matched your search

+34
View File
@@ -0,0 +1,34 @@
import { buildPostgresOptions, databaseUrlFor, DatabaseGuardError } from "../src/infra/db/connection-policy";
import { createPostgresDb } from "../src/infra/db/postgres";
import { assertInitAuthorized, installProjectIdentity } from "../src/infra/db/project-identity";
import { assertAdminTarget } from "../src/infra/db/target-guard";
import { bootstrapScript, runScript } from "./_bootstrap";
/**
* Grava `adaptador_guard.project_identity` uma única vez.
* Exige ADAPTER_INIT_PROJECT_IDENTITY=1 e o destino administrativo já conferido.
* Migrate, seed e deploy não chamam este script.
*/
try {
assertInitAuthorized(process.env);
} catch (error) {
const code = error instanceof DatabaseGuardError ? error.code : "init_recusada";
console.error(`[init-project-identity] ${code}`);
process.exit(1);
}
bootstrapScript("init-project-identity");
runScript("init-project-identity", async () => {
const databaseUrl = databaseUrlFor("admin");
if (!databaseUrl) throw new DatabaseGuardError("url_ausente");
const db = createPostgresDb(databaseUrl, buildPostgresOptions(databaseUrl, "admin"));
try {
await assertAdminTarget(db);
const ref = process.env.ADAPTER_DB_PROJECT_REF?.trim() ?? "";
const result = await installProjectIdentity(db, ref);
console.log(`identidade: ${result}`);
} finally {
await db.close().catch(() => undefined);
}
});
+105
View File
@@ -0,0 +1,105 @@
-- Inicialização explícita da identidade de adaptador-planilha-hml.
-- Não faz parte de migrations/. Migrate, seed e deploy não executam este arquivo.
-- Substitua <<PROJECT_REF>> pelo Reference ID do Dashboard: 20 letras ou dígitos minúsculos.
-- Rode o bloco inteiro de uma vez. Qualquer falha desfaz o schema inteiro.
-- Não altera app.settings.jwt_exp nem os schemas auth e storage.
-- Se o schema já existir, não use este arquivo. O comando identity:init recusa linha divergente e objeto desconhecido.
do $identity$
begin
create schema adaptador_guard;
revoke all on schema adaptador_guard from public;
create table adaptador_guard.project_identity (
singleton boolean primary key default true check (singleton),
project_name text not null,
project_ref text not null,
constraint project_identity_name check (project_name = 'adaptador-planilha-hml'),
constraint project_identity_ref check (project_ref ~ '^[a-z0-9]{20}$')
);
revoke all on table adaptador_guard.project_identity from public;
alter table adaptador_guard.project_identity enable row level security;
alter table adaptador_guard.project_identity force row level security;
comment on table adaptador_guard.project_identity is
'Identidade persistente do projeto de homologacao. Nao e prova criptografica.';
create function adaptador_guard.reject_identity_change()
returns trigger
language plpgsql
security invoker
set search_path = pg_catalog
as $body$
begin
if tg_op <> 'INSERT' then
raise exception 'identidade imutavel' using errcode = '42501';
end if;
if session_user <> 'postgres' and session_user !~ '^postgres[.][a-z0-9]{20}$' then
raise exception 'identidade protegida' using errcode = '42501';
end if;
return new;
end;
$body$;
create trigger project_identity_guard
before insert or update or delete on adaptador_guard.project_identity
for each row execute function adaptador_guard.reject_identity_change();
create function adaptador_guard.adapter_project_identity()
returns table (project_name text, project_ref text)
language sql
stable
security definer
set search_path = pg_catalog
as $body$
select i.project_name, i.project_ref
from adaptador_guard.project_identity i
$body$;
revoke all on function adaptador_guard.adapter_project_identity() from public;
revoke all on function adaptador_guard.reject_identity_change() from public;
insert into adaptador_guard.project_identity (project_name, project_ref)
values ('adaptador-planilha-hml', '<<PROJECT_REF>>');
revoke all on schema adaptador_guard from public;
revoke all on table adaptador_guard.project_identity from public;
revoke all on all functions in schema adaptador_guard from public;
alter default privileges in schema adaptador_guard revoke all on tables from public;
alter default privileges in schema adaptador_guard revoke execute on functions from public;
if exists (select 1 from pg_catalog.pg_roles where rolname = 'adaptador_web') then
revoke all on schema adaptador_guard from adaptador_web;
revoke all on table adaptador_guard.project_identity from adaptador_web;
revoke all on all functions in schema adaptador_guard from adaptador_web;
grant usage on schema adaptador_guard to adaptador_web;
grant execute on function adaptador_guard.adapter_project_identity() to adaptador_web;
end if;
if exists (select 1 from pg_catalog.pg_roles where rolname = 'adaptador_worker') then
revoke all on schema adaptador_guard from adaptador_worker;
revoke all on table adaptador_guard.project_identity from adaptador_worker;
revoke all on all functions in schema adaptador_guard from adaptador_worker;
grant usage on schema adaptador_guard to adaptador_worker;
grant execute on function adaptador_guard.adapter_project_identity() to adaptador_worker;
end if;
if exists (select 1 from pg_catalog.pg_roles where rolname = 'anon') then
revoke all on schema adaptador_guard from anon;
revoke all on table adaptador_guard.project_identity from anon;
revoke all on all functions in schema adaptador_guard from anon;
end if;
if exists (select 1 from pg_catalog.pg_roles where rolname = 'authenticated') then
revoke all on schema adaptador_guard from authenticated;
revoke all on table adaptador_guard.project_identity from authenticated;
revoke all on all functions in schema adaptador_guard from authenticated;
end if;
if exists (select 1 from pg_catalog.pg_roles where rolname = 'service_role') then
revoke all on schema adaptador_guard from service_role;
revoke all on table adaptador_guard.project_identity from service_role;
revoke all on all functions in schema adaptador_guard from service_role;
end if;
if exists (select 1 from pg_catalog.pg_roles where rolname = 'app_user') then
revoke all on schema adaptador_guard from app_user;
revoke all on table adaptador_guard.project_identity from app_user;
revoke all on all functions in schema adaptador_guard from app_user;
end if;
end
$identity$;
+113
View File
@@ -0,0 +1,113 @@
-- Remove somente os objetos conhecidos de adaptador_guard.
-- Recusa se faltar peça, se houver objeto estranho ou se outro schema depender da identidade.
-- Não apaga auth, storage, public nem app.settings.jwt_exp.
-- O bloco e os DROP seguem juntos: se a conferência falhar, nada é removido.
do $revert$
begin
if not exists (select 1 from pg_catalog.pg_namespace where nspname = 'adaptador_guard') then
raise exception 'marcador_ausente' using errcode = 'P0002';
end if;
if exists (
select kind, name
from (
select 'missing'::text as kind, 'project_identity'::text as name
where exists (select 1 from pg_catalog.pg_namespace where nspname = 'adaptador_guard')
and not exists (
select 1
from pg_catalog.pg_class c
join pg_catalog.pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'adaptador_guard' and c.relname = 'project_identity' and c.relkind = 'r'
)
union all
select 'missing', 'adapter_project_identity'
where exists (select 1 from pg_catalog.pg_namespace where nspname = 'adaptador_guard')
and not exists (
select 1
from pg_catalog.pg_proc p
join pg_catalog.pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'adaptador_guard' and p.proname = 'adapter_project_identity'
)
union all
select 'missing', 'reject_identity_change'
where exists (select 1 from pg_catalog.pg_namespace where nspname = 'adaptador_guard')
and not exists (
select 1
from pg_catalog.pg_proc p
join pg_catalog.pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'adaptador_guard' and p.proname = 'reject_identity_change'
)
union all
select 'unknown_relation', c.relname
from pg_catalog.pg_class c
join pg_catalog.pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'adaptador_guard'
and not (
(c.relname = 'project_identity' and c.relkind = 'r')
or (
c.relkind = 'i'
and exists (
select 1
from pg_catalog.pg_index i
join pg_catalog.pg_class t on t.oid = i.indrelid
where i.indexrelid = c.oid
and t.relname = 'project_identity'
and t.relnamespace = n.oid
)
)
)
union all
select 'unknown_function', p.proname
from pg_catalog.pg_proc p
join pg_catalog.pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'adaptador_guard'
and p.proname not in ('adapter_project_identity', 'reject_identity_change')
union all
select 'unknown_type', t.typname
from pg_catalog.pg_type t
join pg_catalog.pg_namespace n on n.oid = t.typnamespace
where n.nspname = 'adaptador_guard'
and not exists (
select 1
from pg_catalog.pg_class c
where c.oid = t.typrelid
and c.relname = 'project_identity'
and c.relnamespace = n.oid
)
and not exists (
select 1
from pg_catalog.pg_type elem
join pg_catalog.pg_class c on c.oid = elem.typrelid
where elem.oid = t.typelem
and c.relname = 'project_identity'
and c.relnamespace = n.oid
)
union all
select 'external', vn.nspname || '.' || v.relname
from pg_catalog.pg_depend d
join pg_catalog.pg_rewrite rw on rw.oid = d.objid
join pg_catalog.pg_class v on v.oid = rw.ev_class
join pg_catalog.pg_namespace vn on vn.oid = v.relnamespace
join pg_catalog.pg_class t on t.oid = d.refobjid
join pg_catalog.pg_namespace tn on tn.oid = t.relnamespace
where tn.nspname = 'adaptador_guard'
and vn.nspname <> 'adaptador_guard'
union all
select 'external', n.nspname || '.' || p.proname
from pg_catalog.pg_depend d
join pg_catalog.pg_proc p on p.oid = d.objid
join pg_catalog.pg_namespace n on n.oid = p.pronamespace
join pg_catalog.pg_class t on t.oid = d.refobjid
join pg_catalog.pg_namespace tn on tn.oid = t.relnamespace
where tn.nspname = 'adaptador_guard'
and n.nspname <> 'adaptador_guard'
) findings
) then
raise exception 'reversao_recusada' using errcode = '55000';
end if;
end
$revert$;
drop function adaptador_guard.adapter_project_identity();
drop table adaptador_guard.project_identity;
drop function adaptador_guard.reject_identity_change();
drop schema adaptador_guard restrict;
+39
View File
@@ -0,0 +1,39 @@
-- Leitura da identidade e dos privilégios. Não grava.
select project_name, project_ref
from adaptador_guard.adapter_project_identity();
select p.proname, p.prosecdef, p.proconfig
from pg_catalog.pg_proc p
join pg_catalog.pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'adaptador_guard'
order by p.proname;
select c.relname, c.relrowsecurity, c.relforcerowsecurity
from pg_catalog.pg_class c
join pg_catalog.pg_namespace n on n.oid = c.relnamespace
where n.nspname = 'adaptador_guard'
and c.relkind = 'r'
order by c.relname;
select 'public' as papel,
has_schema_privilege('public', 'adaptador_guard', 'usage') as uso,
has_table_privilege('public', 'adaptador_guard.project_identity', 'select') as leitura,
has_table_privilege('public', 'adaptador_guard.project_identity', 'update') as alteracao,
has_function_privilege('public', 'adaptador_guard.adapter_project_identity()', 'execute') as executar
union all
select r.rolname,
has_schema_privilege(r.oid, 'adaptador_guard', 'usage'),
has_table_privilege(r.oid, 'adaptador_guard.project_identity', 'select'),
has_table_privilege(r.oid, 'adaptador_guard.project_identity', 'update'),
has_function_privilege(r.oid, 'adaptador_guard.adapter_project_identity()', 'execute')
from pg_catalog.pg_roles r
where r.rolname in ('anon', 'authenticated', 'service_role', 'app_user', 'adaptador_web', 'adaptador_worker')
order by papel;
select item
from pg_catalog.pg_db_role_setting s
cross join lateral unnest(s.setconfig) as item
where s.setdatabase = (select oid from pg_catalog.pg_database where datname = current_database())
and s.setrole = 0
order by item;