From 902845bf825dac8359c5543eb9de2b9033f1c5f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Fournier?= Date: Wed, 9 Jul 2025 09:19:25 +0200 Subject: [PATCH] chore: amend user-created-missing events (#10333) ## About the changes Users could have been created in Unleash without a corresponding event (a.k.a. audit log), due to a non transactional user insert ([fix](https://github.com/Unleash/unleash/pull/10327)). This could have happened because of providing the wrong role id or some other causes we're not aware of. This amends the situation by inserting an event for each user that exists in the instance (not deleted) and doesn't have it's corresponding user-created event. The event is inserted as already announced because this happened in the past. The event log will look like this (simulated the situation in local dev): ```json { "id": 11, "type": "user-created", "createdBy": "unleash_system_user", "createdAt": "2025-07-08T16:06:17.428Z", "createdByUserId": null, "data": { "id": "6", "email": "xyz@three.com" }, "preData": null, "tags": [], "featureName": null, "project": null, "environment": null, "label": "User created", "summary": "**unleash_system_user** created user ****" } ``` The main problem is we can't create the event in the past, so this will have to do it --- ...250708173000-amend-users-created-events.js | 42 ++++++++++++ src/test/e2e/api/admin/user-admin.e2e.test.ts | 68 +++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 src/migrations/20250708173000-amend-users-created-events.js diff --git a/src/migrations/20250708173000-amend-users-created-events.js b/src/migrations/20250708173000-amend-users-created-events.js new file mode 100644 index 0000000000..05eaa68dfe --- /dev/null +++ b/src/migrations/20250708173000-amend-users-created-events.js @@ -0,0 +1,42 @@ +exports.up = function(db, cb) { + db.runSql(` + INSERT INTO events (created_at, created_by, type, data, announced) + SELECT + u.created_at AS created_at, + 'unleash_system_user' AS created_by, + 'user-created' AS type, + json_build_object( + 'id', u.id, + 'name', u.name, + 'email', u.email + )::jsonb AS data, + true AS announced + FROM users u + WHERE + is_system = false + AND is_service = false + AND deleted_at IS NULL + AND email IS NOT NULL + AND NOT EXISTS ( + SELECT 1 FROM events + WHERE type = 'user-created' + AND (data ->> 'id')::int = u.id + LIMIT 1 + ) + ON CONFLICT DO NOTHING; + `, (err, results) => { + if (err) { + console.log('Error inserting user-created events:', err); + return cb(err); + } + if (results.rowCount){ + console.log('Amended user-created event log. Number of new records:', results.rowCount); + } + cb(); + }); +}; + +exports.down = function(db, cb) { + cb(); + // No down migration needed as this is a data backfill. +}; 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 2327c4323c..6a55313471 100644 --- a/src/test/e2e/api/admin/user-admin.e2e.test.ts +++ b/src/test/e2e/api/admin/user-admin.e2e.test.ts @@ -1,3 +1,5 @@ +import postgresPkg from 'pg'; +const { Client } = postgresPkg; import { type IUnleashTest, setupAppWithCustomConfig, @@ -19,6 +21,11 @@ import { omitKeys } from '../../../../lib/util/omit-keys.js'; import type { ISessionStore } from '../../../../lib/types/stores/session-store.js'; import type { IUnleashStores } from '../../../../lib/types/index.js'; import { createHash } from 'crypto'; +import { createDb } from '../../../../lib/db/db-pool.js'; +import { migrateDb } from '../../../../migrator.js'; +import { createTestConfig } from '../../../config/test-config.js'; +import { v4 as uuidv4 } from 'uuid'; +import { getDbConfig } from '../../../../lib/server-impl.js'; let stores: IUnleashStores; let db: ITestDb; @@ -31,6 +38,67 @@ let sessionStore: ISessionStore; let editorRole: IRole; let adminRole: IRole; +describe('Users created without an event are amended', () => { + const testDbName = `migration_test_${uuidv4().replace(/-/g, '')}`; + const dbConnConfig = getDbConfig(); + beforeAll(async () => { + // create a new empty database for this test + const client = new Client(dbConnConfig); + await client.connect(); + await client.query(`CREATE DATABASE ${testDbName}`); + await client.end(); + }); + + test('should amend users created without an event', async () => { + // connect to the new database + const config = createTestConfig({ + db: { + ...dbConnConfig, + ssl: false, + database: testDbName, + }, + getLogger, + }); + const db = createDb(config); + // migrate up to the migration we want to test + await migrateDb(config, '20250707153020-unknown-flags-environment.js'); + + const eventsBefore = await db('events').select('*'); + const insertedUser = ( + await db('users') + .insert({ + created_at: new Date('2023-01-01T00:00:00Z'), + email: 'some@getunelash.io', + name: 'Some Name', + }) + .returning('*') + )[0]; + + expect(insertedUser).toBeDefined(); + expect(insertedUser.name).toBe('Some Name'); + expect(insertedUser.created_at).toBeDefined(); + const eventsAfter = await db('events').select('*'); + expect(eventsAfter.length).toBe(eventsBefore.length); + + // apply the rest of migrations + await migrateDb(config); + const eventsPostMigrations = await db('events').select('*'); + expect(eventsPostMigrations.length).toBe(eventsBefore.length + 1); + const userCreatedEvent = eventsPostMigrations.find( + (e) => e.type === USER_CREATED && e.data.id === insertedUser.id, + ); + expect(userCreatedEvent).toMatchObject({ + type: 'user-created', + created_at: new Date('2023-01-01T00:00:00Z'), + data: { + id: insertedUser.id, + email: 'some@getunelash.io', + name: 'Some Name', + }, + }); + }); +}); + describe('User Admin API with email configuration', () => { beforeAll(async () => { db = await dbInit('user_admin_api_serial', getLogger);