🔒 Tutorial: tornar installation_configs somente-leitura no PostgreSQL (Chatwoot)

🔒 Tutorial: tornar installation_configs somente-leitura no PostgreSQL (Chatwoot)

Objetivo

Bloquear completamente qualquer escrita em public.installation_configs, mantendo apenas leitura, reduzindo o risco de alteração de parâmetros críticos pela aplicação.

Importante: este procedimento altera a estrutura (DDL). Recomenda-se janela de manutenção e, idealmente, parar Chatwoot (rails/sidekiq) durante a mudança.

Antes de começar (checklist rápido)

Você precisa:

  • Acesso ao PostgreSQL como usuário com permissão de DDL (ex.: postgres).
  • Confirmar:
    • Nome do banco (ex.: chatwoot_production)
    • Usuário que a aplicação usa para conectar (ex.: chatwoot_prod)
    • Caminho de backup (ex.: /opt/backups)
  • Janela de manutenção (recomendado): renomear tabela pode causar erros se a app estiver em uso.

Boas práticas:

  • Se você usa PgBouncer em transaction pooling, faça DDL direto no Postgres, não via PgBouncer.
  • Se possível, tenha usuário de migração/admin separado do usuário “app” (produção).

Etapa 0 — Validação inicial (confira se é tabela e o conteúdo)

Conecte no banco e verifique o objeto atual:

\d+ public.installation_configs

Verifique os registros que você pretende ajustar:

SELECT name, serialized_value
FROM public.installation_configs
WHERE name IN (
  'INSTALLATION_NAME',
  'INSTALLATION_PRICING_PLAN',
  'INSTALLATION_PRICING_PLAN_QUANTITY'
);

Etapa 1 — Ajustar valores necessários antes do bloqueio

Por que isto antes?

Depois que virarmos view somente-leitura, atualizações não serão mais possíveis (por design).

Atualização (com YAML Ruby correto)

No PostgreSQL, use strings com escape E'' para \n. Exemplo:

UPDATE public.installation_configs
SET serialized_value = CASE name
  WHEN 'INSTALLATION_NAME'
    THEN E'--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\nvalue: IALUI\n'
  WHEN 'INSTALLATION_PRICING_PLAN'
    THEN E'--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\nvalue: premium\n'
  WHEN 'INSTALLATION_PRICING_PLAN_QUANTITY'
    THEN E'--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\nvalue: 10000\n'
  ELSE serialized_value
END
WHERE name IN (
  'INSTALLATION_NAME',
  'INSTALLATION_PRICING_PLAN',
  'INSTALLATION_PRICING_PLAN_QUANTITY'
)
RETURNING name;

Observação: ajuste os valores (IALUI, premium, 10000) conforme seu cenário.

Etapa 2 — Backup (obrigatório)

2.1 Backup lógico (pg_dump)

Execute no host que tem acesso ao Postgres:

pg_dump -U postgres -Fc chatwoot_production \
  -f /opt/backups/chatwoot_before_installation_configs_readonly_$(date +%F).dump

Se o Postgres estiver em container, rode via docker exec no container do Postgres (ou conecte por host/porta direto no Postgres).

Etapa 3 — Transformar tabela em view e bloquear escrita

3.1 Conectar no banco (psql)

Exemplo:

sudo -u postgres psql -d chatwoot_production -v ON_ERROR_STOP=1

-v ON_ERROR_STOP=1 evita “meia-execução” silenciosa.

3.2 Executar tudo em transação (fail-safe)

3.2.1 Iniciar transação

BEGIN;

3.2.2 Renomear a tabela original

ALTER TABLE public.installation_configs
RENAME TO installation_configs_data;

3.2.3 Criar a view com o nome original

CREATE VIEW public.installation_configs AS
SELECT * FROM public.installation_configs_data;

Etapa 4 — Permissões (essencial para segurança real)

4.1 Revogar escrita do usuário da aplicação

Substitua chatwoot_prod pelo usuário real da aplicação.

Revogue na VIEW:

REVOKE ALL ON public.installation_configs FROM chatwoot_prod;
GRANT SELECT ON public.installation_configs TO chatwoot_prod;

Revogue também na TABELA “data” (boa prática):

REVOKE ALL ON public.installation_configs_data FROM chatwoot_prod;

Por que revogar na installation_configs_data também?

Porque, se esse usuário tiver permissão ali, ele ainda poderia escrever diretamente na tabela (mesmo que a app normalmente use a view).

Etapa 5 — Bloqueio de escrita (recomendado: falhar com erro)

Você tem duas opções. Recomendo a Opção A (fail-fast), porque evita “silêncio” e facilita diagnóstico.

Opção A (recomendada): trigger INSTEAD OF que bloqueia e gera erro

CREATE OR REPLACE FUNCTION public.installation_configs_readonly()
RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
  RAISE EXCEPTION 'installation_configs é somente-leitura (view). Operação % bloqueada.', TG_OP;
END;
$$;

DROP TRIGGER IF EXISTS trg_installation_configs_readonly ON public.installation_configs;

CREATE TRIGGER trg_installation_configs_readonly
INSTEAD OF INSERT OR UPDATE OR DELETE ON public.installation_configs
FOR EACH STATEMENT
EXECUTE FUNCTION public.installation_configs_readonly();

Opção B (não recomendada): regras que ignoram escrita (silencioso)

Use apenas se você preferir “não quebrar” processos que tentem escrever (mas isso mascara problemas):

CREATE OR REPLACE RULE installation_configs_no_insert
  AS ON INSERT TO public.installation_configs DO INSTEAD NOTHING;

CREATE OR REPLACE RULE installation_configs_no_update
  AS ON UPDATE TO public.installation_configs DO INSTEAD NOTHING;

CREATE OR REPLACE RULE installation_configs_no_delete
  AS ON DELETE TO public.installation_configs DO INSTEAD NOTHING;

Etapa 6 — Confirmar alterações

COMMIT;

Etapa 7 — Testes (obrigatórios)

7.1 Leitura (deve funcionar)

SELECT * FROM public.installation_configs LIMIT 1;

7.2 Escrita (deve falhar, se você usou a Opção A)

UPDATE public.installation_configs
SET updated_at = NOW()
WHERE name = 'INSTALLATION_NAME';

7.3 Verifique se a tabela “data” continua íntegra

SELECT count(*) FROM public.installation_configs_data;

🔁 Rollback (desfazer com segurança)

Se precisar voltar à tabela original:

BEGIN;

DROP VIEW IF EXISTS public.installation_configs;

ALTER TABLE public.installation_configs_data
RENAME TO installation_configs;

COMMIT;

Se necessário, restaure do dump:

pg_restore -U postgres -d chatwoot_production -c /opt/backups/chatwoot_before_installation_configs_readonly_YYYY-MM-DD.dump

Observações finais (para operação e upgrades)

  • Atualizações do Chatwoot (migrations):
    Se futuras migrations tentarem alterar installation_configs, elas podem falhar, porque agora é view.
    Procedimento recomendado em upgrade:
    1. Fazer rollback para tabela (desfazer view)
    2. Rodar migrations/upgrade
    3. Reaplicar o bloqueio (recriar view + permissões + trigger)
  • Backup/restore:
    Garanta que installation_configs_data está incluída em rotinas de backup (ela é a tabela real agora).
  • Auditoria:
    Se quiser reforçar, registre o motivo e a data da mudança (change log interno) e valide em ambiente de homologação antes.

WhatsApp