From 345c34a9456595a3219a9527e32493fc91f9957e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Gast=C3=B3n=20Fournier?= <gaston@getunleash.io>
Date: Fri, 24 May 2024 09:53:46 +0200
Subject: [PATCH] feat: add ip to state-service and group-service (#7120)

The add ip to two services. Despite state is being deprecated, I think
we better get it out of the way.
---
 .../metrics/instance/instance-service.ts      |   2 +-
 src/lib/routes/admin-api/state.ts             |   2 +-
 src/lib/server-impl.ts                        |   4 +-
 src/lib/services/group-service.ts             |  49 ++++
 src/lib/services/state-service.test.ts        |  51 ++--
 src/lib/services/state-service.ts             | 223 +++++++-----------
 src/lib/types/events.ts                       | 200 +++++++++++++++-
 src/lib/types/model.ts                        |   7 +-
 src/test/e2e/api/admin/state.e2e.test.ts      |  12 +-
 .../e2e/services/state-service.e2e.test.ts    |  14 +-
 10 files changed, 368 insertions(+), 196 deletions(-)

diff --git a/src/lib/features/metrics/instance/instance-service.ts b/src/lib/features/metrics/instance/instance-service.ts
index 7154f3987c..fca6c96ee5 100644
--- a/src/lib/features/metrics/instance/instance-service.ts
+++ b/src/lib/features/metrics/instance/instance-service.ts
@@ -115,7 +115,7 @@ export default class ClientInstanceService {
             if (appsToAnnounce.length > 0) {
                 const events = appsToAnnounce.map((app) => ({
                     type: APPLICATION_CREATED,
-                    createdBy: app.createdBy || SYSTEM_USER.username,
+                    createdBy: app.createdBy || SYSTEM_USER.username!,
                     data: app,
                     createdByUserId: app.createdByUserId || SYSTEM_USER.id,
                 }));
diff --git a/src/lib/routes/admin-api/state.ts b/src/lib/routes/admin-api/state.ts
index a0d300bef8..d42f616b0b 100644
--- a/src/lib/routes/admin-api/state.ts
+++ b/src/lib/routes/admin-api/state.ts
@@ -123,7 +123,7 @@ class StateController extends Controller {
             userName,
             dropBeforeImport: paramToBool(drop, false),
             keepExisting: paramToBool(keep, true),
-            userId: req.user.id,
+            auditUser: req.audit,
         });
         res.sendStatus(202);
     }
diff --git a/src/lib/server-impl.ts b/src/lib/server-impl.ts
index 7ac7f8edfc..e95b8cd4d8 100644
--- a/src/lib/server-impl.ts
+++ b/src/lib/server-impl.ts
@@ -19,7 +19,7 @@ import {
     type IUnleashOptions,
     type IUnleashServices,
     RoleName,
-    SYSTEM_USER,
+    SYSTEM_USER_AUDIT,
 } from './types';
 
 import User, { type IAuditUser, type IUser } from './types/user';
@@ -101,7 +101,7 @@ async function createApp(
             dropBeforeImport: config.import.dropBeforeImport,
             userName: 'import',
             keepExisting: config.import.keepExisting,
-            userId: SYSTEM_USER.id,
+            auditUser: SYSTEM_USER_AUDIT,
         });
     }
 
diff --git a/src/lib/services/group-service.ts b/src/lib/services/group-service.ts
index 0fd70cb4a6..1394cbf471 100644
--- a/src/lib/services/group-service.ts
+++ b/src/lib/services/group-service.ts
@@ -21,6 +21,8 @@ import {
     GROUP_CREATED,
     GROUP_USER_ADDED,
     GROUP_USER_REMOVED,
+    GroupUserAdded,
+    GroupUserRemoved,
     type IBaseEvent,
 } from '../types/events';
 import NameExistsError from '../error/name-exists-error';
@@ -235,6 +237,7 @@ export class GroupService {
         return this.groupStore.getProjectGroupRoles(projectId);
     }
 
+    /** @deprecated use syncExternalGroupsWithAudit */
     async syncExternalGroups(
         userId: number,
         externalGroups: string[],
@@ -286,6 +289,52 @@ export class GroupService {
         }
     }
 
+    async syncExternalGroupsWithAudit(
+        userId: number,
+        externalGroups: string[],
+        auditUser: IAuditUser,
+    ): Promise<void> {
+        if (Array.isArray(externalGroups)) {
+            const newGroups = await this.groupStore.getNewGroupsForExternalUser(
+                userId,
+                externalGroups,
+            );
+            await this.groupStore.addUserToGroups(
+                userId,
+                newGroups.map((g) => g.id),
+                auditUser.username,
+            );
+            const oldGroups = await this.groupStore.getOldGroupsForExternalUser(
+                userId,
+                externalGroups,
+            );
+            await this.groupStore.deleteUsersFromGroup(oldGroups);
+
+            const events: IBaseEvent[] = [];
+            for (const group of newGroups) {
+                events.push(
+                    new GroupUserAdded({
+                        userId,
+                        groupId: group.id,
+                        auditUser,
+                    }),
+                );
+            }
+
+            for (const group of oldGroups) {
+                events.push(
+                    new GroupUserRemoved({
+                        userId,
+                        groupId: group.groupId,
+                        auditUser,
+                    }),
+                );
+            }
+
+            await this.eventService.storeEvents(events);
+        }
+    }
+
     private mapGroupWithUsers(
         group: IGroup,
         allGroupUsers: IGroupUser[],
diff --git a/src/lib/services/state-service.test.ts b/src/lib/services/state-service.test.ts
index a803bf9bb9..3c8061c4d7 100644
--- a/src/lib/services/state-service.test.ts
+++ b/src/lib/services/state-service.test.ts
@@ -14,7 +14,7 @@ import {
 import { GLOBAL_ENV } from '../types/environment';
 import variantsExportV3 from '../../test/examples/variantsexport_v3.json';
 import EventService from '../features/events/event-service';
-import { SYSTEM_USER_ID } from '../types';
+import { SYSTEM_USER_AUDIT } from '../types';
 import { EventEmitter } from 'stream';
 const oldExportExample = require('./state-service-export-v1.json');
 const TESTUSERID = 3333;
@@ -103,7 +103,7 @@ test('should import a feature', async () => {
         ],
     };
 
-    await stateService.import({ userId: SYSTEM_USER_ID, data });
+    await stateService.import({ auditUser: SYSTEM_USER_AUDIT, data });
 
     const events = await stores.eventStore.getEvents();
     expect(events).toHaveLength(1);
@@ -130,7 +130,7 @@ test('should not import an existing feature', async () => {
     await stateService.import({
         data,
         keepExisting: true,
-        userId: SYSTEM_USER_ID,
+        auditUser: SYSTEM_USER_AUDIT,
     });
 
     const events = await stores.eventStore.getEvents();
@@ -157,7 +157,7 @@ test('should not keep existing feature if drop-before-import', async () => {
         data,
         keepExisting: true,
         dropBeforeImport: true,
-        userId: SYSTEM_USER_ID,
+        auditUser: SYSTEM_USER_AUDIT,
     });
 
     const events = await stores.eventStore.getEvents();
@@ -182,7 +182,7 @@ test('should drop feature before import if specified', async () => {
     await stateService.import({
         data,
         dropBeforeImport: true,
-        userId: SYSTEM_USER_ID,
+        auditUser: SYSTEM_USER_AUDIT,
     });
 
     const events = await stores.eventStore.getEvents();
@@ -204,7 +204,7 @@ test('should import a strategy', async () => {
         ],
     };
 
-    await stateService.import({ userId: SYSTEM_USER_ID, data });
+    await stateService.import({ auditUser: SYSTEM_USER_AUDIT, data });
 
     const events = await stores.eventStore.getEvents();
     expect(events).toHaveLength(1);
@@ -228,7 +228,7 @@ test('should not import an existing strategy', async () => {
 
     await stateService.import({
         data,
-        userId: SYSTEM_USER_ID,
+        auditUser: SYSTEM_USER_AUDIT,
         keepExisting: true,
     });
 
@@ -250,7 +250,7 @@ test('should drop strategies before import if specified', async () => {
 
     await stateService.import({
         data,
-        userId: SYSTEM_USER_ID,
+        auditUser: SYSTEM_USER_AUDIT,
         dropBeforeImport: true,
     });
 
@@ -268,7 +268,7 @@ test('should drop neither features nor strategies when neither is imported', asy
 
     await stateService.import({
         data,
-        userId: SYSTEM_USER_ID,
+        auditUser: SYSTEM_USER_AUDIT,
         dropBeforeImport: true,
     });
 
@@ -286,11 +286,11 @@ test('should not accept gibberish', async () => {
     const data2 = '{somerandomtext/';
 
     await expect(async () =>
-        stateService.import({ userId: SYSTEM_USER_ID, data: data1 }),
+        stateService.import({ auditUser: SYSTEM_USER_AUDIT, data: data1 }),
     ).rejects.toThrow();
 
     await expect(async () =>
-        stateService.import({ userId: SYSTEM_USER_ID, data: data2 }),
+        stateService.import({ auditUser: SYSTEM_USER_AUDIT, data: data2 }),
     ).rejects.toThrow();
 });
 
@@ -386,7 +386,7 @@ test('should import a tag and tag type', async () => {
         tags: [{ type: 'simple', value: 'test' }],
     };
 
-    await stateService.import({ userId: SYSTEM_USER_ID, data });
+    await stateService.import({ auditUser: SYSTEM_USER_AUDIT, data });
 
     const events = await stores.eventStore.getEvents();
     expect(events).toHaveLength(2);
@@ -423,7 +423,7 @@ test('Should not import an existing tag', async () => {
     );
     await stateService.import({
         data,
-        userId: SYSTEM_USER_ID,
+        auditUser: SYSTEM_USER_AUDIT,
         keepExisting: true,
     });
     const events = await stores.eventStore.getEvents();
@@ -460,7 +460,7 @@ test('Should not keep existing tags if drop-before-import', async () => {
     };
     await stateService.import({
         data,
-        userId: SYSTEM_USER_ID,
+        auditUser: SYSTEM_USER_AUDIT,
         dropBeforeImport: true,
     });
     const tagTypes = await stores.tagTypeStore.getAll();
@@ -570,7 +570,7 @@ test('should import a project', async () => {
         ],
     };
 
-    await stateService.import({ userId: SYSTEM_USER_ID, data });
+    await stateService.import({ auditUser: SYSTEM_USER_AUDIT, data });
 
     const events = await stores.eventStore.getEvents();
     expect(events).toHaveLength(1);
@@ -595,13 +595,13 @@ test('Should not import an existing project', async () => {
 
     await stateService.import({
         data,
-        userId: SYSTEM_USER_ID,
+        auditUser: SYSTEM_USER_AUDIT,
         keepExisting: true,
     });
     const events = await stores.eventStore.getEvents();
     expect(events).toHaveLength(0);
 
-    await stateService.import({ userId: SYSTEM_USER_ID, data });
+    await stateService.import({ auditUser: SYSTEM_USER_AUDIT, data });
 });
 
 test('Should drop projects before import if specified', async () => {
@@ -624,7 +624,7 @@ test('Should drop projects before import if specified', async () => {
     });
     await stateService.import({
         data,
-        userId: SYSTEM_USER_ID,
+        auditUser: SYSTEM_USER_AUDIT,
         dropBeforeImport: true,
     });
     const hasProject = await stores.projectStore.hasProject('fancy');
@@ -773,8 +773,7 @@ test('featureStrategies can keep existing', async () => {
     const exported = await stateService.export({});
     await stateService.import({
         data: exported,
-        userId: SYSTEM_USER_ID,
-        userName: 'testing',
+        auditUser: SYSTEM_USER_AUDIT,
         keepExisting: true,
     });
     expect(await stores.featureStrategiesStore.getAll()).toHaveLength(1);
@@ -830,8 +829,7 @@ test('featureStrategies should not keep existing if dropBeforeImport', async ()
     exported.featureStrategies = [];
     await stateService.import({
         data: exported,
-        userId: SYSTEM_USER_ID,
-        userName: 'testing',
+        auditUser: SYSTEM_USER_AUDIT,
         keepExisting: true,
         dropBeforeImport: true,
     });
@@ -842,9 +840,8 @@ test('Import v1 and exporting v2 should work', async () => {
     const { stateService } = getSetup();
     await stateService.import({
         data: oldExportExample,
-        userId: SYSTEM_USER_ID,
+        auditUser: SYSTEM_USER_AUDIT,
         dropBeforeImport: true,
-        userName: 'testing',
     });
     const exported = await stateService.export({});
     const strategiesCount = oldExportExample.features.reduce(
@@ -879,8 +876,7 @@ test('Importing states with deprecated strategies should keep their deprecated s
     };
     await stateService.import({
         data: deprecatedStrategyExample,
-        userId: SYSTEM_USER_ID,
-        userName: 'strategy-importer',
+        auditUser: SYSTEM_USER_AUDIT,
         dropBeforeImport: true,
         keepExisting: false,
     });
@@ -894,9 +890,8 @@ test('Exporting a deprecated strategy and then importing it should keep correct
     await stateService.import({
         data: variantsExportV3,
         keepExisting: false,
-        userId: SYSTEM_USER_ID,
+        auditUser: SYSTEM_USER_AUDIT,
         dropBeforeImport: true,
-        userName: 'strategy importer',
     });
     const rolloutRandom = await stores.strategyStore.get(
         'gradualRolloutRandom',
diff --git a/src/lib/services/state-service.ts b/src/lib/services/state-service.ts
index c97224dd38..c5884f83eb 100644
--- a/src/lib/services/state-service.ts
+++ b/src/lib/services/state-service.ts
@@ -1,19 +1,19 @@
 import { stateSchema } from './state-schema';
 import {
-    DROP_ENVIRONMENTS,
-    DROP_FEATURE_TAGS,
-    DROP_FEATURES,
-    DROP_PROJECTS,
-    DROP_STRATEGIES,
-    DROP_TAG_TYPES,
-    DROP_TAGS,
-    ENVIRONMENT_IMPORT,
-    FEATURE_IMPORT,
-    FEATURE_TAG_IMPORT,
-    PROJECT_IMPORT,
-    STRATEGY_IMPORT,
-    TAG_IMPORT,
-    TAG_TYPE_IMPORT,
+    DropEnvironmentsEvent,
+    DropFeaturesEvent,
+    DropFeatureTagsEvent,
+    DropProjectsEvent,
+    DropStrategiesEvent,
+    DropTagsEvent,
+    DropTagTypesEvent,
+    EnvironmentImport,
+    FeatureImport,
+    FeatureTagImport,
+    ProjectImport,
+    StrategyImport,
+    TagImport,
+    TagTypeImport,
 } from '../types/events';
 
 import { filterEqual, filterExisting, parseFile, readFile } from './state-util';
@@ -53,6 +53,7 @@ import { GLOBAL_ENV } from '../types/environment';
 import type { ISegmentStore } from '../features/segment/segment-store-type';
 import type { PartialSome } from '../types/partial';
 import type EventService from '../features/events/event-service';
+import type { IAuditUser } from '../server-impl';
 
 export interface IBackupOption {
     includeFeatureToggles: boolean;
@@ -117,8 +118,7 @@ export default class StateService {
     async importFile({
         file,
         dropBeforeImport = false,
-        userName = 'import-user',
-        userId,
+        auditUser,
         keepExisting = true,
     }: IImportFile): Promise<void> {
         return readFile(file)
@@ -126,10 +126,9 @@ export default class StateService {
             .then((data) =>
                 this.import({
                     data,
-                    userName,
+                    auditUser,
                     dropBeforeImport,
                     keepExisting,
-                    userId,
                 }),
             );
     }
@@ -169,8 +168,7 @@ export default class StateService {
 
     async import({
         data,
-        userName = 'importUser',
-        userId,
+        auditUser,
         dropBeforeImport = false,
         keepExisting = true,
     }: IImportData): Promise<void> {
@@ -186,10 +184,9 @@ export default class StateService {
         if (importData.environments) {
             importedEnvironments = await this.importEnvironments({
                 environments: data.environments,
-                userName,
                 dropBeforeImport,
                 keepExisting,
-                userId,
+                auditUser,
             });
         }
 
@@ -197,10 +194,9 @@ export default class StateService {
             await this.importProjects({
                 projects: data.projects,
                 importedEnvironments,
-                userName,
                 dropBeforeImport,
                 keepExisting,
-                userId,
+                auditUser,
             });
         }
 
@@ -217,11 +213,10 @@ export default class StateService {
 
             await this.importFeatures({
                 features,
-                userName,
                 dropBeforeImport,
                 keepExisting,
                 featureEnvironments,
-                userId,
+                auditUser,
             });
 
             if (featureEnvironments) {
@@ -240,10 +235,9 @@ export default class StateService {
         if (importData.strategies) {
             await this.importStrategies({
                 strategies: data.strategies,
-                userName,
                 dropBeforeImport,
                 keepExisting,
-                userId,
+                auditUser,
             });
         }
 
@@ -263,18 +257,16 @@ export default class StateService {
                             tagValue: t.tagValue || t.value,
                             tagType: t.tagType || t.type,
                         })) || [],
-                userName,
                 dropBeforeImport,
                 keepExisting,
-                userId,
+                auditUser,
             });
         }
 
         if (importData.segments) {
             await this.importSegments(
                 data.segments,
-                userName,
-                userId,
+                auditUser,
                 dropBeforeImport,
             );
         }
@@ -365,14 +357,12 @@ export default class StateService {
         };
     }
 
-    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
     async importFeatures({
         features,
-        userName,
-        userId,
         dropBeforeImport,
         keepExisting,
         featureEnvironments,
+        auditUser,
     }): Promise<void> {
         this.logger.info(`Importing ${features.length} feature flags`);
         const oldToggles = dropBeforeImport
@@ -382,12 +372,9 @@ export default class StateService {
         if (dropBeforeImport) {
             this.logger.info('Dropping existing feature flags');
             await this.toggleStore.deleteAll();
-            await this.eventService.storeEvent({
-                type: DROP_FEATURES,
-                createdBy: userName,
-                createdByUserId: userId,
-                data: { name: 'all-features' },
-            });
+            await this.eventService.storeEvent(
+                new DropFeaturesEvent({ auditUser }),
+            );
         }
 
         await Promise.all(
@@ -396,7 +383,7 @@ export default class StateService {
                 .filter(filterEqual(oldToggles))
                 .map(async (feature) => {
                     await this.toggleStore.create(feature.project, {
-                        createdByUserId: userId,
+                        createdByUserId: auditUser.id,
                         ...feature,
                     });
                     await this.featureEnvironmentStore.connectFeatureToEnvironmentsForProject(
@@ -404,12 +391,12 @@ export default class StateService {
                         feature.project,
                         this.enabledIn(feature.name, featureEnvironments),
                     );
-                    await this.eventService.storeEvent({
-                        type: FEATURE_IMPORT,
-                        createdByUserId: userId,
-                        createdBy: userName,
-                        data: feature,
-                    });
+                    await this.eventService.storeEvent(
+                        new FeatureImport({
+                            feature,
+                            auditUser,
+                        }),
+                    );
                 }),
         );
     }
@@ -417,10 +404,9 @@ export default class StateService {
     // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
     async importStrategies({
         strategies,
-        userName,
-        userId,
         dropBeforeImport,
         keepExisting,
+        auditUser,
     }): Promise<void> {
         this.logger.info(`Importing ${strategies.length} strategies`);
         const oldStrategies = dropBeforeImport
@@ -430,12 +416,9 @@ export default class StateService {
         if (dropBeforeImport) {
             this.logger.info('Dropping existing strategies');
             await this.strategyStore.dropCustomStrategies();
-            await this.eventService.storeEvent({
-                type: DROP_STRATEGIES,
-                createdBy: userName,
-                createdByUserId: userId,
-                data: { name: 'all-strategies' },
-            });
+            await this.eventService.storeEvent(
+                new DropStrategiesEvent({ auditUser }),
+            );
         }
 
         await Promise.all(
@@ -444,12 +427,9 @@ export default class StateService {
                 .filter(filterEqual(oldStrategies))
                 .map((strategy) =>
                     this.strategyStore.importStrategy(strategy).then(() => {
-                        this.eventService.storeEvent({
-                            type: STRATEGY_IMPORT,
-                            createdBy: userName,
-                            createdByUserId: userId,
-                            data: strategy,
-                        });
+                        this.eventService.storeEvent(
+                            new StrategyImport({ strategy, auditUser }),
+                        );
                     }),
                 ),
         );
@@ -458,8 +438,7 @@ export default class StateService {
     // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
     async importEnvironments({
         environments,
-        userName,
-        userId,
+        auditUser,
         dropBeforeImport,
         keepExisting,
     }): Promise<IEnvironment[]> {
@@ -470,12 +449,9 @@ export default class StateService {
         if (dropBeforeImport) {
             this.logger.info('Dropping existing environments');
             await this.environmentStore.deleteAll();
-            await this.eventService.storeEvent({
-                type: DROP_ENVIRONMENTS,
-                createdBy: userName,
-                createdByUserId: userId,
-                data: { name: 'all-environments' },
-            });
+            await this.eventService.storeEvent(
+                new DropEnvironmentsEvent({ auditUser }),
+            );
         }
         const envsImport = environments.filter((env) =>
             keepExisting ? !oldEnvs.some((old) => old.name === env.name) : true,
@@ -484,12 +460,9 @@ export default class StateService {
         if (envsImport.length > 0) {
             importedEnvs =
                 await this.environmentStore.importEnvironments(envsImport);
-            const importedEnvironmentEvents = importedEnvs.map((env) => ({
-                type: ENVIRONMENT_IMPORT,
-                createdBy: userName,
-                createdByUserId: userId,
-                data: env,
-            }));
+            const importedEnvironmentEvents = importedEnvs.map(
+                (env) => new EnvironmentImport({ auditUser, env }),
+            );
             await this.eventService.storeEvents(importedEnvironmentEvents);
         }
         return importedEnvs;
@@ -499,8 +472,7 @@ export default class StateService {
     async importProjects({
         projects,
         importedEnvironments,
-        userName,
-        userId,
+        auditUser,
         dropBeforeImport,
         keepExisting,
     }): Promise<void> {
@@ -511,12 +483,9 @@ export default class StateService {
         if (dropBeforeImport) {
             this.logger.info('Dropping existing projects');
             await this.projectStore.deleteAll();
-            await this.eventService.storeEvent({
-                type: DROP_PROJECTS,
-                createdBy: userName,
-                createdByUserId: userId,
-                data: { name: 'all-projects' },
-            });
+            await this.eventService.storeEvent(
+                new DropProjectsEvent({ auditUser }),
+            );
         }
         const projectsToImport = projects.filter((project) =>
             keepExisting
@@ -528,12 +497,9 @@ export default class StateService {
                 projectsToImport,
                 importedEnvironments,
             );
-            const importedProjectEvents = importedProjects.map((project) => ({
-                type: PROJECT_IMPORT,
-                createdBy: userName,
-                createdByUserId: userId,
-                data: project,
-            }));
+            const importedProjectEvents = importedProjects.map(
+                (project) => new ProjectImport({ project, auditUser }),
+            );
             await this.eventService.storeEvents(importedProjectEvents);
         }
     }
@@ -543,8 +509,7 @@ export default class StateService {
         tagTypes,
         tags,
         featureTags,
-        userName,
-        userId,
+        auditUser,
         dropBeforeImport,
         keepExisting,
     }): Promise<void> {
@@ -566,40 +531,23 @@ export default class StateService {
             await this.tagStore.deleteAll();
             await this.tagTypeStore.deleteAll();
             await this.eventService.storeEvents([
-                {
-                    type: DROP_FEATURE_TAGS,
-                    createdBy: userName,
-                    createdByUserId: userId,
-                    data: { name: 'all-feature-tags' },
-                },
-                {
-                    type: DROP_TAGS,
-                    createdBy: userName,
-                    createdByUserId: userId,
-                    data: { name: 'all-tags' },
-                },
-                {
-                    type: DROP_TAG_TYPES,
-                    createdBy: userName,
-                    createdByUserId: userId,
-                    data: { name: 'all-tag-types' },
-                },
+                new DropFeatureTagsEvent({ auditUser }),
+                new DropTagsEvent({ auditUser }),
+                new DropTagTypesEvent({ auditUser }),
             ]);
         }
         await this.importTagTypes(
             tagTypes,
             keepExisting,
             oldTagTypes,
-            userName,
-            userId,
+            auditUser,
         );
-        await this.importTags(tags, keepExisting, oldTags, userName, userId);
+        await this.importTags(tags, keepExisting, oldTags, auditUser);
         await this.importFeatureTags(
             featureTags,
             keepExisting,
             oldFeatureTags,
-            userName,
-            userId,
+            auditUser,
         );
     }
 
@@ -615,8 +563,7 @@ export default class StateService {
         featureTags: IFeatureTag[],
         keepExisting: boolean,
         oldFeatureTags: IFeatureTag[],
-        userName: string,
-        userId: number,
+        auditUser: IAuditUser,
     ): Promise<void> {
         const featureTagsToInsert = featureTags
             .filter((tag) =>
@@ -627,18 +574,15 @@ export default class StateService {
                     : true,
             )
             .map((tag) => ({
-                createdByUserId: userId,
+                createdByUserId: auditUser.id,
                 ...tag,
             }));
         if (featureTagsToInsert.length > 0) {
             const importedFeatureTags =
                 await this.featureTagStore.tagFeatures(featureTagsToInsert);
-            const importedFeatureTagEvents = importedFeatureTags.map((tag) => ({
-                type: FEATURE_TAG_IMPORT,
-                createdBy: userName,
-                createdByUserId: userId,
-                data: tag,
-            }));
+            const importedFeatureTagEvents = importedFeatureTags.map(
+                (featureTag) => new FeatureTagImport({ featureTag, auditUser }),
+            );
             await this.eventService.storeEvents(importedFeatureTagEvents);
         }
     }
@@ -650,8 +594,7 @@ export default class StateService {
         tags: ITag[],
         keepExisting: boolean,
         oldTags: ITag[],
-        userName: string,
-        userId: number,
+        auditUser: IAuditUser,
     ): Promise<void> {
         const tagsToInsert = tags.filter((tag) =>
             keepExisting
@@ -660,12 +603,9 @@ export default class StateService {
         );
         if (tagsToInsert.length > 0) {
             const importedTags = await this.tagStore.bulkImport(tagsToInsert);
-            const importedTagEvents = importedTags.map((tag) => ({
-                type: TAG_IMPORT,
-                createdBy: userName,
-                createdByUserId: userId,
-                data: tag,
-            }));
+            const importedTagEvents = importedTags.map(
+                (tag) => new TagImport({ tag, auditUser }),
+            );
             await this.eventService.storeEvents(importedTagEvents);
         }
     }
@@ -674,8 +614,7 @@ export default class StateService {
         tagTypes: ITagType[],
         keepExisting: boolean,
         oldTagTypes: ITagType[],
-        userName: string,
-        userId: number,
+        auditUser: IAuditUser,
     ): Promise<void> {
         const tagTypesToInsert = tagTypes.filter((tagType) =>
             keepExisting
@@ -685,20 +624,16 @@ export default class StateService {
         if (tagTypesToInsert.length > 0) {
             const importedTagTypes =
                 await this.tagTypeStore.bulkImport(tagTypesToInsert);
-            const importedTagTypeEvents = importedTagTypes.map((tagType) => ({
-                type: TAG_TYPE_IMPORT,
-                createdBy: userName,
-                createdByUserId: userId,
-                data: tagType,
-            }));
+            const importedTagTypeEvents = importedTagTypes.map(
+                (tagType) => new TagTypeImport({ tagType, auditUser }),
+            );
             await this.eventService.storeEvents(importedTagTypeEvents);
         }
     }
 
     async importSegments(
         segments: PartialSome<ISegment, 'id'>[],
-        userName: string,
-        userId: number,
+        auditUser: IAuditUser,
         dropBeforeImport: boolean,
     ): Promise<void> {
         if (dropBeforeImport) {
@@ -707,7 +642,9 @@ export default class StateService {
 
         await Promise.all(
             segments.map((segment) =>
-                this.segmentStore.create(segment, { username: userName }),
+                this.segmentStore.create(segment, {
+                    username: auditUser.username,
+                }),
             ),
         );
     }
diff --git a/src/lib/types/events.ts b/src/lib/types/events.ts
index 296e9b20fa..f6591a0b61 100644
--- a/src/lib/types/events.ts
+++ b/src/lib/types/events.ts
@@ -1,7 +1,16 @@
-import type { FeatureToggle, IStrategyConfig, ITag, IVariant } from './model';
+import type {
+    FeatureToggle,
+    IEnvironment,
+    IProject,
+    IStrategyConfig,
+    ITag,
+    IVariant,
+} from './model';
 import type { IApiToken } from './models/api-token';
 import type { IAuditUser, IUserWithRootRole } from './user';
 import type { FeatureLifecycleCompletedSchema } from '../openapi';
+import type { ITagType } from '../features/tag-type/tag-type-store-type';
+import type { IFeatureAndTag } from './stores/feature-tag-store';
 
 export const APPLICATION_CREATED = 'application-created' as const;
 
@@ -356,7 +365,8 @@ export interface IBaseEvent {
     tags?: ITag[];
 }
 
-export interface IEvent extends IBaseEvent {
+// This represents the read model for events
+export interface IEvent extends Omit<IBaseEvent, 'ip'> {
     id: number;
     createdAt: Date;
 }
@@ -366,7 +376,7 @@ export interface IEventList {
     events: IEvent[];
 }
 
-class BaseEvent implements IBaseEvent {
+export class BaseEvent implements IBaseEvent {
     readonly type: IEventType;
 
     readonly createdBy: string;
@@ -633,6 +643,83 @@ export class FeatureCreatedEvent extends BaseEvent {
     }
 }
 
+export class ProjectImport extends BaseEvent {
+    readonly data: IProject;
+    constructor(p: {
+        project: IProject;
+        auditUser: IAuditUser;
+    }) {
+        super(PROJECT_IMPORT, p.auditUser);
+        this.data = p.project;
+    }
+}
+
+export class FeatureImport extends BaseEvent {
+    readonly data: any;
+    constructor(p: {
+        feature: any;
+        auditUser: IAuditUser;
+    }) {
+        super(FEATURE_IMPORT, p.auditUser);
+        this.data = p.feature;
+    }
+}
+
+export class StrategyImport extends BaseEvent {
+    readonly data: any;
+    constructor(p: {
+        strategy: any;
+        auditUser: IAuditUser;
+    }) {
+        super(STRATEGY_IMPORT, p.auditUser);
+        this.data = p.strategy;
+    }
+}
+
+export class EnvironmentImport extends BaseEvent {
+    readonly data: IEnvironment;
+    constructor(p: {
+        env: IEnvironment;
+        auditUser: IAuditUser;
+    }) {
+        super(ENVIRONMENT_IMPORT, p.auditUser);
+        this.data = p.env;
+    }
+}
+
+export class TagTypeImport extends BaseEvent {
+    readonly data: ITagType;
+    constructor(p: {
+        tagType: ITagType;
+        auditUser: IAuditUser;
+    }) {
+        super(TAG_TYPE_IMPORT, p.auditUser);
+        this.data = p.tagType;
+    }
+}
+
+export class TagImport extends BaseEvent {
+    readonly data: ITag;
+    constructor(p: {
+        tag: ITag;
+        auditUser: IAuditUser;
+    }) {
+        super(TAG_IMPORT, p.auditUser);
+        this.data = p.tag;
+    }
+}
+
+export class FeatureTagImport extends BaseEvent {
+    readonly data: IFeatureAndTag;
+    constructor(p: {
+        featureTag: IFeatureAndTag;
+        auditUser: IAuditUser;
+    }) {
+        super(FEATURE_TAG_IMPORT, p.auditUser);
+        this.data = p.featureTag;
+    }
+}
+
 export class FeatureCompletedEvent extends BaseEvent {
     readonly featureName: string;
     readonly data: FeatureLifecycleCompletedSchema;
@@ -1202,6 +1289,36 @@ export class ProjectAccessGroupRolesUpdated extends BaseEvent {
     }
 }
 
+export class GroupUserRemoved extends BaseEvent {
+    readonly preData: any;
+    constructor(p: {
+        userId: number;
+        groupId: number;
+        auditUser: IAuditUser;
+    }) {
+        super(GROUP_USER_REMOVED, p.auditUser);
+        this.preData = {
+            groupId: p.groupId,
+            userId: p.userId,
+        };
+    }
+}
+
+export class GroupUserAdded extends BaseEvent {
+    readonly data: any;
+    constructor(p: {
+        userId: number;
+        groupId: number;
+        auditUser: IAuditUser;
+    }) {
+        super(GROUP_USER_ADDED, p.auditUser);
+        this.data = {
+            groupId: p.groupId,
+            userId: p.userId,
+        };
+    }
+}
+
 export class ProjectAccessUserRolesDeleted extends BaseEvent {
     readonly project: string;
 
@@ -1558,6 +1675,83 @@ export class FeaturesExportedEvent extends BaseEvent {
     }
 }
 
+export class DropProjectsEvent extends BaseEvent {
+    readonly data: any;
+
+    constructor(eventData: {
+        auditUser: IAuditUser;
+    }) {
+        super(DROP_PROJECTS, eventData.auditUser);
+        this.data = { name: 'all-projects' };
+    }
+}
+
+export class DropFeaturesEvent extends BaseEvent {
+    readonly data: any;
+
+    constructor(eventData: {
+        auditUser: IAuditUser;
+    }) {
+        super(DROP_FEATURES, eventData.auditUser);
+        this.data = { name: 'all-features' };
+    }
+}
+
+export class DropStrategiesEvent extends BaseEvent {
+    readonly data: any;
+
+    constructor(eventData: {
+        auditUser: IAuditUser;
+    }) {
+        super(DROP_STRATEGIES, eventData.auditUser);
+        this.data = { name: 'all-strategies' };
+    }
+}
+
+export class DropEnvironmentsEvent extends BaseEvent {
+    readonly data: any;
+
+    constructor(eventData: {
+        auditUser: IAuditUser;
+    }) {
+        super(DROP_ENVIRONMENTS, eventData.auditUser);
+        this.data = { name: 'all-environments' };
+    }
+}
+
+export class DropFeatureTagsEvent extends BaseEvent {
+    readonly data: any;
+
+    constructor(eventData: {
+        auditUser: IAuditUser;
+    }) {
+        super(DROP_FEATURE_TAGS, eventData.auditUser);
+        this.data = { name: 'all-feature-tags' };
+    }
+}
+
+export class DropTagsEvent extends BaseEvent {
+    readonly data: any;
+
+    constructor(eventData: {
+        auditUser: IAuditUser;
+    }) {
+        super(DROP_TAGS, eventData.auditUser);
+        this.data = { name: 'all-tags' };
+    }
+}
+
+export class DropTagTypesEvent extends BaseEvent {
+    readonly data: any;
+
+    constructor(eventData: {
+        auditUser: IAuditUser;
+    }) {
+        super(DROP_TAG_TYPES, eventData.auditUser);
+        this.data = { name: 'all-tag-types' };
+    }
+}
+
 export class RoleCreatedEvent extends BaseEvent {
     readonly data: any;
 
diff --git a/src/lib/types/model.ts b/src/lib/types/model.ts
index 7fcf38e636..20461b1fd5 100644
--- a/src/lib/types/model.ts
+++ b/src/lib/types/model.ts
@@ -1,7 +1,7 @@
 import type { ITagType } from '../features/tag-type/tag-type-store-type';
 import type { LogProvider } from '../logger';
 import type { IRole } from './stores/access-store';
-import type { IUser } from './user';
+import type { IAuditUser, IUser } from './user';
 import type { ALL_OPERATORS } from '../util';
 import type { IProjectStats } from '../features/project/project-service';
 import type { CreateFeatureStrategySchema } from '../openapi';
@@ -479,8 +479,11 @@ export interface IImportFile extends ImportCommon {
 interface ImportCommon {
     dropBeforeImport?: boolean;
     keepExisting?: boolean;
+    /** @deprecated should use auditUser instead */
     userName?: string;
-    userId: number;
+    /** @deprecated should use auditUser instead */
+    userId?: number;
+    auditUser: IAuditUser;
 }
 
 export interface IImportData extends ImportCommon {
diff --git a/src/test/e2e/api/admin/state.e2e.test.ts b/src/test/e2e/api/admin/state.e2e.test.ts
index 56e04e754e..d84a8d90d2 100644
--- a/src/test/e2e/api/admin/state.e2e.test.ts
+++ b/src/test/e2e/api/admin/state.e2e.test.ts
@@ -206,8 +206,7 @@ test('Can roundtrip. I.e. export and then import', async () => {
         data,
         dropBeforeImport: true,
         keepExisting: false,
-        userName: 'export-tester',
-        userId: -9999,
+        auditUser: SYSTEM_USER_AUDIT,
     });
 });
 
@@ -270,8 +269,7 @@ test('Roundtrip with tags works', async () => {
         data,
         dropBeforeImport: true,
         keepExisting: false,
-        userName: 'export-tester',
-        userId: -9999,
+        auditUser: SYSTEM_USER_AUDIT,
     });
 
     const f = await app.services.featureTagService.listTags(featureName);
@@ -341,8 +339,7 @@ test('Roundtrip with strategies in multiple environments works', async () => {
         data,
         dropBeforeImport: true,
         keepExisting: false,
-        userName: 'export-tester',
-        userId: -9999,
+        auditUser: SYSTEM_USER_AUDIT,
     });
     const f = await app.services.featureToggleServiceV2.getFeature({
         featureName,
@@ -423,8 +420,7 @@ test(`should not delete api_tokens on import when drop-flag is set`, async () =>
         data,
         dropBeforeImport: true,
         keepExisting: false,
-        userName: userName,
-        userId: -9999,
+        auditUser: SYSTEM_USER_AUDIT,
     });
 
     const apiTokens = await app.services.apiTokenService.getAllTokens();
diff --git a/src/test/e2e/services/state-service.e2e.test.ts b/src/test/e2e/services/state-service.e2e.test.ts
index 632a8dd217..c683e5e15a 100644
--- a/src/test/e2e/services/state-service.e2e.test.ts
+++ b/src/test/e2e/services/state-service.e2e.test.ts
@@ -4,7 +4,7 @@ import StateService from '../../../lib/services/state-service';
 import oldFormat from '../../examples/variantsexport_v3.json';
 import { WeightType } from '../../../lib/types/model';
 import { EventService } from '../../../lib/services';
-import type { IUnleashStores } from '../../../lib/types';
+import { SYSTEM_USER_AUDIT, type IUnleashStores } from '../../../lib/types';
 
 let stores: IUnleashStores;
 let db: ITestDb;
@@ -143,7 +143,7 @@ test('Should import variants from old format and convert to new format (per envi
         data: oldFormat,
         keepExisting: false,
         dropBeforeImport: true,
-        userId: -9999,
+        auditUser: SYSTEM_USER_AUDIT,
     });
     const featureEnvironments = await stores.featureEnvironmentStore.getAll();
     expect(featureEnvironments).toHaveLength(6); // There are 3 environments enabled and 2 features
@@ -158,14 +158,14 @@ test('Should import variants in new format (per environment)', async () => {
         data: oldFormat,
         keepExisting: false,
         dropBeforeImport: true,
-        userId: -9999,
+        auditUser: SYSTEM_USER_AUDIT,
     });
     const exportedJson = await stateService.export({});
     await stateService.import({
         data: exportedJson,
         keepExisting: false,
         dropBeforeImport: true,
-        userId: -9999,
+        auditUser: SYSTEM_USER_AUDIT,
     });
     const featureEnvironments = await stores.featureEnvironmentStore.getAll();
     expect(featureEnvironments).toHaveLength(6); // 3 environments, 2 features === 6 rows
@@ -190,10 +190,9 @@ test('Importing states with deprecated strategies should keep their deprecated s
     };
     await stateService.import({
         data: deprecatedStrategyExample,
-        userName: 'strategy-importer',
         dropBeforeImport: true,
         keepExisting: false,
-        userId: -9999,
+        auditUser: SYSTEM_USER_AUDIT,
     });
     const deprecatedStrategy =
         await stores.strategyStore.get('deprecatedstrat');
@@ -205,8 +204,7 @@ test('Exporting a deprecated strategy and then importing it should keep correct
         data: oldFormat,
         keepExisting: false,
         dropBeforeImport: true,
-        userName: 'strategy importer',
-        userId: -9999,
+        auditUser: SYSTEM_USER_AUDIT,
     });
     const rolloutRandom = await stores.strategyStore.get(
         'gradualRolloutRandom',