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 { + 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 { 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 { @@ -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 { 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 { 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 { @@ -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 { @@ -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 { @@ -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 { 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 { 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 { 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[], - userName: string, - userId: number, + auditUser: IAuditUser, dropBeforeImport: boolean, ): Promise { 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 { 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',