fix(db): implementa identidade persistente segura para homologacao
This commit is contained in:
1 parent
2c8c5d9507
commit
a4f0a2f649
11 files changed
+1146
-106
No files matched your search
@@ -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);
|
||||
}
|
||||
});
|
||||
@@ -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$;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Reference in new issue
Block a user