Script SQL anonimizzazione db Odoo 14

-- Odoo 14 – Anonimizzazione DB (PostgreSQL) – compatibile e safe
-- Esegui su una COPIA del database.

CREATE EXTENSION IF NOT EXISTS pgcrypto;

-- =========================
-- Funzioni helper
-- =========================

-- Email fittizia deterministica
CREATE OR REPLACE FUNCTION _anon_email(p_id bigint, p_prefix text DEFAULT 'u')
RETURNS text LANGUAGE sql IMMUTABLE AS $$
  SELECT p_prefix || substring(encode(digest(p_id::text,'sha256'),'hex') FROM 1 FOR 10) || '@example.invalid'
$$;

-- Telefono fittizio deterministico
CREATE OR REPLACE FUNCTION _anon_phone(p_id bigint)
RETURNS text LANGUAGE sql IMMUTABLE AS $$
  SELECT '+39 3' || substring(encode(digest(p_id::text,'sha256'),'hex') FROM 1 FOR 1)
         || ' ' || substring(encode(digest(p_id::text,'sha256'),'hex') FROM 2 FOR 8)
$$;

-- Nome sintetico deterministico
CREATE OR REPLACE FUNCTION _anon_name(p_id bigint, p_prefix text)
RETURNS text LANGUAGE sql IMMUTABLE AS $$
  SELECT p_prefix || ' ' || upper(substring(encode(digest(p_id::text,'sha256'),'hex') FROM 1 FOR 8))
$$;

-- VAT fittizia deterministica
CREATE OR REPLACE FUNCTION _anon_vat(p_id bigint)
RETURNS text LANGUAGE sql IMMUTABLE AS $$
  WITH h AS (SELECT encode(digest(p_id::text,'sha256'),'hex') AS hx)
  SELECT 'IT' || substring(regexp_replace(hx,'\D','0','g') FROM 1 FOR 11) FROM h
$$;

-- Indirizzo sintetico deterministico
CREATE OR REPLACE FUNCTION _anon_address(p_id bigint)
RETURNS TABLE (street text, street2 text, zip text, city text, state_id int, country_id int)
LANGUAGE plpgsql IMMUTABLE AS $$
DECLARE h text;
BEGIN
  h := encode(digest(p_id::text,'sha256'),'hex');
  street := 'Via ' || upper(substring(h,1,8)) || ', ' || ((p_id % 200)+1)::text;
  street2 := NULL;
  zip := lpad(((p_id % 90000) + 10000)::text, 5, '0');
  city := 'Citta' || substring(h,9,4);
  state_id := NULL;
  country_id := NULL;
  RETURN;
END
$$;

-- Prefisso robusto per partner: usa is_company se presente, altrimenti 'Partner'
CREATE OR REPLACE FUNCTION _partner_prefix(p_id bigint)
RETURNS text LANGUAGE plpgsql STABLE AS $$
DECLARE has_is_company boolean; is_comp boolean := false;
BEGIN
  SELECT EXISTS (
    SELECT 1 FROM information_schema.columns
    WHERE table_schema='public' AND table_name='res_partner' AND column_name='is_company'
  ) INTO has_is_company;

  IF has_is_company THEN
    EXECUTE 'SELECT COALESCE(is_company,false) FROM res_partner WHERE id=$1'
      INTO is_comp USING p_id;
  END IF;

  RETURN CASE WHEN is_comp THEN 'Company' ELSE 'Partner' END;
END
$$;

-- =========================
-- res.partner
-- =========================
UPDATE res_partner p
SET name       = _anon_name(p.id, _partner_prefix(p.id)),
    email      = _anon_email(p.id, 'p'),
    phone      = CASE WHEN p.phone  IS NOT NULL THEN _anon_phone(p.id) ELSE NULL END,
    mobile     = CASE WHEN p.mobile IS NOT NULL THEN _anon_phone(p.id+999999) ELSE NULL END,
    vat        = CASE WHEN p.vat    IS NOT NULL THEN _anon_vat(p.id)  ELSE NULL END,
    website    = NULL,
    function   = CASE WHEN p.function IS NOT NULL THEN 'Role ' || substring(encode(digest(p.id::text,'sha256'),'hex') FROM 1 FOR 6) ELSE NULL END,
    comment    = NULL,
    street     = (SELECT street  FROM _anon_address(p.id)),
    street2    = (SELECT street2 FROM _anon_address(p.id)),
    zip        = (SELECT zip     FROM _anon_address(p.id)),
    city       = (SELECT city    FROM _anon_address(p.id)),
    state_id   = (SELECT state_id   FROM _anon_address(p.id)),
    country_id = (SELECT country_id FROM _anon_address(p.id))
WHERE p.id != 1;  -- opzionale: preserva il partner "Your Company" se è 1

-- =========================
-- res.company (compatibile)
-- =========================
-- La maggior parte dei campi contatto è related al partner: l'anonimizzazione sopra basta.
UPDATE res_company c
SET name = _anon_name(c.id,'Company');

-- Azzeramento difensivo di colonne opzionali, solo se presenti
DO $$
DECLARE col text;
BEGIN
  FOREACH col IN ARRAY ARRAY['email','phone','website','street','street2','zip','city','company_registry','rml_footer'] LOOP
    IF EXISTS (
      SELECT 1 FROM information_schema.columns
      WHERE table_schema='public' AND table_name='res_company' AND column_name=col
    ) THEN
      EXECUTE format('UPDATE res_company SET %I=NULL', col);
    END IF;
  END LOOP;
END
$$;

-- =========================
-- res.users (compatibile)
-- =========================
WITH keep AS (
  SELECT id FROM res_users WHERE login IN ('admin','__system__') OR id IN (2)
)
UPDATE res_users u
SET login='user.' || substring(encode(digest(u.id::text,'sha256'),'hex') FROM 1 FOR 10),
    signature=NULL
WHERE u.id NOT IN (SELECT id FROM keep);

-- Azzeramento difensivo di colonne opzionali se esistono
DO $$
DECLARE col text;
BEGIN
  FOREACH col IN ARRAY ARRAY['email','phone','mobile'] LOOP
    IF EXISTS (
      SELECT 1 FROM information_schema.columns
      WHERE table_schema='public' AND table_name='res_users' AND column_name=col
    ) THEN
      EXECUTE format(
        'UPDATE res_users SET %I=NULL WHERE id NOT IN (SELECT id FROM res_users WHERE login IN (''admin'',''__system__'') OR id IN (2))',
        col
      );
    END IF;
  END LOOP;
END
$$;

-- =========================
-- Mail server off
-- =========================
UPDATE ir_mail_server SET active=FALSE, smtp_pass=NULL;

-- =========================
-- CRM lead/opportunity
-- =========================
UPDATE crm_lead l
SET contact_name = CASE WHEN l.contact_name IS NOT NULL THEN _anon_name(l.id,'Contact') ELSE NULL END,
    email_from   = CASE WHEN l.email_from  IS NOT NULL THEN _anon_email(l.id,'l')    ELSE NULL END,
    phone        = CASE WHEN l.phone       IS NOT NULL THEN _anon_phone(l.id)       ELSE NULL END,
    mobile       = CASE WHEN l.mobile      IS NOT NULL THEN _anon_phone(l.id+777777) ELSE NULL END,
    street       = (SELECT street  FROM _anon_address(l.id)),
    street2      = (SELECT street2 FROM _anon_address(l.id)),
    city         = (SELECT city    FROM _anon_address(l.id)),
    zip          = (SELECT zip     FROM _anon_address(l.id)),
    description  = NULL;

-- =========================
-- HR (se presente)
-- =========================
UPDATE hr_employee e
SET name = _anon_name(e.id,'Employee'),
    work_email   = _anon_email(e.id,'e'),
    mobile_phone = CASE WHEN e.mobile_phone IS NOT NULL THEN _anon_phone(e.id) ELSE NULL END,
    work_phone   = NULL,
    identification_id = NULL,
    address_home_id   = address_home_id
WHERE EXISTS (SELECT 1 FROM pg_class WHERE relname='hr_employee');

-- =========================
-- Vendite/Acquisti/Contabilità (testi liberi)
-- =========================
UPDATE sale_order     SET client_order_ref=NULL, note=NULL;
UPDATE purchase_order SET notes=NULL;
UPDATE account_move   SET narration=NULL;

UPDATE account_move_line aml
SET name = CASE WHEN aml.display_type IS NULL
                THEN 'Line ' || substring(encode(digest(aml.id::text,'sha256'),'hex') FROM 1 FOR 6)
                ELSE aml.name END;

-- =========================
-- Messaggistica
-- =========================
UPDATE mail_message
SET body=NULL, email_from=NULL, message_id=NULL, reply_to=NULL;

-- mail_mail – compatibile con schemi senza alcune colonne
DO $$
DECLARE col text;
BEGIN
  FOREACH col IN ARRAY ARRAY['email_to','email_cc','email_bcc','reply_to','message_id','email_from','headers','subject','body_html'] LOOP
    IF EXISTS (
      SELECT 1
      FROM information_schema.columns
      WHERE table_schema='public' AND table_name='mail_mail' AND column_name=col
    ) THEN
      EXECUTE format('UPDATE mail_mail SET %I = NULL', col);
    END IF;
  END LOOP;

  IF EXISTS (
    SELECT 1 FROM information_schema.columns
    WHERE table_schema='public' AND table_name='mail_mail' AND column_name='state'
  ) THEN
    EXECUTE 'UPDATE mail_mail SET state = ''cancel''';
  END IF;
END
$$;

-- =========================
-- Allegati (compatibile con varianti di schema)
-- =========================
DO $$
DECLARE
  has_res_model boolean;
  has_type      boolean;
  where_expr    text := '1=1';
  col           text;
BEGIN
  SELECT EXISTS (
    SELECT 1 FROM information_schema.columns
    WHERE table_schema='public' AND table_name='ir_attachment' AND column_name='res_model'
  ) INTO has_res_model;

  SELECT EXISTS (
    SELECT 1 FROM information_schema.columns
    WHERE table_schema='public' AND table_name='ir_attachment' AND column_name='type'
  ) INTO has_type;

  IF has_res_model THEN
    where_expr := where_expr || ' AND res_model IN (' ||
      quote_literal('res.partner') || ',' ||
      quote_literal('res.company') || ',' ||
      quote_literal('crm.lead') || ',' ||
      quote_literal('sale.order') || ',' ||
      quote_literal('purchase.order') || ',' ||
      quote_literal('account.move') || ',' ||
      quote_literal('mail.message') || ',' ||
      quote_literal('mail.mail') || ',' ||
      quote_literal('hr.employee') || ')';
  END IF;

  IF has_type THEN
    where_expr := where_expr || ' AND type = ' || quote_literal('binary');
  END IF;

  FOREACH col IN ARRAY ARRAY['datas','db_datas','store_fname','checksum','index_content','url'] LOOP
    IF EXISTS (
      SELECT 1 FROM information_schema.columns
      WHERE table_schema='public' AND table_name='ir_attachment' AND column_name=col
    ) THEN
      EXECUTE format('UPDATE ir_attachment SET %I = NULL WHERE %s', col, where_expr);
    END IF;
  END LOOP;

  IF EXISTS (
    SELECT 1 FROM information_schema.columns
    WHERE table_schema='public' AND table_name='ir_attachment' AND column_name='file_size'
  ) THEN
    EXECUTE format('UPDATE ir_attachment SET file_size = 0 WHERE %s', where_expr);
  END IF;
END
$$;

-- =========================
-- Marketing/Tracking
-- =========================
UPDATE utm_source   SET name='SRC-' || substring(encode(digest(id::text,'sha256'),'hex') FROM 1 FOR 6);
UPDATE utm_medium   SET name='MED-' || substring(encode(digest(id::text,'sha256'),'hex') FROM 1 FOR 6);
UPDATE utm_campaign SET name='CMP-' || substring(encode(digest(id::text,'sha256'),'hex') FROM 1 FOR 6);

-- =========================
-- Parametri/config esterni – safe (no NULL su value)
-- =========================
DO $$
BEGIN
  UPDATE ir_config_parameter
  SET value = CASE
    WHEN key IN ('web.base.url', 'web.base.url.freeze')
      THEN 'http://example.invalid'
    WHEN key = 'mail.catchall.domain'
      THEN 'example.invalid'
    WHEN key IN ('mail.catchall.alias', 'mail.bounce.alias', 'fetchmail.user')
      THEN 'disabled'
    WHEN key = 'fetchmail.server'
      THEN 'localhost'
    WHEN key = 'fetchmail.port'
      THEN '0'
    WHEN key = 'fetchmail.password'
      THEN ''       -- vuoto per rispettare NOT NULL
    WHEN key = 'database.uuid'
      THEN substring(encode(digest(current_database() || '-' || now()::text, 'sha256'),'hex') FROM 1 FOR 32)
    ELSE value
  END
  WHERE key IN (
    'web.base.url.freeze','web.base.url','database.uuid',
    'mail.catchall.domain','mail.catchall.alias','mail.bounce.alias',
    'fetchmail.server','fetchmail.port','fetchmail.user','fetchmail.password'
  );
END
$$;

 

Lascia un commento