From b428ce9086166e6a4df2d9a6c59ac61bb9feaf7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Fromme?= Date: Tue, 27 Oct 2026 10:13:02 +0100 Subject: [PATCH] feat: ddev anonymize-db removes personal data from production dumps Runs automatically after every `ddev import-db`. Drops all zzz_deleted_ tables (renamed in production but still holding their data, e.g. the retired address check and form answers), replaces names and emails of backend and frontend users, sets every password to the local dev password "password" and clears reset tokens and MFA secrets. Sessions, record locks, CSP reports and logs are emptied (IP addresses, user agents). Usernames stay, so everybody logs in locally as their own user. Co-Authored-By: Claude Opus 5.5 --- .ddev/anonymize-db.sql | 44 +++++++++++++++++++++++++++++++++ .ddev/commands/web/anonymize-db | 19 ++++++++++++++ .ddev/config.yaml | 4 +++ 3 files changed, 67 insertions(+) create mode 100644 .ddev/anonymize-db.sql create mode 100755 .ddev/commands/web/anonymize-db diff --git a/.ddev/anonymize-db.sql b/.ddev/anonymize-db.sql new file mode 100644 index 0000000..1ba6332 --- /dev/null +++ b/.ddev/anonymize-db.sql @@ -0,0 +1,44 @@ +-- Removes personal data from a production dump. Run by `ddev anonymize-db`, which ddev also +-- runs after every `ddev import-db`. Safe to run more than once. +-- @devPasswordHash is set by the command before this file runs. + +-- Tables renamed by the database analyzer in production keep their data until dropped there +SET @zzzTables = ( + SELECT GROUP_CONCAT('`', table_name, '`') + FROM information_schema.tables + WHERE table_schema = DATABASE() AND table_name LIKE 'zzz\_deleted\_%' +); +SET @dropZzzTables = IF(@zzzTables IS NULL, 'DO 0', CONCAT('DROP TABLE ', @zzzTables)); +PREPARE dropZzzTables FROM @dropZzzTables; +EXECUTE dropZzzTables; +DEALLOCATE PREPARE dropZzzTables; + +-- Usernames stay, so everybody logs in locally as their own user (with the dev password) +UPDATE be_users SET + realName = IF('' = realName, '', CONCAT('BE User ', uid)), + email = IF('' = email, '', CONCAT('be-user-', uid, '@example.invalid')), + password = @devPasswordHash, + password_reset_token = '', + mfa = NULL; + +UPDATE fe_users SET + username = CONCAT('fe-user-', uid), + password = @devPasswordHash, + name = IF('' = name, '', CONCAT('FE User ', uid)), + first_name = IF('' = first_name, '', 'Vorname'), + middle_name = '', + last_name = IF('' = last_name, '', CONCAT('Nachname ', uid)), + address = IF('' = address, '', 'Musterstraße 1'), + telephone = IF('' = telephone, '', '0000 000000'), + email = IF('' = email, '', CONCAT('fe-user-', uid, '@example.invalid')), + zip = IF('' = zip, '', '00000'), + city = IF('' = city, '', 'Musterstadt'); + +-- Sessions, locks, reports and logs: IP addresses, user agents, user data +TRUNCATE be_sessions; +TRUNCATE fe_sessions; +TRUNCATE sys_lockedrecords; +TRUNCATE sys_http_report; +TRUNCATE sys_log; +TRUNCATE tx_epproducts_log; +TRUNCATE tx_epevents_log; diff --git a/.ddev/commands/web/anonymize-db b/.ddev/commands/web/anonymize-db new file mode 100755 index 0000000..9231a9c --- /dev/null +++ b/.ddev/commands/web/anonymize-db @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +## Description: Remove personal data from the imported production database +## Usage: anonymize-db +## Example: "ddev anonymize-db" + +set -euo pipefail + +DEV_PASSWORD=password + +# TYPO3 verifies argon2i hashes with plain password_verify() +hash=$(php -r 'echo password_hash($argv[1], PASSWORD_ARGON2I);' "$DEV_PASSWORD") + +{ + echo "SET @devPasswordHash = '${hash}';" + cat /var/www/html/.ddev/anonymize-db.sql +} | mysql --host=db --user=db --password=db db + +echo "Database anonymized. Backend users log in with their username and the password '${DEV_PASSWORD}'." diff --git a/.ddev/config.yaml b/.ddev/config.yaml index 79d717a..f8e37e5 100644 --- a/.ddev/config.yaml +++ b/.ddev/config.yaml @@ -313,3 +313,7 @@ corepack_enable: false #hooks: # post-start: # - exec: composer install -d /var/www/html + +hooks: + post-import-db: + - exec: .ddev/commands/web/anonymize-db