diff --git a/src/lib/features/users/user-store.ts b/src/lib/features/users/user-store.ts index 1fe1df053a..81c4e2feef 100644 --- a/src/lib/features/users/user-store.ts +++ b/src/lib/features/users/user-store.ts @@ -104,7 +104,7 @@ export class UserStore implements IUserStore { async insert(user: ICreateUser): Promise { const emailHash = user.email - ? this.db.raw('md5(?)', [user.email]) + ? this.db.raw(`encode(sha256(?::bytea), 'hex')`, [user.email]) : null; const rows = await this.db(TABLE) .insert({ diff --git a/src/migrations/20241112113555-user-email-hash.js b/src/migrations/20241112113555-user-email-hash.js index 08d04517b8..52830cfa7d 100644 --- a/src/migrations/20241112113555-user-email-hash.js +++ b/src/migrations/20241112113555-user-email-hash.js @@ -2,9 +2,6 @@ exports.up = (db, cb) => { db.runSql(` ALTER TABLE users ADD COLUMN IF NOT EXISTS email_hash VARCHAR(32); - - UPDATE users - SET email_hash = md5(email::text); `, cb); }; diff --git a/src/migrations/20250211114205-scim-email-hash-backfill.js b/src/migrations/20250211114205-scim-email-hash-backfill.js index e1ae9ad0f7..a0fe9ab365 100644 --- a/src/migrations/20250211114205-scim-email-hash-backfill.js +++ b/src/migrations/20250211114205-scim-email-hash-backfill.js @@ -1,14 +1,8 @@ exports.up = (db, cb) => { - db.runSql(` - UPDATE users - SET email_hash = md5(email::text) - WHERE scim_id IS NOT NULL; - `, cb); - + cb(); }; - + exports.down = (db, cb) => { cb(); }; - - \ No newline at end of file + diff --git a/src/migrations/20250612070900-backfill-email-hashes-using-sha256.js b/src/migrations/20250612070900-backfill-email-hashes-using-sha256.js new file mode 100644 index 0000000000..d243cea23f --- /dev/null +++ b/src/migrations/20250612070900-backfill-email-hashes-using-sha256.js @@ -0,0 +1,12 @@ +exports.up = function(db, cb) { + db.runSql(` + ALTER TABLE users DROP COLUMN email_hash; + ALTER TABLE users ADD COLUMN email_hash TEXT; + UPDATE users SET email_hash = encode(sha256(email::bytea), 'hex') WHERE email IS NOT NULL; + CREATE INDEX users_email_hash_idx ON users(email_hash); +`, cb); +}; + +exports.down = function(db, cb) { + cb(); +}; diff --git a/src/migrations/20250612114001-backfill-email-hashes-from-event-log.js b/src/migrations/20250612114001-backfill-email-hashes-from-event-log.js new file mode 100644 index 0000000000..3da653caf1 --- /dev/null +++ b/src/migrations/20250612114001-backfill-email-hashes-from-event-log.js @@ -0,0 +1,19 @@ +exports.up = function(db, cb) { + db.runSql(` + WITH deleted_users AS ( + SELECT (pre_data ->> 'id')::int AS user_id, + pre_data ->> 'email' AS email + FROM events + WHERE type = 'user-deleted' + AND created_at >= now() - interval '30 days' + ) + UPDATE users u + SET email_hash = encode(sha256(du.email::bytea), 'hex') + FROM deleted_users du + WHERE u.id = du.user_id AND u.email IS NULL; + `, cb); +}; + +exports.down = function(db, cb) { + cb(); +}; diff --git a/src/test/e2e/api/admin/user-admin.e2e.test.ts b/src/test/e2e/api/admin/user-admin.e2e.test.ts index 896c625de1..fb0e5919e4 100644 --- a/src/test/e2e/api/admin/user-admin.e2e.test.ts +++ b/src/test/e2e/api/admin/user-admin.e2e.test.ts @@ -409,7 +409,7 @@ test('Anonymises name, username and email fields if anonymiseEventLog flag is se expect(body.users[0].username).toEqual(''); // Not set, so anonymise should return the empty string. }); -test('creates user with email md5 hash', async () => { +test('creates user with email sha256 hash', async () => { await app.request .post('/api/admin/user-admin') .send({ @@ -424,7 +424,7 @@ test('creates user with email md5 hash', async () => { .where({ email: 'hasher@getunleash.ai' }) .first(['email_hash']); - const expectedHash = createHash('md5') + const expectedHash = createHash('sha256') .update('hasher@getunleash.ai') .digest('hex');