From 7241ef9001ce3b489cc17fdac6dba28127d84dfc Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Tue, 12 Dec 2023 15:02:24 +0100 Subject: [PATCH] another checkpoint, backend builds. tests need to wait for PR --- src/lib/addons/slack-app.test.ts | 7 +- src/lib/addons/slack.test.ts | 2 +- src/lib/addons/teams.test.ts | 2 +- src/lib/addons/webhook.test.ts | 3 +- src/lib/db/event-store.ts | 1 + .../export-import-service.ts | 3 + .../export-import.e2e.test.ts | 2 + .../feature-toggle-controller.ts | 6 + .../feature-toggle-legacy-controller.ts | 62 ++++++-- .../tests/feature-toggle-service.e2e.test.ts | 53 +++++-- .../tests/feature-toggles.auth.e2e.test.ts | 1 + .../maintenance/maintenance-controller.ts | 1 + .../maintenance/maintenance-service.test.ts | 1 + .../maintenance/maintenance-service.ts | 2 + .../environment-service.test.ts | 63 ++++++-- .../environment-service.ts | 18 ++- .../project-environments/environments.ts | 3 + .../scheduler/scheduler-service.test.ts | 1 + src/lib/features/tag-type/tag-type-service.ts | 18 ++- src/lib/features/tag-type/tag-type.ts | 10 +- .../middleware/cors-origin-middleware.test.ts | 7 + src/lib/routes/admin-api/addon.ts | 15 +- src/lib/routes/admin-api/api-token.ts | 7 +- src/lib/routes/admin-api/config.ts | 1 + src/lib/routes/admin-api/context.ts | 8 +- src/lib/routes/admin-api/events.test.ts | 7 +- src/lib/routes/admin-api/project/api-token.ts | 6 +- .../admin-api/project/project-archive.ts | 8 +- src/lib/routes/admin-api/project/variants.ts | 1 + src/lib/routes/admin-api/public-signup.ts | 2 + src/lib/routes/admin-api/state.ts | 1 + src/lib/routes/admin-api/strategy.ts | 20 ++- src/lib/routes/admin-api/tag.ts | 8 +- src/lib/server-impl.ts | 2 + src/lib/services/access-service.test.ts | 6 +- src/lib/services/access-service.ts | 14 +- src/lib/services/addon-service.test.ts | 144 ++++++++++++------ src/lib/services/addon-service.ts | 19 ++- src/lib/services/api-token-service.test.ts | 9 +- src/lib/services/api-token-service.ts | 45 +++++- .../client-metrics/instance-service.ts | 11 +- .../tests/last-seen-service.e2e.test.ts | 2 + src/lib/services/context-service.ts | 11 +- src/lib/services/favorites-service.ts | 4 + src/lib/services/feature-tag-service.ts | 20 ++- src/lib/services/feature-type-service.ts | 1 + src/lib/services/group-service.ts | 65 +++++--- src/lib/services/pat-service.ts | 2 + src/lib/services/project-service.ts | 32 +++- src/lib/services/proxy-service.ts | 2 + .../services/public-signup-token-service.ts | 9 +- src/lib/services/segment-service.ts | 9 +- src/lib/services/setting-service.ts | 10 +- src/lib/services/state-service.test.ts | 75 +++++++-- src/lib/services/state-service.ts | 38 ++++- src/lib/services/strategy-service.ts | 12 +- src/lib/services/tag-service.ts | 10 +- src/lib/types/core.ts | 13 +- src/lib/types/events.ts | 6 +- src/lib/types/model.ts | 1 + .../types/stores/client-applications-store.ts | 1 + .../e2e/api/admin/api-token.auth.e2e.test.ts | 2 + src/test/e2e/api/admin/config.e2e.test.ts | 1 + src/test/e2e/api/admin/event.e2e.test.ts | 6 +- src/test/e2e/api/admin/state.e2e.test.ts | 24 ++- src/test/e2e/api/client/feature.e2e.test.ts | 14 +- .../client/feature.env.disabled.e2e.test.ts | 5 +- .../api/client/feature.optimal304.e2e.test.ts | 11 +- .../client/feature.token.access.e2e.test.ts | 19 ++- src/test/e2e/api/client/metricsV2.e2e.test.ts | 4 +- src/test/e2e/api/proxy/proxy.e2e.test.ts | 8 +- .../e2e/services/access-service.e2e.test.ts | 15 +- .../e2e/services/addon-service.e2e.test.ts | 7 +- .../services/api-token-service.e2e.test.ts | 2 +- .../e2e/services/group-service.e2e.test.ts | 3 + .../e2e/services/project-service.e2e.test.ts | 70 ++++++++- src/test/e2e/services/setting-service.test.ts | 33 +++- .../e2e/services/state-service.e2e.test.ts | 7 +- .../e2e/services/user-service.e2e.test.ts | 1 + src/test/e2e/stores/event-store.e2e.test.ts | 13 ++ 80 files changed, 954 insertions(+), 214 deletions(-) diff --git a/src/lib/addons/slack-app.test.ts b/src/lib/addons/slack-app.test.ts index 2994769c0198..4c496464836d 100644 --- a/src/lib/addons/slack-app.test.ts +++ b/src/lib/addons/slack-app.test.ts @@ -1,10 +1,7 @@ -import { - IEvent, - FEATURE_ENVIRONMENT_ENABLED, - SYSTEM_USER_ID, -} from '../types/events'; +import { IEvent, FEATURE_ENVIRONMENT_ENABLED } from '../types/events'; import SlackAppAddon from './slack-app'; import { ChatPostMessageArguments, ErrorCode } from '@slack/web-api'; +import { SYSTEM_USER_ID } from '../types'; const slackApiCalls: ChatPostMessageArguments[] = []; diff --git a/src/lib/addons/slack.test.ts b/src/lib/addons/slack.test.ts index ef4f86d35081..06d03de2e9ca 100644 --- a/src/lib/addons/slack.test.ts +++ b/src/lib/addons/slack.test.ts @@ -3,13 +3,13 @@ import { FEATURE_ARCHIVED, FEATURE_ENVIRONMENT_DISABLED, IEvent, - SYSTEM_USER_ID, } from '../types/events'; import { Logger } from '../logger'; import SlackAddon from './slack'; import noLogger from '../../test/fixtures/no-logger'; +import { SYSTEM_USER_ID } from '../types'; let fetchRetryCalls: any[] = []; diff --git a/src/lib/addons/teams.test.ts b/src/lib/addons/teams.test.ts index fd3bacbaef21..8bbd239e40d1 100644 --- a/src/lib/addons/teams.test.ts +++ b/src/lib/addons/teams.test.ts @@ -5,12 +5,12 @@ import { FEATURE_CREATED, FEATURE_ENVIRONMENT_DISABLED, IEvent, - SYSTEM_USER_ID, } from '../types/events'; import TeamsAddon from './teams'; import noLogger from '../../test/fixtures/no-logger'; +import { SYSTEM_USER_ID } from '../types'; let fetchRetryCalls: any[]; diff --git a/src/lib/addons/webhook.test.ts b/src/lib/addons/webhook.test.ts index b91adec1ab82..95d281319da2 100644 --- a/src/lib/addons/webhook.test.ts +++ b/src/lib/addons/webhook.test.ts @@ -1,10 +1,11 @@ import { Logger } from '../logger'; -import { FEATURE_CREATED, IEvent, SYSTEM_USER_ID } from '../types/events'; +import { FEATURE_CREATED, IEvent } from '../types/events'; import WebhookAddon from './webhook'; import noLogger from '../../test/fixtures/no-logger'; +import { SYSTEM_USER_ID } from '../types'; let fetchRetryCalls: any[] = []; diff --git a/src/lib/db/event-store.ts b/src/lib/db/event-store.ts index ef4bb1bb9069..dafe9a4d2d16 100644 --- a/src/lib/db/event-store.ts +++ b/src/lib/db/event-store.ts @@ -379,6 +379,7 @@ class EventStore implements IEventStore { return { type: e.type, created_by: e.createdBy ?? 'admin', + created_by_user_id: e.createdByUserId, data: Array.isArray(e.data) ? JSON.stringify(e.data) : e.data, pre_data: Array.isArray(e.preData) ? JSON.stringify(e.preData) diff --git a/src/lib/features/export-import-toggles/export-import-service.ts b/src/lib/features/export-import-toggles/export-import-service.ts index a855089059d9..b8e74c95cc0a 100644 --- a/src/lib/features/export-import-toggles/export-import-service.ts +++ b/src/lib/features/export-import-toggles/export-import-service.ts @@ -389,6 +389,7 @@ export default class ExportImportService value: tag.tagValue, }, extractUsernameFromUser(user), + user.id, ); } } @@ -406,6 +407,7 @@ export default class ExportImportService stickiness: contextField.stickiness, }, extractUsernameFromUser(user), + user.id, ), ), ); @@ -419,6 +421,7 @@ export default class ExportImportService ? this.tagTypeService.createTagType( tagType, extractUsernameFromUser(user), + user.id, ) : Promise.resolve(); }), diff --git a/src/lib/features/export-import-toggles/export-import.e2e.test.ts b/src/lib/features/export-import-toggles/export-import.e2e.test.ts index 21abaeed8e22..f5001a6ad4b2 100644 --- a/src/lib/features/export-import-toggles/export-import.e2e.test.ts +++ b/src/lib/features/export-import-toggles/export-import.e2e.test.ts @@ -62,6 +62,7 @@ const createToggle = async ( tags: string[] = [], projectId: string = 'default', username: string = 'test', + userId: number = -9999, ) => { await app.services.featureToggleServiceV2.createFeatureToggle( projectId, @@ -89,6 +90,7 @@ const createToggle = async ( value: tag, }, username, + userId, ); }), ); diff --git a/src/lib/features/feature-toggle/feature-toggle-controller.ts b/src/lib/features/feature-toggle/feature-toggle-controller.ts index 5801ebbb8d9e..e6860fd2727e 100644 --- a/src/lib/features/feature-toggle/feature-toggle-controller.ts +++ b/src/lib/features/feature-toggle/feature-toggle-controller.ts @@ -685,6 +685,7 @@ export default class ProjectFeaturesController extends Controller { description: req.body.description || undefined, }, userName, + req.user.id, ); this.openApiService.respondWithValidation( @@ -734,6 +735,7 @@ export default class ProjectFeaturesController extends Controller { }, userName, featureName, + req.user.id, ); this.openApiService.respondWithValidation( @@ -759,6 +761,7 @@ export default class ProjectFeaturesController extends Controller { featureName, extractUsername(req), req.body, + req.user.id, ); this.openApiService.respondWithValidation( 200, @@ -801,6 +804,7 @@ export default class ProjectFeaturesController extends Controller { stale, userName, projectId, + req.user.id, ); res.status(202).end(); } @@ -1109,6 +1113,7 @@ export default class ProjectFeaturesController extends Controller { value, { environment, projectId, featureName }, userName, + req.user.id, ); res.status(200).json(updatedStrategy); } @@ -1124,6 +1129,7 @@ export default class ProjectFeaturesController extends Controller { tags.addedTags, tags.removedTags, userName, + req.user.id, ); res.status(200).end(); } diff --git a/src/lib/features/feature-toggle/legacy/feature-toggle-legacy-controller.ts b/src/lib/features/feature-toggle/legacy/feature-toggle-legacy-controller.ts index 8caffa6f90cf..472c9159b9e4 100644 --- a/src/lib/features/feature-toggle/legacy/feature-toggle-legacy-controller.ts +++ b/src/lib/features/feature-toggle/legacy/feature-toggle-legacy-controller.ts @@ -260,6 +260,7 @@ class FeatureController extends Controller { featureName, req.body, userName, + req.user.id, ); res.status(201).header('location', `${featureName}/tags`).json(tag); } @@ -279,13 +280,23 @@ class FeatureController extends Controller { await Promise.all( addedTags.map((addedTag) => - this.tagService.addTag(featureName, addedTag, userName), + this.tagService.addTag( + featureName, + addedTag, + userName, + req.user.id, + ), ), ); await Promise.all( removedTags.map((removedTag) => - this.tagService.removeTag(featureName, removedTag, userName), + this.tagService.removeTag( + featureName, + removedTag, + userName, + req.user.id, + ), ), ); @@ -300,7 +311,12 @@ class FeatureController extends Controller { ): Promise { const { featureName, type, value } = req.params; const userName = extractUsername(req); - await this.tagService.removeTag(featureName, { type, value }, userName); + await this.tagService.removeTag( + featureName, + { type, value }, + userName, + req.user.id, + ); res.status(200).end(); } @@ -328,6 +344,7 @@ class FeatureController extends Controller { project, validatedToggle, userName, + req.user.id, true, ); const strategies = await Promise.all( @@ -351,7 +368,13 @@ class FeatureController extends Controller { enabled, userName, ); - await this.service.saveVariants(name, project, variants, userName); + await this.service.saveVariants( + name, + project, + variants, + userName, + req.user.id, + ); res.status(201).json({ ...createdFeature, @@ -376,6 +399,7 @@ class FeatureController extends Controller { value, userName, featureName, + req.user.id, ); await this.service.removeAllStrategiesForEnv(featureName); @@ -385,7 +409,11 @@ class FeatureController extends Controller { updatedFeature.strategies.map(async (s) => this.service.createStrategy( s, - { projectId, featureName, environment: DEFAULT_ENV }, + { + projectId: projectId!!, + featureName, + environment: DEFAULT_ENV, + }, userName, req.user, ), @@ -393,22 +421,25 @@ class FeatureController extends Controller { ); } await this.service.updateEnabled( - projectId, + projectId!!, featureName, DEFAULT_ENV, updatedFeature.enabled, userName, + req.user, ); await this.service.saveVariants( featureName, - projectId, + projectId!!, value.variants || [], userName, + req.user.id, ); const feature = await this.service.storeFeatureUpdatedEventLegacy( featureName, userName, + req.user.id, ); res.status(200).json(feature); @@ -432,6 +463,7 @@ class FeatureController extends Controller { await this.service.storeFeatureUpdatedEventLegacy( featureName, userName, + req.user.id, ); res.status(200).json(feature); } @@ -450,6 +482,7 @@ class FeatureController extends Controller { await this.service.storeFeatureUpdatedEventLegacy( featureName, userName, + req.user.id, ); res.json(feature); } @@ -468,6 +501,7 @@ class FeatureController extends Controller { await this.service.storeFeatureUpdatedEventLegacy( featureName, userName, + req.user.id, ); res.json(feature); } @@ -475,7 +509,12 @@ class FeatureController extends Controller { async staleOn(req: IAuthRequest, res: Response): Promise { const { featureName } = req.params; const userName = extractUsername(req); - await this.service.updateStale(featureName, true, userName); + await this.service.updateStale( + featureName, + true, + userName, + req.user.id, + ); const feature = await this.service.getFeatureToggleLegacy(featureName); res.json(feature); } @@ -483,7 +522,12 @@ class FeatureController extends Controller { async staleOff(req: IAuthRequest, res: Response): Promise { const { featureName } = req.params; const userName = extractUsername(req); - await this.service.updateStale(featureName, false, userName); + await this.service.updateStale( + featureName, + false, + userName, + req.user.id, + ); const feature = await this.service.getFeatureToggleLegacy(featureName); res.json(feature); } diff --git a/src/lib/features/feature-toggle/tests/feature-toggle-service.e2e.test.ts b/src/lib/features/feature-toggle/tests/feature-toggle-service.e2e.test.ts index fec458ec8ae2..c36534d603d5 100644 --- a/src/lib/features/feature-toggle/tests/feature-toggle-service.e2e.test.ts +++ b/src/lib/features/feature-toggle/tests/feature-toggle-service.e2e.test.ts @@ -9,6 +9,7 @@ import { IUnleashStores, IVariant, SKIP_CHANGE_REQUEST, + SYSTEM_USER, SYSTEM_USER_ID, } from '../../../types'; import EnvironmentService from '../../project-environments/environment-service'; @@ -28,7 +29,7 @@ let segmentService: ISegmentService; let eventService: EventService; let environmentService: EnvironmentService; let unleashConfig; - +const TEST_USER_ID = -9999; const mockConstraints = (): IConstraint[] => { return Array.from({ length: 5 }).map(() => ({ values: ['x', 'y', 'z'], @@ -63,7 +64,6 @@ afterAll(async () => { beforeEach(async () => { await db.rawDatabase('change_request_settings').del(); }); - test('Should create feature toggle strategy configuration', async () => { const projectId = 'default'; const username = 'feature-toggle'; @@ -79,6 +79,7 @@ test('Should create feature toggle strategy configuration', async () => { name: 'Demo', }, 'test', + TEST_USER_ID, ); const createdConfig = await service.createStrategy( @@ -107,6 +108,7 @@ test('Should be able to update existing strategy configuration', async () => { name: featureName, }, 'test', + TEST_USER_ID, ); const createdConfig = await service.createStrategy( @@ -143,6 +145,7 @@ test('Should be able to get strategy by id', async () => { name: featureName, }, userName, + TEST_USER_ID, ); const createdConfig = await service.createStrategy( @@ -168,6 +171,7 @@ test('should ignore name in the body when updating feature toggle', async () => description: 'First toggle', }, userName, + TEST_USER_ID, ); await service.createFeatureToggle( @@ -177,6 +181,7 @@ test('should ignore name in the body when updating feature toggle', async () => description: 'Second toggle', }, userName, + TEST_USER_ID, ); const update = { @@ -184,7 +189,13 @@ test('should ignore name in the body when updating feature toggle', async () => description: "I'm changed", }; - await service.updateFeatureToggle(projectId, update, userName, featureName); + await service.updateFeatureToggle( + projectId, + update, + userName, + featureName, + TEST_USER_ID, + ); const featureOne = await service.getFeature({ featureName }); const featureTwo = await service.getFeature({ featureName: secondFeatureName, @@ -206,6 +217,7 @@ test('should not get empty rows as features', async () => { description: 'First toggle', }, userName, + TEST_USER_ID, ); await service.createFeatureToggle( @@ -215,6 +227,7 @@ test('should not get empty rows as features', async () => { description: 'Second toggle', }, userName, + TEST_USER_ID, ); const user = { email: 'test@example.com' } as User; @@ -255,6 +268,7 @@ test('adding and removing an environment preserves variants when variants per en ], }, 'random_user', + TEST_USER_ID, ); //force the variantEnvironments flag off so that we can test legacy behavior @@ -270,9 +284,24 @@ test('adding and removing an environment preserves variants when variants per en eventService, ); - await environmentService.addEnvironmentToProject(prodEnv, 'default'); - await environmentService.removeEnvironmentFromProject(prodEnv, 'default'); - await environmentService.addEnvironmentToProject(prodEnv, 'default'); + await environmentService.addEnvironmentToProject( + prodEnv, + 'default', + SYSTEM_USER.username, + SYSTEM_USER.id, + ); + await environmentService.removeEnvironmentFromProject( + prodEnv, + 'default', + SYSTEM_USER.username, + SYSTEM_USER.id, + ); + await environmentService.addEnvironmentToProject( + prodEnv, + 'default', + SYSTEM_USER.username, + SYSTEM_USER.id, + ); const toggle = await service.getFeature({ featureName, @@ -293,6 +322,7 @@ test('cloning a feature toggle copies variant environments correctly', async () name: newToggleName, }, 'test', + TEST_USER_ID, ); await stores.environmentStore.create({ @@ -337,8 +367,8 @@ test('cloning a feature toggle copies variant environments correctly', async () ); const newEnv = clonedToggle.environments.find((x) => x.name === targetEnv); - expect(defaultEnv.variants).toHaveLength(0); - expect(newEnv.variants).toHaveLength(1); + expect(defaultEnv!!.variants).toHaveLength(0); + expect(newEnv!!.variants).toHaveLength(1); }); test('cloning a feature toggle not allowed for change requests enabled', async () => { @@ -368,7 +398,7 @@ test('changing to a project with change requests enabled should not be allowed', environment: 'default', }); await expect( - service.changeProject('newToggleName', 'default', 'user'), + service.changeProject('newToggleName', 'default', 'user', TEST_USER_ID), ).rejects.toEqual( new ForbiddenError( `Changing project not allowed. Project default has change requests enabled.`, @@ -396,6 +426,7 @@ test('Cloning a feature toggle also clones segments correctly', async () => { name: featureName, }, 'test-user', + TEST_USER_ID, ); const config: Omit = { @@ -435,6 +466,7 @@ test('If change requests are enabled, cannot change variants without going via C 'default', { name: featureName }, 'test-user', + TEST_USER_ID, ); // Force all feature flags on to make sure we have Change requests on @@ -514,6 +546,7 @@ test('If CRs are protected for any environment in the project stops bulk update project.id, { name: 'crOnVariantToggle' }, user.username, + user.id, ); const variant: IVariant = { @@ -584,6 +617,7 @@ test('getPlaygroundFeatures should return ids and titles (if they exist) on clie name: featureName, }, userName, + TEST_USER_ID, ); await service.createStrategy( @@ -677,6 +711,7 @@ test('Should return last seen at per environment', async () => { name: featureName, }, userName, + TEST_USER_ID, ); const date = await insertFeatureEnvironmentsLastSeen( diff --git a/src/lib/features/feature-toggle/tests/feature-toggles.auth.e2e.test.ts b/src/lib/features/feature-toggle/tests/feature-toggles.auth.e2e.test.ts index a26742919bb4..5e9bc5fa768f 100644 --- a/src/lib/features/feature-toggle/tests/feature-toggles.auth.e2e.test.ts +++ b/src/lib/features/feature-toggle/tests/feature-toggles.auth.e2e.test.ts @@ -88,6 +88,7 @@ test('Should not be possible auto-enable feature toggle without CREATE_FEATURE_S 'default', { name }, 'me', + -9999, true, ); diff --git a/src/lib/features/maintenance/maintenance-controller.ts b/src/lib/features/maintenance/maintenance-controller.ts index 9ba528606997..b8cab5483e83 100644 --- a/src/lib/features/maintenance/maintenance-controller.ts +++ b/src/lib/features/maintenance/maintenance-controller.ts @@ -84,6 +84,7 @@ export default class MaintenanceController extends Controller { await this.maintenanceService.toggleMaintenanceMode( req.body, extractUsername(req), + req.user.id, ); res.status(204).end(); } diff --git a/src/lib/features/maintenance/maintenance-service.test.ts b/src/lib/features/maintenance/maintenance-service.test.ts index 892970a0171f..bbbaadb82eb7 100644 --- a/src/lib/features/maintenance/maintenance-service.test.ts +++ b/src/lib/features/maintenance/maintenance-service.test.ts @@ -42,6 +42,7 @@ test('Scheduler should not run scheduled functions if maintenance mode is on', a await maintenanceService.toggleMaintenanceMode( { enabled: true }, 'irrelevant user', + -9999, ); const job = jest.fn(); diff --git a/src/lib/features/maintenance/maintenance-service.ts b/src/lib/features/maintenance/maintenance-service.ts index 4e8d89af2fbb..8686762ede73 100644 --- a/src/lib/features/maintenance/maintenance-service.ts +++ b/src/lib/features/maintenance/maintenance-service.ts @@ -39,11 +39,13 @@ export default class MaintenanceService implements IMaintenanceStatus { async toggleMaintenanceMode( setting: MaintenanceSchema, user: string, + toggledByUserId: number, ): Promise { return this.settingService.insert( maintenanceSettingsKey, setting, user, + toggledByUserId, false, ); } diff --git a/src/lib/features/project-environments/environment-service.test.ts b/src/lib/features/project-environments/environment-service.test.ts index 4f92cd509ffb..1ff97b055d82 100644 --- a/src/lib/features/project-environments/environment-service.test.ts +++ b/src/lib/features/project-environments/environment-service.test.ts @@ -2,7 +2,7 @@ import EnvironmentService from './environment-service'; import { createTestConfig } from '../../../test/config/test-config'; import dbInit from '../../../test/e2e/helpers/database-init'; import NotFoundError from '../../error/notfound-error'; -import { IUnleashStores } from '../../types'; +import { IUnleashStores, SYSTEM_USER } from '../../types'; import NameExistsError from '../../error/name-exists-error'; import { EventService } from '../../services'; @@ -53,7 +53,12 @@ test('Can connect environment to project', async () => { description: '', stale: false, }); - await service.addEnvironmentToProject('test-connection', 'default', 'user'); + await service.addEnvironmentToProject( + 'test-connection', + 'default', + SYSTEM_USER.username, + SYSTEM_USER.id, + ); const overview = await stores.featureStrategiesStore.getFeatureOverview({ projectId: 'default', }); @@ -88,8 +93,18 @@ test('Can remove environment from project', async () => { await stores.featureToggleStore.create('default', { name: 'removal-test', }); - await service.removeEnvironmentFromProject('test-connection', 'default'); - await service.addEnvironmentToProject('removal-test', 'default'); + await service.removeEnvironmentFromProject( + 'test-connection', + 'default', + SYSTEM_USER.username, + SYSTEM_USER.id, + ); + await service.addEnvironmentToProject( + 'removal-test', + 'default', + SYSTEM_USER.username, + SYSTEM_USER.id, + ); let overview = await stores.featureStrategiesStore.getFeatureOverview({ projectId: 'default', }); @@ -111,7 +126,8 @@ test('Can remove environment from project', async () => { await service.removeEnvironmentFromProject( 'removal-test', 'default', - 'user', + SYSTEM_USER.username, + SYSTEM_USER.id, ); overview = await stores.featureStrategiesStore.getFeatureOverview({ projectId: 'default', @@ -134,13 +150,33 @@ test('Adding same environment twice should throw a NameExistsError', async () => name: 'uniqueness-test', type: 'production', }); - await service.addEnvironmentToProject('uniqueness-test', 'default'); + await service.addEnvironmentToProject( + 'uniqueness-test', + 'default', + SYSTEM_USER.username, + SYSTEM_USER.id, + ); - await service.removeEnvironmentFromProject('test-connection', 'default'); - await service.removeEnvironmentFromProject('removal-test', 'default'); + await service.removeEnvironmentFromProject( + 'test-connection', + 'default', + SYSTEM_USER.username, + SYSTEM_USER.id, + ); + await service.removeEnvironmentFromProject( + 'removal-test', + 'default', + SYSTEM_USER.username, + SYSTEM_USER.id, + ); return expect(async () => - service.addEnvironmentToProject('uniqueness-test', 'default'), + service.addEnvironmentToProject( + 'uniqueness-test', + 'default', + SYSTEM_USER.username, + SYSTEM_USER.id, + ), ).rejects.toThrow( new NameExistsError( 'default already has the environment uniqueness-test enabled', @@ -153,6 +189,8 @@ test('Removing environment not connected to project should be a noop', async () service.removeEnvironmentFromProject( 'some-non-existing-environment', 'default', + SYSTEM_USER.username, + SYSTEM_USER.id, ), ).resolves); @@ -247,7 +285,12 @@ test('When given overrides should remap projects to override environments', asyn stale: false, }); - await service.addEnvironmentToProject(disabledEnvName, 'default'); + await service.addEnvironmentToProject( + disabledEnvName, + 'default', + SYSTEM_USER.username, + SYSTEM_USER.id, + ); await service.overrideEnabledProjects([enabledEnvName]); diff --git a/src/lib/features/project-environments/environment-service.ts b/src/lib/features/project-environments/environment-service.ts index 311df68e60df..616fddf715e7 100644 --- a/src/lib/features/project-environments/environment-service.ts +++ b/src/lib/features/project-environments/environment-service.ts @@ -10,6 +10,7 @@ import { IUnleashStores, PROJECT_ENVIRONMENT_ADDED, PROJECT_ENVIRONMENT_REMOVED, + SYSTEM_USER, } from '../../types'; import { Logger } from '../../logger'; import { BadDataError, UNIQUE_CONSTRAINT_VIOLATION } from '../../error'; @@ -100,7 +101,8 @@ export default class EnvironmentService { async addEnvironmentToProject( environment: string, projectId: string, - username = 'unknown', + username: string, + userId: number, ): Promise { try { await this.featureEnvironmentStore.connectProject( @@ -116,6 +118,7 @@ export default class EnvironmentService { project: projectId, environment, createdBy: username, + createdByUserId: userId, }); } catch (e) { if (e.code === UNIQUE_CONSTRAINT_VIOLATION) { @@ -132,6 +135,7 @@ export default class EnvironmentService { projectId: string, strategy: CreateFeatureStrategySchema, username: string, + userId: number, ): Promise { if (strategy.name !== 'flexibleRollout') { throw new BadDataError( @@ -152,6 +156,7 @@ export default class EnvironmentService { createdBy: username, preData: previousDefaultStrategy, data: defaultStrategy, + createdByUserId: userId, }); return defaultStrategy; @@ -217,7 +222,12 @@ export default class EnvironmentService { const linkTasks = uniqueProjects.flatMap((project) => { return toEnable.map((enabledEnv) => { - return this.addEnvironmentToProject(enabledEnv.name, project); + return this.addEnvironmentToProject( + enabledEnv.name, + project, + SYSTEM_USER.username, + SYSTEM_USER.id, + ); }); }); @@ -241,7 +251,8 @@ export default class EnvironmentService { async removeEnvironmentFromProject( environment: string, projectId: string, - username = 'unknown', + username: string, + userId: number, ): Promise { const projectEnvs = await this.projectStore.getEnvironmentsForProject(projectId); @@ -256,6 +267,7 @@ export default class EnvironmentService { project: projectId, environment, createdBy: username, + createdByUserId: userId, }); return; } diff --git a/src/lib/features/project-environments/environments.ts b/src/lib/features/project-environments/environments.ts index 7a875e3d5e17..b45b5a39c317 100644 --- a/src/lib/features/project-environments/environments.ts +++ b/src/lib/features/project-environments/environments.ts @@ -145,6 +145,7 @@ export default class EnvironmentsController extends Controller { environment, projectId, extractUsername(req), + req.user.id, ), ); @@ -162,6 +163,7 @@ export default class EnvironmentsController extends Controller { environment, projectId, extractUsername(req), + req.user.id, ), ); @@ -184,6 +186,7 @@ export default class EnvironmentsController extends Controller { projectId, strategy, extractUsername(req), + req.user.id, ), ); diff --git a/src/lib/features/scheduler/scheduler-service.test.ts b/src/lib/features/scheduler/scheduler-service.test.ts index afb4fc035f00..87a55938d5e9 100644 --- a/src/lib/features/scheduler/scheduler-service.test.ts +++ b/src/lib/features/scheduler/scheduler-service.test.ts @@ -35,6 +35,7 @@ const toggleMaintenanceMode = async ( await maintenanceService.toggleMaintenanceMode( { enabled }, 'irrelevant user', + -9999, ); }; diff --git a/src/lib/features/tag-type/tag-type-service.ts b/src/lib/features/tag-type/tag-type-service.ts index e04464db0ffc..b330620af8c4 100644 --- a/src/lib/features/tag-type/tag-type-service.ts +++ b/src/lib/features/tag-type/tag-type-service.ts @@ -13,6 +13,7 @@ import { Logger } from '../../logger'; import { ITagType, ITagTypeStore } from './tag-type-store-type'; import { IUnleashConfig } from '../../types/option'; import EventService from '../../services/event-service'; +import { SYSTEM_USER } from '../../types'; export default class TagTypeService { private tagTypeStore: ITagTypeStore; @@ -42,6 +43,7 @@ export default class TagTypeService { async createTagType( newTagType: ITagType, userName: string, + userId: number, ): Promise { const data = (await tagTypeSchema.validateAsync( newTagType, @@ -50,7 +52,8 @@ export default class TagTypeService { await this.tagTypeStore.createTagType(data); await this.eventService.storeEvent({ type: TAG_TYPE_CREATED, - createdBy: userName || 'unleash-system', + createdBy: userName || SYSTEM_USER.username, + createdByUserId: userId, data, }); return data; @@ -73,12 +76,17 @@ export default class TagTypeService { } } - async deleteTagType(name: string, userName: string): Promise { + async deleteTagType( + name: string, + userName: string, + userId: number, + ): Promise { const tagType = await this.tagTypeStore.get(name); await this.tagTypeStore.delete(name); await this.eventService.storeEvent({ type: TAG_TYPE_DELETED, - createdBy: userName || 'unleash-system', + createdBy: userName || SYSTEM_USER.username, + createdByUserId: userId, preData: tagType, }); } @@ -86,12 +94,14 @@ export default class TagTypeService { async updateTagType( updatedTagType: ITagType, userName: string, + userId: number, ): Promise { const data = await tagTypeSchema.validateAsync(updatedTagType); await this.tagTypeStore.updateTagType(data); await this.eventService.storeEvent({ type: TAG_TYPE_UPDATED, - createdBy: userName || 'unleash-system', + createdBy: userName || SYSTEM_USER.username, + createdByUserId: userId, data, }); return data; diff --git a/src/lib/features/tag-type/tag-type.ts b/src/lib/features/tag-type/tag-type.ts index 157484223fb5..8ec5c25f1d74 100644 --- a/src/lib/features/tag-type/tag-type.ts +++ b/src/lib/features/tag-type/tag-type.ts @@ -203,7 +203,7 @@ class TagTypeController extends Controller { ): Promise { const userName = extractUsername(req); const tagType = await this.tagTypeService.transactional((service) => - service.createTagType(req.body, userName), + service.createTagType(req.body, userName, req.user.id), ); res.status(201) .header('location', `tag-types/${tagType.name}`) @@ -219,7 +219,11 @@ class TagTypeController extends Controller { const userName = extractUsername(req); await this.tagTypeService.transactional((service) => - service.updateTagType({ name, description, icon }, userName), + service.updateTagType( + { name, description, icon }, + userName, + req.user.id, + ), ); res.status(200).end(); } @@ -235,7 +239,7 @@ class TagTypeController extends Controller { const { name } = req.params; const userName = extractUsername(req); await this.tagTypeService.transactional((service) => - service.deleteTagType(name, userName), + service.deleteTagType(name, userName, req.user.id), ); res.status(200).end(); } diff --git a/src/lib/middleware/cors-origin-middleware.test.ts b/src/lib/middleware/cors-origin-middleware.test.ts index abd44622daea..f5c6c73532a8 100644 --- a/src/lib/middleware/cors-origin-middleware.test.ts +++ b/src/lib/middleware/cors-origin-middleware.test.ts @@ -9,6 +9,7 @@ import { ISettingStore } from '../../lib/types'; import { frontendSettingsKey } from '../../lib/types/settings/frontend-settings'; import FakeFeatureTagStore from '../../test/fixtures/fake-feature-tag-store'; +const TEST_USER_ID = -9999; const createSettingService = ( frontendApiOrigins: string[], ): { proxyService: ProxyService; settingStore: ISettingStore } => { @@ -52,6 +53,7 @@ test('corsOriginMiddleware origin validation', async () => { proxyService.setFrontendSettings( { frontendApiOrigins: ['a'] }, userName, + TEST_USER_ID, ), ).rejects.toThrow('Invalid origin: a'); }); @@ -65,6 +67,7 @@ test('corsOriginMiddleware without config', async () => { await proxyService.setFrontendSettings( { frontendApiOrigins: [] }, userName, + TEST_USER_ID, ); expect(await proxyService.getFrontendSettings(false)).toEqual({ frontendApiOrigins: [], @@ -72,6 +75,7 @@ test('corsOriginMiddleware without config', async () => { await proxyService.setFrontendSettings( { frontendApiOrigins: ['*'] }, userName, + TEST_USER_ID, ); expect(await proxyService.getFrontendSettings(false)).toEqual({ frontendApiOrigins: ['*'], @@ -91,6 +95,7 @@ test('corsOriginMiddleware with config', async () => { await proxyService.setFrontendSettings( { frontendApiOrigins: [] }, userName, + TEST_USER_ID, ); expect(await proxyService.getFrontendSettings(false)).toEqual({ frontendApiOrigins: [], @@ -98,6 +103,7 @@ test('corsOriginMiddleware with config', async () => { await proxyService.setFrontendSettings( { frontendApiOrigins: ['https://example.com', 'https://example.org'] }, userName, + TEST_USER_ID, ); expect(await proxyService.getFrontendSettings(false)).toEqual({ frontendApiOrigins: ['https://example.com', 'https://example.org'], @@ -120,6 +126,7 @@ test('corsOriginMiddleware with caching enabled', async () => { await proxyService.setFrontendSettings( { frontendApiOrigins: ['*'] }, userName, + TEST_USER_ID, ); //still get cached value diff --git a/src/lib/routes/admin-api/addon.ts b/src/lib/routes/admin-api/addon.ts index 581e4b09c522..7ded8039c52b 100644 --- a/src/lib/routes/admin-api/addon.ts +++ b/src/lib/routes/admin-api/addon.ts @@ -181,7 +181,12 @@ Note: passing \`null\` as a value for the description property will set it to an const createdBy = extractUsername(req); const data = req.body; - const addon = await this.addonService.updateAddon(id, data, createdBy); + const addon = await this.addonService.updateAddon( + id, + data, + createdBy, + req.user.id, + ); this.openApiService.respondWithValidation( 200, @@ -197,7 +202,11 @@ Note: passing \`null\` as a value for the description property will set it to an ): Promise { const createdBy = extractUsername(req); const data = req.body; - const addon = await this.addonService.createAddon(data, createdBy); + const addon = await this.addonService.createAddon( + data, + createdBy, + req.user.id, + ); this.openApiService.respondWithValidation( 201, @@ -213,7 +222,7 @@ Note: passing \`null\` as a value for the description property will set it to an ): Promise { const { id } = req.params; const username = extractUsername(req); - await this.addonService.removeAddon(id, username); + await this.addonService.removeAddon(id, username, req.user.id); res.status(200).end(); } diff --git a/src/lib/routes/admin-api/api-token.ts b/src/lib/routes/admin-api/api-token.ts index 6dc1b9b02dbf..a494c2859077 100644 --- a/src/lib/routes/admin-api/api-token.ts +++ b/src/lib/routes/admin-api/api-token.ts @@ -363,6 +363,7 @@ export class ApiTokenController extends Controller { token, new Date(expiresAt), extractUsername(req), + req.user.id, ); return res.status(200).end(); @@ -393,7 +394,11 @@ export class ApiTokenController extends Controller { `You do not have the required access [${permissionRequired}] to perform this operation`, ); } - await this.apiTokenService.delete(token, extractUsername(req)); + await this.apiTokenService.delete( + token, + extractUsername(req), + req.user.id, + ); await this.proxyService.deleteClientForProxyToken(token); res.status(200).end(); } diff --git a/src/lib/routes/admin-api/config.ts b/src/lib/routes/admin-api/config.ts index 2cfd9127f6b3..9c4aca8c8d3d 100644 --- a/src/lib/routes/admin-api/config.ts +++ b/src/lib/routes/admin-api/config.ts @@ -160,6 +160,7 @@ class ConfigController extends Controller { await this.proxyService.setFrontendSettings( req.body.frontendSettings, extractUsername(req), + req.user.id, ); res.sendStatus(204); return; diff --git a/src/lib/routes/admin-api/context.ts b/src/lib/routes/admin-api/context.ts index 10db5ee4a53b..45ffaa079fd1 100644 --- a/src/lib/routes/admin-api/context.ts +++ b/src/lib/routes/admin-api/context.ts @@ -248,6 +248,7 @@ export class ContextController extends Controller { const result = await this.contextService.createContextField( value, userName, + req.user.id, ); this.openApiService.respondWithValidation( @@ -270,6 +271,7 @@ export class ContextController extends Controller { await this.contextService.updateContextField( { ...contextField, name }, userName, + req.user.id, ); res.status(200).end(); } @@ -281,7 +283,11 @@ export class ContextController extends Controller { const name = req.params.contextField; const userName = extractUsername(req); - await this.contextService.deleteContextField(name, userName); + await this.contextService.deleteContextField( + name, + userName, + req.user.id, + ); res.status(200).end(); } diff --git a/src/lib/routes/admin-api/events.test.ts b/src/lib/routes/admin-api/events.test.ts index db37ba932105..6f63decc1ead 100644 --- a/src/lib/routes/admin-api/events.test.ts +++ b/src/lib/routes/admin-api/events.test.ts @@ -11,7 +11,7 @@ import { ProjectUserAddedEvent, ProjectUserRemovedEvent, } from '../../types/events'; - +const TEST_USER_ID = -9999; async function getSetup(anonymise: boolean = false) { const base = `/random${Math.round(Math.random() * 1000)}`; const stores = createStores(); @@ -49,6 +49,7 @@ test('should get events list via admin', async () => { data: { name: 'test', project: 'default' }, featureName: 'test', project: 'default', + createdByUserId: TEST_USER_ID, }), ); const { body } = await request @@ -68,6 +69,7 @@ test('should anonymise events list via admin', async () => { data: { name: 'test', project: 'default' }, featureName: 'test', project: 'default', + createdByUserId: TEST_USER_ID, }), ); const { body } = await request @@ -87,6 +89,7 @@ test('should also anonymise email fields in data and preData properties', async eventService.storeEvent( new ProjectUserAddedEvent({ createdBy: 'some@email.com', + createdByUserId: TEST_USER_ID, data: { name: 'test', project: 'default', email: email1 }, project: 'default', }), @@ -94,6 +97,7 @@ test('should also anonymise email fields in data and preData properties', async eventService.storeEvent( new ProjectUserRemovedEvent({ createdBy: 'some@email.com', + createdByUserId: TEST_USER_ID, preData: { name: 'test', project: 'default', email: email2 }, project: 'default', }), @@ -115,6 +119,7 @@ test('should anonymise any PII fields, no matter the depth', async () => { eventService.storeEvent( new ProjectAccessAddedEvent({ createdBy: 'some@email.com', + createdByUserId: TEST_USER_ID, data: { groups: [ { diff --git a/src/lib/routes/admin-api/project/api-token.ts b/src/lib/routes/admin-api/project/api-token.ts index 62d6fd80f57a..3a1631794f3d 100644 --- a/src/lib/routes/admin-api/project/api-token.ts +++ b/src/lib/routes/admin-api/project/api-token.ts @@ -220,7 +220,11 @@ export class ProjectApiTokenController extends Controller { (storedToken.projects.length === 1 && storedToken.project[0] === projectId)) ) { - await this.apiTokenService.delete(token, extractUsername(req)); + await this.apiTokenService.delete( + token, + extractUsername(req), + user.id, + ); await this.proxyService.deleteClientForProxyToken(token); res.status(200).end(); } else if (!storedToken) { diff --git a/src/lib/routes/admin-api/project/project-archive.ts b/src/lib/routes/admin-api/project/project-archive.ts index 8fd915396fbd..63f2d1730dff 100644 --- a/src/lib/routes/admin-api/project/project-archive.ts +++ b/src/lib/routes/admin-api/project/project-archive.ts @@ -166,7 +166,12 @@ export default class ProjectArchiveController extends Controller { const { projectId } = req.params; const { features } = req.body; const user = extractUsername(req); - await this.featureService.deleteFeatures(features, projectId, user); + await this.featureService.deleteFeatures( + features, + projectId, + user, + req.user.id, + ); res.status(200).end(); } @@ -182,6 +187,7 @@ export default class ProjectArchiveController extends Controller { features, projectId, user, + req.user.id, ), ); res.status(200).end(); diff --git a/src/lib/routes/admin-api/project/variants.ts b/src/lib/routes/admin-api/project/variants.ts index f027f2d6c265..8d85d33775fc 100644 --- a/src/lib/routes/admin-api/project/variants.ts +++ b/src/lib/routes/admin-api/project/variants.ts @@ -257,6 +257,7 @@ The backend will also distribute remaining weight up to 1000 after adding the va projectId, req.body, userName, + req.user.id, ); res.status(200).json({ version: 1, diff --git a/src/lib/routes/admin-api/public-signup.ts b/src/lib/routes/admin-api/public-signup.ts index 3ae135e9daae..efefd19e6d82 100644 --- a/src/lib/routes/admin-api/public-signup.ts +++ b/src/lib/routes/admin-api/public-signup.ts @@ -190,6 +190,7 @@ export class PublicSignupController extends Controller { await this.publicSignupTokenService.createNewPublicSignupToken( req.body, username, + req.user.id, ); this.openApiService.respondWithValidation( 201, @@ -219,6 +220,7 @@ export class PublicSignupController extends Controller { ...(expiresAt ? { expiresAt: new Date(expiresAt) } : {}), }, extractUsername(req), + req.user.id, ); this.openApiService.respondWithValidation( diff --git a/src/lib/routes/admin-api/state.ts b/src/lib/routes/admin-api/state.ts index 64561307bd8a..aeea572cdf9c 100644 --- a/src/lib/routes/admin-api/state.ts +++ b/src/lib/routes/admin-api/state.ts @@ -122,6 +122,7 @@ class StateController extends Controller { userName, dropBeforeImport: paramToBool(drop, false), keepExisting: paramToBool(keep, true), + userId: req.user.id, }); res.sendStatus(202); } diff --git a/src/lib/routes/admin-api/strategy.ts b/src/lib/routes/admin-api/strategy.ts index b358f26656f1..652a7fa0a6c5 100644 --- a/src/lib/routes/admin-api/strategy.ts +++ b/src/lib/routes/admin-api/strategy.ts @@ -233,7 +233,11 @@ class StrategyController extends Controller { const strategyName = req.params.name; const userName = extractUsername(req); - await this.strategyService.removeStrategy(strategyName, userName); + await this.strategyService.removeStrategy( + strategyName, + userName, + req.user.id, + ); res.status(200).end(); } @@ -246,6 +250,7 @@ class StrategyController extends Controller { const strategy = await this.strategyService.createStrategy( req.body, userName, + req.user.id, ); this.openApiService.respondWithValidation( 201, @@ -265,6 +270,7 @@ class StrategyController extends Controller { await this.strategyService.updateStrategy( { ...req.body, name: req.params.name }, userName, + req.user.id, ); res.status(200).end(); } @@ -276,7 +282,11 @@ class StrategyController extends Controller { const userName = extractUsername(req); const { strategyName } = req.params; - await this.strategyService.deprecateStrategy(strategyName, userName); + await this.strategyService.deprecateStrategy( + strategyName, + userName, + req.user.id, + ); res.status(200).end(); } @@ -287,7 +297,11 @@ class StrategyController extends Controller { const userName = extractUsername(req); const { strategyName } = req.params; - await this.strategyService.reactivateStrategy(strategyName, userName); + await this.strategyService.reactivateStrategy( + strategyName, + userName, + req.user.id, + ); res.status(200).end(); } } diff --git a/src/lib/routes/admin-api/tag.ts b/src/lib/routes/admin-api/tag.ts index 65afe240ec95..a7b6b7153570 100644 --- a/src/lib/routes/admin-api/tag.ts +++ b/src/lib/routes/admin-api/tag.ts @@ -200,7 +200,11 @@ class TagController extends Controller { res: Response, ): Promise { const userName = extractUsername(req); - const tag = await this.tagService.createTag(req.body, userName); + const tag = await this.tagService.createTag( + req.body, + userName, + req.user.id, + ); res.status(201) .header('location', `tags/${tag.type}/${tag.value}`) .json({ version, tag }) @@ -213,7 +217,7 @@ class TagController extends Controller { ): Promise { const { type, value } = req.params; const userName = extractUsername(req); - await this.tagService.deleteTag({ type, value }, userName); + await this.tagService.deleteTag({ type, value }, userName, req.user.id); res.status(200).end(); } } diff --git a/src/lib/server-impl.ts b/src/lib/server-impl.ts index 93d99c2750dc..26a5382e345f 100644 --- a/src/lib/server-impl.ts +++ b/src/lib/server-impl.ts @@ -19,6 +19,7 @@ import { IUnleashServices, RoleName, CustomAuthHandler, + SYSTEM_USER, } from './types'; import User, { IUser } from './types/user'; @@ -93,6 +94,7 @@ async function createApp( dropBeforeImport: config.import.dropBeforeImport, userName: 'import', keepExisting: config.import.keepExisting, + userId: SYSTEM_USER.id, }); } diff --git a/src/lib/services/access-service.test.ts b/src/lib/services/access-service.test.ts index fd0a544a680e..d0640ff15681 100644 --- a/src/lib/services/access-service.test.ts +++ b/src/lib/services/access-service.test.ts @@ -16,7 +16,7 @@ import AccessStoreMock from '../../test/fixtures/fake-access-store'; import { GroupService } from '../services/group-service'; import FakeEventStore from '../../test/fixtures/fake-event-store'; import { IRole } from '../../lib/types/stores/access-store'; -import { IGroup, ROLE_CREATED } from '../../lib/types'; +import { IGroup, ROLE_CREATED, SYSTEM_USER } from '../../lib/types'; import EventService from './event-service'; import FakeFeatureTagStore from '../../test/fixtures/fake-feature-tag-store'; import BadDataError from '../../lib/error/bad-data-error'; @@ -40,6 +40,7 @@ test('should fail when name exists', async () => { name: 'existing role', description: 'description', permissions: [], + createdByUserId: -9999, }); expect(accessService.validateRole(existingRole)).rejects.toThrow( @@ -172,6 +173,7 @@ test('user with custom root role should get a user root role', async () => { name: 'custom-root-role', description: 'test custom root role', type: CUSTOM_ROOT_ROLE_TYPE, + createdByUserId: -9999, permissions: [ { id: 1, @@ -259,7 +261,7 @@ test('throws error when trying to delete a project role in use by group', async ); try { - await accessService.deleteRole(1); + await accessService.deleteRole(1, SYSTEM_USER.username, SYSTEM_USER.id); } catch (e) { expect(e.toString()).toBe( 'RoleInUseError: Role is in use by users(0) or groups(1). You cannot delete a role that is in use without first removing the role from the users and groups.', diff --git a/src/lib/services/access-service.ts b/src/lib/services/access-service.ts index 10585a0719aa..5252857b9b1e 100644 --- a/src/lib/services/access-service.ts +++ b/src/lib/services/access-service.ts @@ -46,6 +46,7 @@ import { ROLE_CREATED, ROLE_DELETED, ROLE_UPDATED, + SYSTEM_USER, } from '../types'; import EventService from './event-service'; @@ -70,6 +71,7 @@ export interface IRoleCreation { type?: 'root-custom' | 'custom'; permissions?: PermissionRef[]; createdBy?: string; + createdByUserId: number; } export interface IRoleValidation { @@ -85,6 +87,7 @@ export interface IRoleUpdate { type?: 'root-custom' | 'custom'; permissions?: PermissionRef[]; createdBy?: string; + createdByUserId: number; } export interface AccessWithRoles { @@ -674,6 +677,7 @@ export class AccessService { this.eventService.storeEvent({ type: ROLE_CREATED, createdBy: role.createdBy || 'unknown', + createdByUserId: role.createdByUserId, data: { ...newRole, permissions: this.sanitizePermissions(addedPermissions), @@ -729,7 +733,8 @@ export class AccessService { ); this.eventService.storeEvent({ type: ROLE_UPDATED, - createdBy: role.createdBy || 'unknown', + createdBy: role.createdBy || SYSTEM_USER.username, + createdByUserId: role.createdByUserId, data: { ...updatedRole, permissions: this.sanitizePermissions(updatedPermissions), @@ -754,7 +759,11 @@ export class AccessService { }); } - async deleteRole(id: number, deletedBy = 'unknown'): Promise { + async deleteRole( + id: number, + deletedBy: string, + deletedByUserId: number, + ): Promise { await this.validateRoleIsNotBuiltIn(id); const roleUsers = await this.getUsersForRole(id); @@ -772,6 +781,7 @@ export class AccessService { this.eventService.storeEvent({ type: ROLE_DELETED, createdBy: deletedBy, + createdByUserId: deletedByUserId, preData: { ...existingRole, permissions: this.sanitizePermissions(existingPermissions), diff --git a/src/lib/services/addon-service.test.ts b/src/lib/services/addon-service.test.ts index 8c7451500e32..535081985ee7 100644 --- a/src/lib/services/addon-service.test.ts +++ b/src/lib/services/addon-service.test.ts @@ -15,9 +15,12 @@ import { IAddonDto } from '../types/stores/addon-store'; import SimpleAddon from './addon-service-test-simple-addon'; import { IAddonProviders } from '../addons'; import EventService from './event-service'; +import { SYSTEM_USER } from '../types'; const MASKED_VALUE = '*****'; +const TEST_USER_ID = -9999; + let addonProvider: IAddonProviders; function getSetup() { @@ -64,7 +67,7 @@ test('should load provider definitions', async () => { const simple = providerDefinitions.find((p) => p.name === 'simple'); expect(providerDefinitions.length).toBe(1); - expect(simple.name).toBe('simple'); + expect(simple!.name).toBe('simple'); }); test('should not allow addon-config for unknown provider', async () => { @@ -80,6 +83,7 @@ test('should not allow addon-config for unknown provider', async () => { description: '', }, 'test', + TEST_USER_ID, ); }).rejects.toThrow(ValidationError); }); @@ -98,12 +102,13 @@ test('should trigger simple-addon eventHandler', async () => { description: '', }; - await addonService.createAddon(config, 'me@mail.com'); + await addonService.createAddon(config, 'me@mail.com', TEST_USER_ID); // Feature toggle was created await eventService.storeEvent({ type: FEATURE_CREATED, - createdBy: 'some@user.com', + createdBy: SYSTEM_USER.username, + createdByUserId: SYSTEM_USER.id, data: { name: 'some-toggle', enabled: false, @@ -133,10 +138,11 @@ test('should not trigger event handler if project of event is different from add }, }; - await addonService.createAddon(config, 'me@mail.com'); + await addonService.createAddon(config, 'me@mail.com', TEST_USER_ID); await eventService.storeEvent({ type: FEATURE_CREATED, - createdBy: 'some@user.com', + createdBy: SYSTEM_USER.username, + createdByUserId: SYSTEM_USER.id, project: 'someotherproject', data: { name: 'some-toggle', @@ -166,10 +172,11 @@ test('should trigger event handler if project for event is one of the desired pr }, }; - await addonService.createAddon(config, 'me@mail.com'); + await addonService.createAddon(config, 'me@mail.com', TEST_USER_ID); await eventService.storeEvent({ type: FEATURE_CREATED, - createdBy: 'some@user.com', + createdBy: SYSTEM_USER.username, + createdByUserId: SYSTEM_USER.id, project: desiredProject, data: { name: 'some-toggle', @@ -179,7 +186,8 @@ test('should trigger event handler if project for event is one of the desired pr }); await eventService.storeEvent({ type: FEATURE_CREATED, - createdBy: 'some@user.com', + createdBy: SYSTEM_USER.username, + createdByUserId: SYSTEM_USER.id, project: otherProject, data: { name: 'other-toggle', @@ -211,10 +219,11 @@ test('should trigger events for multiple projects if addon is setup to filter mu }, }; - await addonService.createAddon(config, 'me@mail.com'); + await addonService.createAddon(config, 'me@mail.com', TEST_USER_ID); await eventService.storeEvent({ type: FEATURE_CREATED, - createdBy: 'some@user.com', + createdBy: SYSTEM_USER.username, + createdByUserId: SYSTEM_USER.id, project: desiredProjects[0], data: { name: 'some-toggle', @@ -224,7 +233,8 @@ test('should trigger events for multiple projects if addon is setup to filter mu }); await eventService.storeEvent({ type: FEATURE_CREATED, - createdBy: 'some@user.com', + createdBy: SYSTEM_USER.username, + createdByUserId: SYSTEM_USER.id, project: otherProject, data: { name: 'other-toggle', @@ -234,7 +244,8 @@ test('should trigger events for multiple projects if addon is setup to filter mu }); await eventService.storeEvent({ type: FEATURE_CREATED, - createdBy: 'some@user.com', + createdBy: SYSTEM_USER.username, + createdByUserId: SYSTEM_USER.id, project: desiredProjects[1], data: { name: 'third-toggle', @@ -269,10 +280,11 @@ test('should filter events on environment if addon is setup to filter for it', a }, }; - await addonService.createAddon(config, 'me@mail.com'); + await addonService.createAddon(config, 'me@mail.com', TEST_USER_ID); await eventService.storeEvent({ type: FEATURE_CREATED, - createdBy: 'some@user.com', + createdBy: SYSTEM_USER.username, + createdByUserId: SYSTEM_USER.id, project: desiredEnvironment, environment: desiredEnvironment, data: { @@ -283,7 +295,8 @@ test('should filter events on environment if addon is setup to filter for it', a }); await eventService.storeEvent({ type: FEATURE_CREATED, - createdBy: 'some@user.com', + createdBy: SYSTEM_USER.username, + createdByUserId: SYSTEM_USER.id, environment: otherEnvironment, data: { name: 'other-toggle', @@ -317,7 +330,8 @@ test('should not filter out global events (no specific environment) even if addo const globalEventWithNoEnvironment = { type: FEATURE_CREATED, - createdBy: 'some@user.com', + createdBy: SYSTEM_USER.username, + createdByUserId: SYSTEM_USER.id, project: 'some-project', data: { name: 'some-toggle', @@ -326,7 +340,7 @@ test('should not filter out global events (no specific environment) even if addo }, }; - await addonService.createAddon(config, 'me@mail.com'); + await addonService.createAddon(config, 'me@mail.com', TEST_USER_ID); await eventService.storeEvent(globalEventWithNoEnvironment); const simpleProvider = addonService.addonProviders.simple; // @ts-expect-error @@ -354,7 +368,8 @@ test('should not filter out global events (no specific project) even if addon is const globalEventWithNoProject = { type: FEATURE_CREATED, - createdBy: 'some@user.com', + createdBy: SYSTEM_USER.username, + createdByUserId: SYSTEM_USER.id, data: { name: 'some-toggle', enabled: false, @@ -362,7 +377,7 @@ test('should not filter out global events (no specific project) even if addon is }, }; - await addonService.createAddon(config, 'me@mail.com'); + await addonService.createAddon(config, 'me@mail.com', TEST_USER_ID); await eventService.storeEvent(globalEventWithNoProject); const simpleProvider = addonService.addonProviders.simple; // @ts-expect-error @@ -388,10 +403,11 @@ test('should support wildcard option for filtering addons', async () => { }, }; - await addonService.createAddon(config, 'me@mail.com'); + await addonService.createAddon(config, 'me@mail.com', TEST_USER_ID); await eventService.storeEvent({ type: FEATURE_CREATED, - createdBy: 'some@user.com', + createdBy: SYSTEM_USER.username, + createdByUserId: SYSTEM_USER.id, project: desiredProjects[0], data: { name: 'some-toggle', @@ -401,7 +417,8 @@ test('should support wildcard option for filtering addons', async () => { }); await eventService.storeEvent({ type: FEATURE_CREATED, - createdBy: 'some@user.com', + createdBy: SYSTEM_USER.username, + createdByUserId: SYSTEM_USER.id, project: otherProject, data: { name: 'other-toggle', @@ -411,7 +428,8 @@ test('should support wildcard option for filtering addons', async () => { }); await eventService.storeEvent({ type: FEATURE_CREATED, - createdBy: 'some@user.com', + createdBy: SYSTEM_USER.username, + createdByUserId: SYSTEM_USER.id, project: desiredProjects[1], data: { name: 'third-toggle', @@ -452,10 +470,11 @@ test('Should support filtering by both project and environment', async () => { 'desired-toggle2', 'desired-toggle3', ]; - await addonService.createAddon(config, 'me@mail.com'); + await addonService.createAddon(config, 'me@mail.com', TEST_USER_ID); await eventService.storeEvent({ type: FEATURE_CREATED, - createdBy: 'some@user.com', + createdBy: SYSTEM_USER.username, + createdByUserId: SYSTEM_USER.id, project: desiredProjects[0], environment: desiredEnvironments[0], data: { @@ -466,7 +485,8 @@ test('Should support filtering by both project and environment', async () => { }); await eventService.storeEvent({ type: FEATURE_CREATED, - createdBy: 'some@user.com', + createdBy: SYSTEM_USER.username, + createdByUserId: SYSTEM_USER.id, project: desiredProjects[0], environment: 'wrongenvironment', data: { @@ -477,7 +497,8 @@ test('Should support filtering by both project and environment', async () => { }); await eventService.storeEvent({ type: FEATURE_CREATED, - createdBy: 'some@user.com', + createdBy: SYSTEM_USER.username, + createdByUserId: SYSTEM_USER.id, project: desiredProjects[2], environment: desiredEnvironments[1], data: { @@ -488,7 +509,8 @@ test('Should support filtering by both project and environment', async () => { }); await eventService.storeEvent({ type: FEATURE_CREATED, - createdBy: 'some@user.com', + createdBy: SYSTEM_USER.username, + createdByUserId: SYSTEM_USER.id, project: desiredProjects[2], environment: desiredEnvironments[2], data: { @@ -499,7 +521,8 @@ test('Should support filtering by both project and environment', async () => { }); await eventService.storeEvent({ type: FEATURE_CREATED, - createdBy: 'some@user.com', + createdBy: SYSTEM_USER.username, + createdByUserId: SYSTEM_USER.id, project: 'wrongproject', environment: desiredEnvironments[0], data: { @@ -536,7 +559,7 @@ test('should create simple-addon config', async () => { description: '', }; - await addonService.createAddon(config, 'me@mail.com'); + await addonService.createAddon(config, 'me@mail.com', TEST_USER_ID); const addons = await addonService.getAddons(); expect(addons.length).toBe(1); @@ -557,7 +580,7 @@ test('should create tag type for simple-addon', async () => { description: '', }; - await addonService.createAddon(config, 'me@mail.com'); + await addonService.createAddon(config, 'me@mail.com', TEST_USER_ID); const tagType = await tagTypeService.getTagType('me'); expect(tagType.name).toBe('me'); @@ -577,7 +600,7 @@ test('should store ADDON_CONFIG_CREATE event', async () => { description: '', }; - await addonService.createAddon(config, 'me@mail.com'); + await addonService.createAddon(config, 'me@mail.com', TEST_USER_ID); const { events } = await eventService.getEvents(); @@ -600,10 +623,19 @@ test('should store ADDON_CONFIG_UPDATE event', async () => { events: [FEATURE_CREATED], }; - const addonConfig = await addonService.createAddon(config, 'me@mail.com'); + const addonConfig = await addonService.createAddon( + config, + 'me@mail.com', + TEST_USER_ID, + ); const updated = { ...addonConfig, description: 'test' }; - await addonService.updateAddon(addonConfig.id, updated, 'me@mail.com'); + await addonService.updateAddon( + addonConfig.id, + updated, + 'me@mail.com', + TEST_USER_ID, + ); const { events } = await eventService.getEvents(); @@ -626,9 +658,13 @@ test('should store ADDON_CONFIG_REMOVE event', async () => { events: [FEATURE_CREATED], }; - const addonConfig = await addonService.createAddon(config, 'me@mail.com'); + const addonConfig = await addonService.createAddon( + config, + 'me@mail.com', + TEST_USER_ID, + ); - await addonService.removeAddon(addonConfig.id, 'me@mail.com'); + await addonService.removeAddon(addonConfig.id, 'me@mail.com', TEST_USER_ID); const { events } = await eventService.getEvents(); @@ -652,7 +688,11 @@ test('should hide sensitive fields when fetching', async () => { events: [FEATURE_CREATED], }; - const createdConfig = await addonService.createAddon(config, 'me@mail.com'); + const createdConfig = await addonService.createAddon( + config, + 'me@mail.com', + TEST_USER_ID, + ); const addons = await addonService.getAddons(); const addonRetrieved = await addonService.getAddon(createdConfig.id); @@ -677,14 +717,23 @@ test('should not overwrite masked values when updating', async () => { description: '', }; - const addonConfig = await addonService.createAddon(config, 'me@mail.com'); + const addonConfig = await addonService.createAddon( + config, + 'me@mail.com', + TEST_USER_ID, + ); const updated = { ...addonConfig, parameters: { url: MASKED_VALUE, var: 'some-new-value' }, description: 'test', }; - await addonService.updateAddon(addonConfig.id, updated, 'me@mail.com'); + await addonService.updateAddon( + addonConfig.id, + updated, + 'me@mail.com', + TEST_USER_ID, + ); const updatedConfig = await stores.addonStore.get(addonConfig.id); // @ts-ignore @@ -707,7 +756,7 @@ test('should reject addon config with missing required parameter when creating', }; await expect(async () => - addonService.createAddon(config, 'me@mail.com'), + addonService.createAddon(config, 'me@mail.com', TEST_USER_ID), ).rejects.toThrow(ValidationError); }); @@ -725,14 +774,23 @@ test('should reject updating addon config with missing required parameter', asyn description: '', }; - const config = await addonService.createAddon(addonConfig, 'me@mail.com'); + const config = await addonService.createAddon( + addonConfig, + 'me@mail.com', + TEST_USER_ID, + ); const updated = { ...config, parameters: { var: 'some-new-value' }, description: 'test', }; await expect(async () => - addonService.updateAddon(config.id, updated, 'me@mail.com'), + addonService.updateAddon( + config.id, + updated, + 'me@mail.com', + TEST_USER_ID, + ), ).rejects.toThrow(ValidationError); }); @@ -751,6 +809,6 @@ test('Should reject addon config if a required parameter is just the empty strin }; await expect(async () => - addonService.createAddon(config, 'me@mail.com'), + addonService.createAddon(config, 'me@mail.com', TEST_USER_ID), ).rejects.toThrow(ValidationError); }); diff --git a/src/lib/services/addon-service.ts b/src/lib/services/addon-service.ts index 622c34afa631..c063e43f8fc4 100644 --- a/src/lib/services/addon-service.ts +++ b/src/lib/services/addon-service.ts @@ -8,7 +8,7 @@ import { IFeatureToggleStore } from '../features/feature-toggle/types/feature-to import { Logger } from '../logger'; import TagTypeService from '../features/tag-type/tag-type-service'; import { IAddon, IAddonDto, IAddonStore } from '../types/stores/addon-store'; -import { IUnleashStores, IUnleashConfig } from '../types'; +import { IUnleashStores, IUnleashConfig, SYSTEM_USER } from '../types'; import { IAddonDefinition } from '../types/model'; import { minutesToMilliseconds } from 'date-fns'; import EventService from './event-service'; @@ -179,6 +179,7 @@ export default class AddonService { await this.tagTypeService.createTagType( tagType, providerName, + SYSTEM_USER.id, ); } catch (err) { if (!(err instanceof NameExistsError)) { @@ -191,7 +192,11 @@ export default class AddonService { return Promise.resolve(); } - async createAddon(data: IAddonDto, userName: string): Promise { + async createAddon( + data: IAddonDto, + userName: string, + userId: number, + ): Promise { const addonConfig = await addonSchema.validateAsync(data); await this.validateKnownProvider(addonConfig); await this.validateRequiredParameters(addonConfig); @@ -206,6 +211,7 @@ export default class AddonService { await this.eventService.storeEvent({ type: events.ADDON_CONFIG_CREATED, createdBy: userName, + createdByUserId: userId, data: omitKeys(createdAddon, 'parameters'), }); @@ -216,6 +222,7 @@ export default class AddonService { id: number, data: IAddonDto, userName: string, + userId: number, ): Promise { const existingConfig = await this.addonStore.get(id); // because getting an early 404 here makes more sense const addonConfig = await addonSchema.validateAsync(data); @@ -239,6 +246,7 @@ export default class AddonService { await this.eventService.storeEvent({ type: events.ADDON_CONFIG_UPDATED, createdBy: userName, + createdByUserId: userId, preData: omitKeys(existingConfig, 'parameters'), data: omitKeys(result, 'parameters'), }); @@ -246,12 +254,17 @@ export default class AddonService { return result; } - async removeAddon(id: number, userName: string): Promise { + async removeAddon( + id: number, + userName: string, + removedByuserId: number, + ): Promise { const existingConfig = await this.addonStore.get(id); await this.addonStore.delete(id); await this.eventService.storeEvent({ type: events.ADDON_CONFIG_DELETED, createdBy: userName, + createdByUserId: removedByuserId, preData: omitKeys(existingConfig, 'parameters'), }); this.logger.info(`User ${userName} removed addon ${id}`); diff --git a/src/lib/services/api-token-service.test.ts b/src/lib/services/api-token-service.test.ts index cf8c5ab1bae9..49c8af45f759 100644 --- a/src/lib/services/api-token-service.test.ts +++ b/src/lib/services/api-token-service.test.ts @@ -62,7 +62,7 @@ test("Shouldn't return frontend token when secret is undefined", async () => { secret: '*:*:some-random-string', type: ApiTokenType.FRONTEND, tokenName: 'front', - expiresAt: null, + expiresAt: undefined, }; const config: IUnleashConfig = createTestConfig({}); @@ -94,7 +94,6 @@ test("Shouldn't return frontend token when secret is undefined", async () => { await apiTokenService.createApiTokenWithProjects(token); await apiTokenService.fetchActiveTokens(); - expect(apiTokenService.getUserForToken(undefined)).toEqual(undefined); expect(apiTokenService.getUserForToken('')).toEqual(undefined); }); @@ -105,7 +104,7 @@ test('Api token operations should all have events attached', async () => { secret: '*:*:some-random-string', type: ApiTokenType.FRONTEND, tokenName: 'front', - expiresAt: null, + expiresAt: undefined, }; const config: IUnleashConfig = createTestConfig({}); @@ -135,8 +134,8 @@ test('Api token operations should all have events attached', async () => { ); const saved = await apiTokenService.createApiTokenWithProjects(token); const newExpiry = addDays(new Date(), 30); - await apiTokenService.updateExpiry(saved.secret, newExpiry, 'test'); - await apiTokenService.delete(saved.secret, 'test'); + await apiTokenService.updateExpiry(saved.secret, newExpiry, 'test', -9999); + await apiTokenService.delete(saved.secret, 'test', -9999); const { events } = await eventService.getEvents(); const createdApiTokenEvents = events.filter( (e) => e.type === API_TOKEN_CREATED, diff --git a/src/lib/services/api-token-service.ts b/src/lib/services/api-token-service.ts index da968332da0e..a7bb93970078 100644 --- a/src/lib/services/api-token-service.ts +++ b/src/lib/services/api-token-service.ts @@ -23,6 +23,8 @@ import { ApiTokenCreatedEvent, ApiTokenDeletedEvent, ApiTokenUpdatedEvent, + SYSTEM_USER, + SYSTEM_USER_ID, } from '../types'; import { omitKeys } from '../util'; import EventService from './event-service'; @@ -114,7 +116,13 @@ export class ApiTokenService { try { const createAll = tokens .map(mapLegacyTokenWithSecret) - .map((t) => this.insertNewApiToken(t, 'init-api-tokens')); + .map((t) => + this.insertNewApiToken( + t, + 'init-api-tokens', + SYSTEM_USER_ID, + ), + ); await Promise.all(createAll); } catch (e) { this.logger.error('Unable to create initial Admin API tokens'); @@ -162,12 +170,14 @@ export class ApiTokenService { secret: string, expiresAt: Date, updatedBy: string, + updatedById: number, ): Promise { const previous = await this.store.get(secret); const token = await this.store.setExpiry(secret, expiresAt); await this.eventService.storeEvent( new ApiTokenUpdatedEvent({ createdBy: updatedBy, + createdByUserId: updatedById, previousToken: omitKeys(previous, 'secret'), apiToken: omitKeys(token, 'secret'), }), @@ -175,13 +185,18 @@ export class ApiTokenService { return token; } - public async delete(secret: string, deletedBy: string): Promise { + public async delete( + secret: string, + deletedBy: string, + deletedByUserId: number, + ): Promise { if (await this.store.exists(secret)) { const token = await this.store.get(secret); await this.store.delete(secret); await this.eventService.storeEvent( new ApiTokenDeletedEvent({ createdBy: deletedBy, + createdByUserId: deletedByUserId, apiToken: omitKeys(token, 'secret'), }), ); @@ -193,15 +208,21 @@ export class ApiTokenService { */ public async createApiToken( newToken: Omit, - createdBy: string = 'unleash-system', + createdBy: string = SYSTEM_USER.username, + createdByUserId: number = SYSTEM_USER.id, ): Promise { const token = mapLegacyToken(newToken); - return this.createApiTokenWithProjects(token, createdBy); + return this.createApiTokenWithProjects( + token, + createdBy, + createdByUserId, + ); } public async createApiTokenWithProjects( newToken: Omit, - createdBy: string = 'unleash-system', + createdBy: string = SYSTEM_USER.username, + createdByUserId: number = SYSTEM_USER.id, ): Promise { validateApiToken(newToken); const environments = await this.environmentStore.getAll(); @@ -209,7 +230,11 @@ export class ApiTokenService { const secret = this.generateSecretKey(newToken); const createNewToken = { ...newToken, secret }; - return this.insertNewApiToken(createNewToken, createdBy); + return this.insertNewApiToken( + createNewToken, + createdBy, + createdByUserId, + ); } // TODO: Remove this service method after embedded proxy has been released in @@ -221,12 +246,17 @@ export class ApiTokenService { const secret = this.generateSecretKey(newToken); const createNewToken = { ...newToken, secret }; - return this.insertNewApiToken(createNewToken, 'system-migration'); + return this.insertNewApiToken( + createNewToken, + 'system-migration', + SYSTEM_USER_ID, + ); } private async insertNewApiToken( newApiToken: IApiTokenCreate, createdBy: string, + createdByUserId: number, ): Promise { try { const token = await this.store.insert(newApiToken); @@ -234,6 +264,7 @@ export class ApiTokenService { await this.eventService.storeEvent( new ApiTokenCreatedEvent({ createdBy, + createdByUserId, apiToken: omitKeys(token, 'secret'), }), ); diff --git a/src/lib/services/client-metrics/instance-service.ts b/src/lib/services/client-metrics/instance-service.ts index 7b31d891a615..c350afb7c86e 100644 --- a/src/lib/services/client-metrics/instance-service.ts +++ b/src/lib/services/client-metrics/instance-service.ts @@ -14,18 +14,18 @@ import { IApplicationQuery } from '../../types/query'; import { IClientApp } from '../../types/model'; import { clientRegisterSchema } from './schema'; -import { minutesToMilliseconds, secondsToMilliseconds } from 'date-fns'; import { IClientMetricsStoreV2 } from '../../types/stores/client-metrics-store-v2'; import { clientMetricsSchema } from './schema'; import { PartialSome } from '../../types/partial'; import { IPrivateProjectChecker } from '../../features/private-project/privateProjectCheckerType'; -import { IFlagResolver } from '../../types'; +import { IFlagResolver, SYSTEM_USER } from '../../types'; import { ALL_PROJECTS } from '../../util'; +import { Logger } from '../../logger'; export default class ClientInstanceService { apps = {}; - logger = null; + logger: Logger; seenClients: Record = {}; @@ -112,8 +112,9 @@ export default class ClientInstanceService { if (appsToAnnounce.length > 0) { const events = appsToAnnounce.map((app) => ({ type: APPLICATION_CREATED, - createdBy: app.createdBy || 'unknown', + createdBy: app.createdBy || SYSTEM_USER.username, data: app, + createdByUserId: app.createdByUserId || SYSTEM_USER.id, })); await this.eventStore.batchStore(events); } @@ -132,7 +133,7 @@ export default class ClientInstanceService { this.clientInstanceStore ) { const uniqueRegistrations = Object.values(this.seenClients); - const uniqueApps = Object.values( + const uniqueApps: Partial[] = Object.values( uniqueRegistrations.reduce((soFar, reg) => { // eslint-disable-next-line no-param-reassign soFar[reg.appName] = reg; diff --git a/src/lib/services/client-metrics/last-seen/tests/last-seen-service.e2e.test.ts b/src/lib/services/client-metrics/last-seen/tests/last-seen-service.e2e.test.ts index 91614c1c46a7..226f88df6161 100644 --- a/src/lib/services/client-metrics/last-seen/tests/last-seen-service.e2e.test.ts +++ b/src/lib/services/client-metrics/last-seen/tests/last-seen-service.e2e.test.ts @@ -44,6 +44,7 @@ test('should clean unknown feature toggle names from last seen store', async () 'default', { name: featureName }, 'user', + -9999, ), ), ); @@ -99,6 +100,7 @@ test('should clean unknown feature toggle environments from last seen store', as 'default', { name: feature.name }, 'user', + -9999, ), ), ); diff --git a/src/lib/services/context-service.ts b/src/lib/services/context-service.ts index 6ad41b332554..e934718a7910 100644 --- a/src/lib/services/context-service.ts +++ b/src/lib/services/context-service.ts @@ -110,6 +110,7 @@ class ContextService { async createContextField( value: IContextFieldDto, userName: string, + createdByUserId: number, ): Promise { // validations await this.validateUniqueName(value); @@ -120,6 +121,7 @@ class ContextService { await this.eventService.storeEvent({ type: CONTEXT_FIELD_CREATED, createdBy: userName, + createdByUserId, data: contextField, }); @@ -129,6 +131,7 @@ class ContextService { async updateContextField( updatedContextField: IContextFieldDto, userName: string, + updatedByUserId: number, ): Promise { const contextField = await this.contextFieldStore.get( updatedContextField.name, @@ -140,12 +143,17 @@ class ContextService { await this.eventService.storeEvent({ type: CONTEXT_FIELD_UPDATED, createdBy: userName, + createdByUserId: updatedByUserId, preData: contextField, data: value, }); } - async deleteContextField(name: string, userName: string): Promise { + async deleteContextField( + name: string, + userName: string, + deletedByUserId: number, + ): Promise { const contextField = await this.contextFieldStore.get(name); // delete @@ -153,6 +161,7 @@ class ContextService { await this.eventService.storeEvent({ type: CONTEXT_FIELD_DELETED, createdBy: userName, + createdByUserId: deletedByUserId, preData: contextField, }); } diff --git a/src/lib/services/favorites-service.ts b/src/lib/services/favorites-service.ts index 5639a823dd92..515038bd356f 100644 --- a/src/lib/services/favorites-service.ts +++ b/src/lib/services/favorites-service.ts @@ -65,6 +65,7 @@ export class FavoritesService { type: FEATURE_FAVORITED, featureName: feature, createdBy: extractUsernameFromUser(user), + createdByUserId: user.id, data: { feature, }, @@ -84,6 +85,7 @@ export class FavoritesService { type: FEATURE_UNFAVORITED, featureName: feature, createdBy: extractUsernameFromUser(user), + createdByUserId: user.id, data: { feature, }, @@ -102,6 +104,7 @@ export class FavoritesService { await this.eventService.storeEvent({ type: PROJECT_FAVORITED, createdBy: extractUsernameFromUser(user), + createdByUserId: user.id, data: { project, }, @@ -120,6 +123,7 @@ export class FavoritesService { await this.eventService.storeEvent({ type: PROJECT_UNFAVORITED, createdBy: extractUsernameFromUser(user), + createdByUserId: user.id, data: { project, }, diff --git a/src/lib/services/feature-tag-service.ts b/src/lib/services/feature-tag-service.ts index 96380562f69b..0205729563cb 100644 --- a/src/lib/services/feature-tag-service.ts +++ b/src/lib/services/feature-tag-service.ts @@ -56,15 +56,17 @@ class FeatureTagService { featureName: string, tag: ITag, userName: string, + addedByUserId: number, ): Promise { const featureToggle = await this.featureToggleStore.get(featureName); const validatedTag = await tagSchema.validateAsync(tag); - await this.createTagIfNeeded(validatedTag, userName); + await this.createTagIfNeeded(validatedTag, userName, addedByUserId); await this.featureTagStore.tagFeature(featureName, validatedTag); await this.eventService.storeEvent({ type: FEATURE_TAGGED, createdBy: userName, + createdByUserId: addedByUserId, featureName, project: featureToggle.project, data: validatedTag, @@ -77,11 +79,14 @@ class FeatureTagService { addedTags: ITag[], removedTags: ITag[], userName: string, + updatedByUserId: number, ): Promise { const featureToggles = await this.featureToggleStore.getAllByNames(featureNames); await Promise.all( - addedTags.map((tag) => this.createTagIfNeeded(tag, userName)), + addedTags.map((tag) => + this.createTagIfNeeded(tag, userName, updatedByUserId), + ), ); const createdFeatureTags: IFeatureTag[] = featureNames.flatMap( (featureName) => @@ -112,6 +117,7 @@ class FeatureTagService { featureName: featureToggle.name, project: featureToggle.project, data: addedTag, + createdByUserId: updatedByUserId, })), ); @@ -122,6 +128,7 @@ class FeatureTagService { featureName: featureToggle.name, project: featureToggle.project, preData: removedTag, + createdByUserId: updatedByUserId, })), ); @@ -131,7 +138,11 @@ class FeatureTagService { ]); } - async createTagIfNeeded(tag: ITag, userName: string): Promise { + async createTagIfNeeded( + tag: ITag, + userName: string, + createdByUserId: number, + ): Promise { try { await this.tagStore.getTag(tag.type, tag.value); } catch (error) { @@ -141,6 +152,7 @@ class FeatureTagService { await this.eventService.storeEvent({ type: TAG_CREATED, createdBy: userName, + createdByUserId, data: tag, }); } catch (err) { @@ -159,6 +171,7 @@ class FeatureTagService { featureName: string, tag: ITag, userName: string, + removedByUserId: number, ): Promise { const featureToggle = await this.featureToggleStore.get(featureName); const tags = @@ -167,6 +180,7 @@ class FeatureTagService { await this.eventService.storeEvent({ type: FEATURE_UNTAGGED, createdBy: userName, + createdByUserId: removedByUserId, featureName, project: featureToggle.project, preData: tag, diff --git a/src/lib/services/feature-type-service.ts b/src/lib/services/feature-type-service.ts index 1827e28a4509..b5d5b8d53966 100644 --- a/src/lib/services/feature-type-service.ts +++ b/src/lib/services/feature-type-service.ts @@ -57,6 +57,7 @@ export default class FeatureTypeService { await this.eventService.storeEvent({ type: FEATURE_TYPE_UPDATED, createdBy: extractUsernameFromUser(user), + createdByUserId: user.id, data: { ...featureType, lifetimeDays: translatedLifetime }, preData: featureType, }); diff --git a/src/lib/services/group-service.ts b/src/lib/services/group-service.ts index c0264f14e77a..26920c7bd996 100644 --- a/src/lib/services/group-service.ts +++ b/src/lib/services/group-service.ts @@ -94,6 +94,7 @@ export class GroupService { async createGroup( group: ICreateGroupModel, userName: string, + createdByUserId: number, ): Promise { await this.validateGroup(group); @@ -111,13 +112,18 @@ export class GroupService { await this.eventService.storeEvent({ type: GROUP_CREATED, createdBy: userName, + createdByUserId, data: { ...group, users: newUserIds }, }); return newGroup; } - async updateGroup(group: IGroupModel, userName: string): Promise { + async updateGroup( + group: IGroupModel, + userName: string, + createdByUserId: number, + ): Promise { const existingGroup = await this.groupStore.get(group.id); await this.validateGroup(group, existingGroup); @@ -149,6 +155,7 @@ export class GroupService { await this.eventService.storeEvent({ type: GROUP_UPDATED, createdBy: userName, + createdByUserId, data: { ...newGroup, users: newUserIds }, preData: { ...existingGroup, users: existingUserIds }, }); @@ -183,7 +190,11 @@ export class GroupService { return []; } - async deleteGroup(id: number, userName: string): Promise { + async deleteGroup( + id: number, + userName: string, + createdByUserId: number, + ): Promise { const group = await this.groupStore.get(id); const existingUsers = await this.groupStore.getAllUsersByGroups([ @@ -196,6 +207,7 @@ export class GroupService { await this.eventService.storeEvent({ type: GROUP_DELETED, createdBy: userName, + createdByUserId, preData: { ...group, users: existingUserIds }, }); } @@ -219,33 +231,11 @@ export class GroupService { return this.groupStore.getProjectGroupRoles(projectId); } - private mapGroupWithUsers( - group: IGroup, - allGroupUsers: IGroupUser[], - allUsers: IUser[], - ): IGroupModel { - const groupUsers = allGroupUsers.filter( - (user) => user.groupId === group.id, - ); - const groupUsersId = groupUsers.map((user) => user.userId); - const selectedUsers = allUsers.filter((user) => - groupUsersId.includes(user.id), - ); - const finalUsers = selectedUsers.map((user) => { - const roleUser = groupUsers.find((gu) => gu.userId === user.id); - return { - user: user, - joinedAt: roleUser?.joinedAt, - createdBy: roleUser?.createdBy, - }; - }); - return { ...group, users: finalUsers }; - } - async syncExternalGroups( userId: number, externalGroups: string[], createdBy?: string, + createdByUserId?: number, ): Promise { if (Array.isArray(externalGroups)) { const newGroups = await this.groupStore.getNewGroupsForExternalUser( @@ -268,6 +258,7 @@ export class GroupService { events.push({ type: GROUP_USER_ADDED, createdBy: createdBy ?? 'unknown', + createdByUserId: createdByUserId ?? -9999, data: { groupId: group.id, userId, @@ -279,6 +270,7 @@ export class GroupService { events.push({ type: GROUP_USER_REMOVED, createdBy: createdBy ?? 'unknown', + createdByUserId: createdByUserId ?? -9999, preData: { groupId: group.groupId, userId, @@ -290,6 +282,29 @@ export class GroupService { } } + private mapGroupWithUsers( + group: IGroup, + allGroupUsers: IGroupUser[], + allUsers: IUser[], + ): IGroupModel { + const groupUsers = allGroupUsers.filter( + (user) => user.groupId === group.id, + ); + const groupUsersId = groupUsers.map((user) => user.userId); + const selectedUsers = allUsers.filter((user) => + groupUsersId.includes(user.id), + ); + const finalUsers = selectedUsers.map((user) => { + const roleUser = groupUsers.find((gu) => gu.userId === user.id); + return { + user: user, + joinedAt: roleUser?.joinedAt, + createdBy: roleUser?.createdBy, + }; + }); + return { ...group, users: finalUsers }; + } + async getGroupsForUser(userId: number): Promise { return this.groupStore.getGroupsForUser(userId); } diff --git a/src/lib/services/pat-service.ts b/src/lib/services/pat-service.ts index f02bfedbaa44..3bdcb108d0e0 100644 --- a/src/lib/services/pat-service.ts +++ b/src/lib/services/pat-service.ts @@ -45,6 +45,7 @@ export default class PatService { await this.eventService.storeEvent({ type: PAT_CREATED, createdBy: editor.email || editor.username, + createdByUserId: editor.id, data: pat, }); @@ -66,6 +67,7 @@ export default class PatService { await this.eventService.storeEvent({ type: PAT_DELETED, createdBy: editor.email || editor.username, + createdByUserId: editor.id, data: pat, }); diff --git a/src/lib/services/project-service.ts b/src/lib/services/project-service.ts index 4674e65bda0a..d83a2e2c76bc 100644 --- a/src/lib/services/project-service.ts +++ b/src/lib/services/project-service.ts @@ -41,6 +41,7 @@ import { CreateProject, IProjectUpdate, IProjectHealth, + SYSTEM_USER, } from '../types'; import { IProjectQuery, @@ -255,6 +256,7 @@ export default class ProjectService { await this.eventService.storeEvent({ type: PROJECT_CREATED, createdBy: getCreatedBy(user), + createdByUserId: user.id, data, project: newProject.id, }); @@ -277,6 +279,7 @@ export default class ProjectService { type: PROJECT_UPDATED, project: updatedProject.id, createdBy: getCreatedBy(user), + createdByUserId: user.id, data: afterData, preData, }); @@ -300,6 +303,7 @@ export default class ProjectService { type: PROJECT_UPDATED, project: updatedProject.id, createdBy: getCreatedBy(user), + createdByUserId: user.id, data: { ...preData, ...updatedProject }, preData, }); @@ -363,6 +367,7 @@ export default class ProjectService { featureName, newProjectId, getCreatedBy(user), + user.id, ); await this.featureToggleService.updateFeatureStrategyProject( featureName, @@ -399,6 +404,7 @@ export default class ProjectService { archivedToggles.map((toggle) => toggle.name), id, user.name, + user.id, ); await this.projectStore.delete(id); @@ -407,6 +413,7 @@ export default class ProjectService { type: PROJECT_DELETED, createdBy: getCreatedBy(user), project: id, + createdByUserId: user.id, }); await this.accessService.removeDefaultProjectRoles(user, id); @@ -460,7 +467,8 @@ export default class ProjectService { await this.eventService.storeEvent( new ProjectUserAddedEvent({ project: projectId, - createdBy: createdBy || 'system-user', + createdBy: createdBy || SYSTEM_USER.username, + createdByUserId: user.id || SYSTEM_USER.id, data: { roleId, userId, @@ -479,6 +487,7 @@ export default class ProjectService { roleId: number, userId: number, createdBy: string, + createdByUserId: number, ): Promise { const role = await this.findProjectRole(projectId, roleId); @@ -492,6 +501,7 @@ export default class ProjectService { new ProjectUserRemovedEvent({ project: projectId, createdBy, + createdByUserId, preData: { roleId, userId, @@ -506,6 +516,7 @@ export default class ProjectService { projectId: string, userId: number, createdBy: string, + createdByUserId: number, ): Promise { const existingRoles = await this.accessService.getProjectRolesForUser( projectId, @@ -526,6 +537,7 @@ export default class ProjectService { new ProjectAccessUserRolesDeleted({ project: projectId, createdBy, + createdByUserId, preData: { roles: existingRoles, userId, @@ -538,6 +550,7 @@ export default class ProjectService { projectId: string, groupId: number, createdBy: string, + createdByUserId: number, ): Promise { const existingRoles = await this.accessService.getProjectRolesForGroup( projectId, @@ -558,6 +571,7 @@ export default class ProjectService { new ProjectAccessUserRolesDeleted({ project: projectId, createdBy, + createdByUserId, preData: { roles: existingRoles, groupId, @@ -571,6 +585,7 @@ export default class ProjectService { roleId: number, groupId: number, modifiedBy: string, + modifiedById: number, ): Promise { const role = await this.accessService.getRole(roleId); const group = await this.groupService.getGroup(groupId); @@ -593,6 +608,7 @@ export default class ProjectService { new ProjectGroupAddedEvent({ project: project.id, createdBy: modifiedBy, + createdByUserId: modifiedById, data: { groupId: group.id, projectId: project.id, @@ -610,6 +626,7 @@ export default class ProjectService { roleId: number, groupId: number, modifiedBy: string, + modifiedById: number, ): Promise { const group = await this.groupService.getGroup(groupId); const role = await this.accessService.getRole(roleId); @@ -633,6 +650,7 @@ export default class ProjectService { new ProjectGroupRemovedEvent({ project: projectId, createdBy: modifiedBy, + createdByUserId: modifiedById, preData: { groupId: group.id, projectId: project.id, @@ -647,6 +665,7 @@ export default class ProjectService { roleId: number, usersAndGroups: IProjectAccessModel, createdBy: string, + createdByUserId: number, ): Promise { await this.accessService.addRoleAccessToProject( usersAndGroups.users, @@ -660,6 +679,7 @@ export default class ProjectService { new ProjectAccessAddedEvent({ project: projectId, createdBy, + createdByUserId, data: { roleId, groups: usersAndGroups.groups.map(({ id }) => id), @@ -675,6 +695,7 @@ export default class ProjectService { groups: number[], users: number[], createdBy: string, + createdByUserId: number, ): Promise { await this.accessService.addAccessToProject( roles, @@ -688,6 +709,7 @@ export default class ProjectService { new ProjectAccessAddedEvent({ project: projectId, createdBy, + createdByUserId, data: { roles, groups, @@ -702,6 +724,7 @@ export default class ProjectService { userId: number, newRoles: number[], createdByUserName: string, + createdByUserId: number, ): Promise { const currentRoles = await this.accessService.getProjectRolesForUser( projectId, @@ -727,6 +750,7 @@ export default class ProjectService { new ProjectAccessUserRolesUpdated({ project: projectId, createdBy: createdByUserName, + createdByUserId, data: { roles: newRoles, userId, @@ -744,6 +768,7 @@ export default class ProjectService { groupId: number, newRoles: number[], createdBy: string, + createdByUserId: number, ): Promise { const currentRoles = await this.accessService.getProjectRolesForGroup( projectId, @@ -769,6 +794,7 @@ export default class ProjectService { new ProjectAccessGroupRolesUpdated({ project: projectId, createdBy, + createdByUserId, data: { roles: newRoles, groupId, @@ -863,6 +889,7 @@ export default class ProjectService { roleId: number, userId: number, createdBy: string, + createdByUserId: number, ): Promise { const usersWithRoles = await this.getAccessToProject(projectId); const user = usersWithRoles.users.find((u) => u.id === userId); @@ -896,6 +923,7 @@ export default class ProjectService { new ProjectUserUpdateRoleEvent({ project: projectId, createdBy, + createdByUserId, preData: { userId, roleId: currentRole.id, @@ -917,6 +945,7 @@ export default class ProjectService { roleId: number, userId: number, createdBy: string, + createdByUserId: number, ): Promise { const usersWithRoles = await this.getAccessToProject(projectId); const user = usersWithRoles.groups.find((u) => u.id === userId); @@ -949,6 +978,7 @@ export default class ProjectService { new ProjectGroupUpdateRoleEvent({ project: projectId, createdBy, + createdByUserId, preData: { userId, roleId: currentRole.id, diff --git a/src/lib/services/proxy-service.ts b/src/lib/services/proxy-service.ts index 7bcb64862db9..3345d92af7c2 100644 --- a/src/lib/services/proxy-service.ts +++ b/src/lib/services/proxy-service.ts @@ -162,6 +162,7 @@ export class ProxyService { async setFrontendSettings( value: FrontendSettings, createdBy: string, + createdByUserId: number, ): Promise { const error = validateOrigins(value.frontendApiOrigins); if (error) { @@ -171,6 +172,7 @@ export class ProxyService { frontendSettingsKey, value, createdBy, + createdByUserId, false, ); } diff --git a/src/lib/services/public-signup-token-service.ts b/src/lib/services/public-signup-token-service.ts index 57010297977d..f7f3246cd9f8 100644 --- a/src/lib/services/public-signup-token-service.ts +++ b/src/lib/services/public-signup-token-service.ts @@ -1,6 +1,6 @@ import crypto from 'crypto'; import { Logger } from '../logger'; -import { IUnleashConfig, IUnleashStores } from '../types'; +import { IUnleashConfig, IUnleashStores, SYSTEM_USER } from '../types'; import { IPublicSignupTokenStore } from '../types/stores/public-signup-token-store'; import { PublicSignupTokenSchema } from '../openapi/spec/public-signup-token-schema'; import { IRoleStore } from '../types/stores/role-store'; @@ -77,11 +77,13 @@ export class PublicSignupTokenService { secret: string, { expiresAt, enabled }: { expiresAt?: Date; enabled?: boolean }, createdBy: string, + createdByUserId: number, ): Promise { const result = await this.store.update(secret, { expiresAt, enabled }); await this.eventService.storeEvent( new PublicSignupTokenUpdatedEvent({ createdBy, + createdByUserId, data: { secret, enabled, expiresAt }, }), ); @@ -100,7 +102,8 @@ export class PublicSignupTokenService { await this.store.addTokenUser(secret, user.id); await this.eventService.storeEvent( new PublicSignupTokenUserAddedEvent({ - createdBy: 'System', + createdBy: SYSTEM_USER.username, + createdByUserId: SYSTEM_USER.id, data: { secret, userId: user.id }, }), ); @@ -110,6 +113,7 @@ export class PublicSignupTokenService { public async createNewPublicSignupToken( tokenCreate: PublicSignupTokenCreateSchema, createdBy: string, + createdByUserId: number, ): Promise { const viewerRole = await this.roleStore.getRoleByName(RoleName.VIEWER); const secret = this.generateSecretKey(); @@ -131,6 +135,7 @@ export class PublicSignupTokenService { await this.eventService.storeEvent( new PublicSignupTokenCreatedEvent({ createdBy: createdBy, + createdByUserId, data: token, }), ); diff --git a/src/lib/services/segment-service.ts b/src/lib/services/segment-service.ts index 7565a29ab239..f4c844117458 100644 --- a/src/lib/services/segment-service.ts +++ b/src/lib/services/segment-service.ts @@ -4,6 +4,7 @@ import { IFlagResolver, IUnleashStores, SKIP_CHANGE_REQUEST, + SYSTEM_USER, } from '../types'; import { Logger } from '../logger'; import NameExistsError from '../error/name-exists-error'; @@ -143,7 +144,7 @@ export class SegmentService implements ISegmentService { async create( data: unknown, - user: Partial>, + user: Partial>, ): Promise { const input = await segmentSchema.validateAsync(data); this.validateSegmentValuesLimit(input); @@ -152,7 +153,8 @@ export class SegmentService implements ISegmentService { await this.eventService.storeEvent({ type: SEGMENT_CREATED, - createdBy: user.email || user.username || 'unknown', + createdBy: user.email || user.username || SYSTEM_USER.username, + createdByUserId: user.id || SYSTEM_USER.id, data: segment, project: segment.project, }); @@ -186,6 +188,7 @@ export class SegmentService implements ISegmentService { await this.eventService.storeEvent({ type: SEGMENT_UPDATED, createdBy: user.email || user.username || 'unknown', + createdByUserId: user.id, data: segment, preData, project: segment.project, @@ -199,6 +202,7 @@ export class SegmentService implements ISegmentService { await this.eventService.storeEvent({ type: SEGMENT_DELETED, createdBy: user.email || user.username, + createdByUserId: user.id, preData: segment, project: segment.project, }); @@ -210,6 +214,7 @@ export class SegmentService implements ISegmentService { await this.eventService.storeEvent({ type: SEGMENT_DELETED, createdBy: user.email || user.username, + createdByUserId: user.id, preData: segment, }); } diff --git a/src/lib/services/setting-service.ts b/src/lib/services/setting-service.ts index 04711d859e09..6cc45c4c61f0 100644 --- a/src/lib/services/setting-service.ts +++ b/src/lib/services/setting-service.ts @@ -46,6 +46,7 @@ export default class SettingService { id: string, value: object, createdBy: string, + createdByUserId: number, hideEventDetails: boolean = true, ): Promise { const existingSettings = await this.settingStore.get(id); @@ -65,6 +66,7 @@ export default class SettingService { { createdBy, data, + createdByUserId, }, preData, ), @@ -73,6 +75,7 @@ export default class SettingService { await this.settingStore.insert(id, value); await this.eventService.storeEvent( new SettingCreatedEvent({ + createdByUserId, createdBy, data, }), @@ -80,10 +83,15 @@ export default class SettingService { } } - async delete(id: string, createdBy: string): Promise { + async delete( + id: string, + createdBy: string, + createdByUserId: number, + ): Promise { await this.settingStore.delete(id); await this.eventService.storeEvent( new SettingDeletedEvent({ + createdByUserId, createdBy, data: { id, diff --git a/src/lib/services/state-service.test.ts b/src/lib/services/state-service.test.ts index 366d74599359..c068d396eabe 100644 --- a/src/lib/services/state-service.test.ts +++ b/src/lib/services/state-service.test.ts @@ -14,6 +14,7 @@ import { import { GLOBAL_ENV } from '../types/environment'; import variantsExportV3 from '../../test/examples/variantsexport_v3.json'; import EventService from './event-service'; +import { SYSTEM_USER_ID } from '../types'; const oldExportExample = require('./state-service-export-v1.json'); function getSetup() { @@ -93,7 +94,7 @@ test('should import a feature', async () => { ], }; - await stateService.import({ data }); + await stateService.import({ userId: SYSTEM_USER_ID, data }); const events = await stores.eventStore.getEvents(); expect(events).toHaveLength(1); @@ -116,7 +117,11 @@ test('should not import an existing feature', async () => { await stores.featureToggleStore.create('default', data.features[0]); - await stateService.import({ data, keepExisting: true }); + await stateService.import({ + data, + keepExisting: true, + userId: SYSTEM_USER_ID, + }); const events = await stores.eventStore.getEvents(); expect(events).toHaveLength(0); @@ -141,6 +146,7 @@ test('should not keep existing feature if drop-before-import', async () => { data, keepExisting: true, dropBeforeImport: true, + userId: SYSTEM_USER_ID, }); const events = await stores.eventStore.getEvents(); @@ -162,7 +168,11 @@ test('should drop feature before import if specified', async () => { ], }; - await stateService.import({ data, dropBeforeImport: true }); + await stateService.import({ + data, + dropBeforeImport: true, + userId: SYSTEM_USER_ID, + }); const events = await stores.eventStore.getEvents(); expect(events).toHaveLength(2); @@ -183,7 +193,7 @@ test('should import a strategy', async () => { ], }; - await stateService.import({ data }); + await stateService.import({ userId: SYSTEM_USER_ID, data }); const events = await stores.eventStore.getEvents(); expect(events).toHaveLength(1); @@ -205,7 +215,11 @@ test('should not import an existing strategy', async () => { await stores.strategyStore.createStrategy(data.strategies[0]); - await stateService.import({ data, keepExisting: true }); + await stateService.import({ + data, + userId: SYSTEM_USER_ID, + keepExisting: true, + }); const events = await stores.eventStore.getEvents(); expect(events).toHaveLength(0); @@ -223,7 +237,11 @@ test('should drop strategies before import if specified', async () => { ], }; - await stateService.import({ data, dropBeforeImport: true }); + await stateService.import({ + data, + userId: SYSTEM_USER_ID, + dropBeforeImport: true, + }); const events = await stores.eventStore.getEvents(); expect(events).toHaveLength(2); @@ -237,7 +255,11 @@ test('should drop neither features nor strategies when neither is imported', asy const data = {}; - await stateService.import({ data, dropBeforeImport: true }); + await stateService.import({ + data, + userId: SYSTEM_USER_ID, + dropBeforeImport: true, + }); const events = await stores.eventStore.getEvents(); expect(events).toHaveLength(0); @@ -253,11 +275,11 @@ test('should not accept gibberish', async () => { const data2 = '{somerandomtext/'; await expect(async () => - stateService.import({ data: data1 }), + stateService.import({ userId: SYSTEM_USER_ID, data: data1 }), ).rejects.toThrow(); await expect(async () => - stateService.import({ data: data2 }), + stateService.import({ userId: SYSTEM_USER_ID, data: data2 }), ).rejects.toThrow(); }); @@ -349,7 +371,7 @@ test('should import a tag and tag type', async () => { tags: [{ type: 'simple', value: 'test' }], }; - await stateService.import({ data }); + await stateService.import({ userId: SYSTEM_USER_ID, data }); const events = await stores.eventStore.getEvents(); expect(events).toHaveLength(2); @@ -380,7 +402,11 @@ test('Should not import an existing tag', async () => { type: data.featureTags[0].tagType, value: data.featureTags[0].tagValue, }); - await stateService.import({ data, keepExisting: true }); + await stateService.import({ + data, + userId: SYSTEM_USER_ID, + keepExisting: true, + }); const events = await stores.eventStore.getEvents(); expect(events).toHaveLength(0); }); @@ -413,7 +439,11 @@ test('Should not keep existing tags if drop-before-import', async () => { }, ], }; - await stateService.import({ data, dropBeforeImport: true }); + await stateService.import({ + data, + userId: SYSTEM_USER_ID, + dropBeforeImport: true, + }); const tagTypes = await stores.tagTypeStore.getAll(); expect(tagTypes).toHaveLength(1); }); @@ -513,7 +543,7 @@ test('should import a project', async () => { ], }; - await stateService.import({ data }); + await stateService.import({ userId: SYSTEM_USER_ID, data }); const events = await stores.eventStore.getEvents(); expect(events).toHaveLength(1); @@ -536,11 +566,15 @@ test('Should not import an existing project', async () => { }; await stores.projectStore.create(data.projects[0]); - await stateService.import({ data, keepExisting: true }); + await stateService.import({ + data, + userId: SYSTEM_USER_ID, + keepExisting: true, + }); const events = await stores.eventStore.getEvents(); expect(events).toHaveLength(0); - await stateService.import({ data }); + await stateService.import({ userId: SYSTEM_USER_ID, data }); }); test('Should drop projects before import if specified', async () => { @@ -561,7 +595,11 @@ test('Should drop projects before import if specified', async () => { description: 'Not expected to be seen after import', mode: 'open' as const, }); - await stateService.import({ data, dropBeforeImport: true }); + await stateService.import({ + data, + userId: SYSTEM_USER_ID, + dropBeforeImport: true, + }); const hasProject = await stores.projectStore.hasProject('fancy'); expect(hasProject).toBe(false); }); @@ -695,6 +733,7 @@ test('featureStrategies can keep existing', async () => { const exported = await stateService.export({}); await stateService.import({ data: exported, + userId: SYSTEM_USER_ID, userName: 'testing', keepExisting: true, }); @@ -746,6 +785,7 @@ test('featureStrategies should not keep existing if dropBeforeImport', async () exported.featureStrategies = []; await stateService.import({ data: exported, + userId: SYSTEM_USER_ID, userName: 'testing', keepExisting: true, dropBeforeImport: true, @@ -757,6 +797,7 @@ test('Import v1 and exporting v2 should work', async () => { const { stateService } = getSetup(); await stateService.import({ data: oldExportExample, + userId: SYSTEM_USER_ID, dropBeforeImport: true, userName: 'testing', }); @@ -793,6 +834,7 @@ test('Importing states with deprecated strategies should keep their deprecated s }; await stateService.import({ data: deprecatedStrategyExample, + userId: SYSTEM_USER_ID, userName: 'strategy-importer', dropBeforeImport: true, keepExisting: false, @@ -807,6 +849,7 @@ test('Exporting a deprecated strategy and then importing it should keep correct await stateService.import({ data: variantsExportV3, keepExisting: false, + userId: SYSTEM_USER_ID, dropBeforeImport: true, userName: 'strategy importer', }); diff --git a/src/lib/services/state-service.ts b/src/lib/services/state-service.ts index 7b4fdb82c57f..fa562f2a62d1 100644 --- a/src/lib/services/state-service.ts +++ b/src/lib/services/state-service.ts @@ -118,6 +118,7 @@ export default class StateService { file, dropBeforeImport = false, userName = 'import-user', + userId, keepExisting = true, }: IImportFile): Promise { return readFile(file) @@ -128,6 +129,7 @@ export default class StateService { userName, dropBeforeImport, keepExisting, + userId, }), ); } @@ -168,6 +170,7 @@ export default class StateService { async import({ data, userName = 'importUser', + userId, dropBeforeImport = false, keepExisting = true, }: IImportData): Promise { @@ -186,6 +189,7 @@ export default class StateService { userName, dropBeforeImport, keepExisting, + userId, }); } @@ -196,6 +200,7 @@ export default class StateService { userName, dropBeforeImport, keepExisting, + userId, }); } @@ -215,6 +220,7 @@ export default class StateService { dropBeforeImport, keepExisting, featureEnvironments, + userId, }); if (featureEnvironments) { @@ -236,6 +242,7 @@ export default class StateService { userName, dropBeforeImport, keepExisting, + userId, }); } @@ -258,6 +265,7 @@ export default class StateService { userName, dropBeforeImport, keepExisting, + userId, }); } @@ -265,6 +273,7 @@ export default class StateService { await this.importSegments( data.segments, userName, + userId, dropBeforeImport, ); } @@ -361,6 +370,7 @@ export default class StateService { async importFeatures({ features, userName, + userId, dropBeforeImport, keepExisting, featureEnvironments, @@ -376,6 +386,7 @@ export default class StateService { await this.eventService.storeEvent({ type: DROP_FEATURES, createdBy: userName, + createdByUserId: userId, data: { name: 'all-features' }, }); } @@ -393,6 +404,7 @@ export default class StateService { ); await this.eventService.storeEvent({ type: FEATURE_IMPORT, + createdByUserId: userId, createdBy: userName, data: feature, }); @@ -404,6 +416,7 @@ export default class StateService { async importStrategies({ strategies, userName, + userId, dropBeforeImport, keepExisting, }): Promise { @@ -418,6 +431,7 @@ export default class StateService { await this.eventService.storeEvent({ type: DROP_STRATEGIES, createdBy: userName, + createdByUserId: userId, data: { name: 'all-strategies' }, }); } @@ -431,6 +445,7 @@ export default class StateService { this.eventService.storeEvent({ type: STRATEGY_IMPORT, createdBy: userName, + createdByUserId: userId, data: strategy, }); }), @@ -442,6 +457,7 @@ export default class StateService { async importEnvironments({ environments, userName, + userId, dropBeforeImport, keepExisting, }): Promise { @@ -455,19 +471,21 @@ export default class StateService { await this.eventService.storeEvent({ type: DROP_ENVIRONMENTS, createdBy: userName, + createdByUserId: userId, data: { name: 'all-environments' }, }); } const envsImport = environments.filter((env) => keepExisting ? !oldEnvs.some((old) => old.name === env.name) : true, ); - let importedEnvs = []; + let importedEnvs: IEnvironment[] = []; if (envsImport.length > 0) { importedEnvs = await this.environmentStore.importEnvironments(envsImport); const importedEnvironmentEvents = importedEnvs.map((env) => ({ type: ENVIRONMENT_IMPORT, createdBy: userName, + createdByUserId: userId, data: env, })); await this.eventService.storeEvents(importedEnvironmentEvents); @@ -480,6 +498,7 @@ export default class StateService { projects, importedEnvironments, userName, + userId, dropBeforeImport, keepExisting, }): Promise { @@ -493,6 +512,7 @@ export default class StateService { await this.eventService.storeEvent({ type: DROP_PROJECTS, createdBy: userName, + createdByUserId: userId, data: { name: 'all-projects' }, }); } @@ -509,6 +529,7 @@ export default class StateService { const importedProjectEvents = importedProjects.map((project) => ({ type: PROJECT_IMPORT, createdBy: userName, + createdByUserId: userId, data: project, })); await this.eventService.storeEvents(importedProjectEvents); @@ -521,6 +542,7 @@ export default class StateService { tags, featureTags, userName, + userId, dropBeforeImport, keepExisting, }): Promise { @@ -545,16 +567,19 @@ export default class StateService { { 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' }, }, ]); @@ -564,13 +589,15 @@ export default class StateService { keepExisting, oldTagTypes, userName, + userId, ); - await this.importTags(tags, keepExisting, oldTags, userName); + await this.importTags(tags, keepExisting, oldTags, userName, userId); await this.importFeatureTags( featureTags, keepExisting, oldFeatureTags, userName, + userId, ); } @@ -587,6 +614,7 @@ export default class StateService { keepExisting: boolean, oldFeatureTags: IFeatureTag[], userName: string, + userId: number, ): Promise { const featureTagsToInsert = featureTags.filter((tag) => keepExisting @@ -601,6 +629,7 @@ export default class StateService { const importedFeatureTagEvents = importedFeatureTags.map((tag) => ({ type: FEATURE_TAG_IMPORT, createdBy: userName, + createdByUserId: userId, data: tag, })); await this.eventService.storeEvents(importedFeatureTagEvents); @@ -615,6 +644,7 @@ export default class StateService { keepExisting: boolean, oldTags: ITag[], userName: string, + userId: number, ): Promise { const tagsToInsert = tags.filter((tag) => keepExisting @@ -626,6 +656,7 @@ export default class StateService { const importedTagEvents = importedTags.map((tag) => ({ type: TAG_IMPORT, createdBy: userName, + createdByUserId: userId, data: tag, })); await this.eventService.storeEvents(importedTagEvents); @@ -637,6 +668,7 @@ export default class StateService { keepExisting: boolean, oldTagTypes: ITagType[], userName: string, + userId: number, ): Promise { const tagTypesToInsert = tagTypes.filter((tagType) => keepExisting @@ -649,6 +681,7 @@ export default class StateService { const importedTagTypeEvents = importedTagTypes.map((tagType) => ({ type: TAG_TYPE_IMPORT, createdBy: userName, + createdByUserId: userId, data: tagType, })); await this.eventService.storeEvents(importedTagTypeEvents); @@ -658,6 +691,7 @@ export default class StateService { async importSegments( segments: PartialSome[], userName: string, + userId: number, dropBeforeImport: boolean, ): Promise { if (dropBeforeImport) { diff --git a/src/lib/services/strategy-service.ts b/src/lib/services/strategy-service.ts index 970a8f69d520..20d9b67871d1 100644 --- a/src/lib/services/strategy-service.ts +++ b/src/lib/services/strategy-service.ts @@ -47,6 +47,7 @@ class StrategyService { async removeStrategy( strategyName: string, userName: string, + userId: number, ): Promise { const strategy = await this.strategyStore.get(strategyName); await this._validateEditable(strategy); @@ -54,6 +55,7 @@ class StrategyService { await this.eventService.storeEvent({ type: STRATEGY_DELETED, createdBy: userName, + createdByUserId: userId, data: { name: strategyName, }, @@ -63,6 +65,7 @@ class StrategyService { async deprecateStrategy( strategyName: string, userName: string, + userId: number, ): Promise { if (await this.strategyStore.exists(strategyName)) { // Check existence @@ -70,6 +73,7 @@ class StrategyService { await this.eventService.storeEvent({ type: STRATEGY_DEPRECATED, createdBy: userName, + createdByUserId: userId, data: { name: strategyName, }, @@ -84,12 +88,14 @@ class StrategyService { async reactivateStrategy( strategyName: string, userName: string, + userId: number, ): Promise { await this.strategyStore.get(strategyName); // Check existence await this.strategyStore.reactivateStrategy({ name: strategyName }); await this.eventService.storeEvent({ type: STRATEGY_REACTIVATED, createdBy: userName, + createdByUserId: userId, data: { name: strategyName, }, @@ -99,6 +105,7 @@ class StrategyService { async createStrategy( value: IMinimalStrategy, userName: string, + userId: number, ): Promise { const strategy = await strategySchema.validateAsync(value); strategy.deprecated = false; @@ -108,6 +115,7 @@ class StrategyService { type: STRATEGY_CREATED, createdBy: userName, data: strategy, + createdByUserId: userId, }); return this.strategyStore.get(strategy.name); } @@ -115,6 +123,7 @@ class StrategyService { async updateStrategy( input: IMinimalStrategy, userName: string, + userId: number, ): Promise { const value = await strategySchema.validateAsync(input); const strategy = await this.strategyStore.get(input.name); @@ -124,6 +133,7 @@ class StrategyService { type: STRATEGY_UPDATED, createdBy: userName, data: value, + createdByUserId: userId, }); } @@ -146,7 +156,7 @@ class StrategyService { // This check belongs in the store. _validateEditable(strategy: IStrategy): void { - if (strategy.editable === false) { + if (!strategy.editable) { throw new Error(`Cannot edit strategy ${strategy.name}`); } } diff --git a/src/lib/services/tag-service.ts b/src/lib/services/tag-service.ts index fd41b38a60ff..e7165efcb65d 100644 --- a/src/lib/services/tag-service.ts +++ b/src/lib/services/tag-service.ts @@ -50,23 +50,29 @@ export default class TagService { return data; } - async createTag(tag: ITag, userName: string): Promise { + async createTag( + tag: ITag, + userName: string, + userId: number, + ): Promise { const data = await this.validate(tag); await this.tagStore.createTag(data); await this.eventService.storeEvent({ type: TAG_CREATED, createdBy: userName, + createdByUserId: userId, data, }); return data; } - async deleteTag(tag: ITag, userName: string): Promise { + async deleteTag(tag: ITag, userName: string, userId): Promise { await this.tagStore.delete(tag); await this.eventService.storeEvent({ type: TAG_DELETED, createdBy: userName, + createdByUserId: userId, data: tag, }); } diff --git a/src/lib/types/core.ts b/src/lib/types/core.ts index 70d1b0769765..1475d07972a6 100644 --- a/src/lib/types/core.ts +++ b/src/lib/types/core.ts @@ -2,7 +2,7 @@ import { Request } from 'express'; import EventEmitter from 'events'; import * as https from 'https'; import * as http from 'http'; -import User from './user'; +import User, { IUser } from './user'; import { IUnleashConfig } from './option'; import { IUnleashStores } from './stores'; import { IUnleashServices } from './services'; @@ -21,3 +21,14 @@ export interface IUnleash { stop: () => Promise; version: string; } + +export const SYSTEM_USER: IUser = { + email: 'systemuser@getunleash.io', + id: -1337, + imageUrl: '', + isAPI: false, + name: 'Used by unleash internally for performing system actions that have no user', + permissions: [], + username: 'unleash_system_user', +}; +export const SYSTEM_USER_ID: number = SYSTEM_USER.id; diff --git a/src/lib/types/events.ts b/src/lib/types/events.ts index 16836e910597..501b2dad0204 100644 --- a/src/lib/types/events.ts +++ b/src/lib/types/events.ts @@ -102,6 +102,8 @@ export const ENVIRONMENT_DELETED = 'environment-deleted' as const; export const SEGMENT_CREATED = 'segment-created' as const; export const SEGMENT_UPDATED = 'segment-updated' as const; export const SEGMENT_DELETED = 'segment-deleted' as const; + +export const SEGMENT_IMPORT = 'segment-import' as const; export const GROUP_CREATED = 'group-created' as const; export const GROUP_UPDATED = 'group-updated' as const; export const GROUP_DELETED = 'group-deleted' as const; @@ -173,7 +175,6 @@ export const BANNER_CREATED = 'banner-created' as const; export const BANNER_UPDATED = 'banner-updated' as const; export const BANNER_DELETED = 'banner-deleted' as const; -export const SYSTEM_USER_ID: number = -1337; export const IEventTypes = [ APPLICATION_CREATED, FEATURE_CREATED, @@ -307,6 +308,7 @@ export const IEventTypes = [ PROJECT_ENVIRONMENT_ADDED, PROJECT_ENVIRONMENT_REMOVED, DEFAULT_STRATEGY_UPDATED, + SEGMENT_IMPORT, ] as const; export type IEventType = (typeof IEventTypes)[number]; @@ -354,7 +356,7 @@ class BaseEvent implements IBaseEvent { typeof createdBy === 'string' ? createdBy : extractUsernameFromUser(createdBy); - this.createdByUserId = createdByUserId || SYSTEM_USER_ID; + this.createdByUserId = createdByUserId; } } diff --git a/src/lib/types/model.ts b/src/lib/types/model.ts index 670863b0a709..5e0c46da4289 100644 --- a/src/lib/types/model.ts +++ b/src/lib/types/model.ts @@ -443,6 +443,7 @@ interface ImportCommon { dropBeforeImport?: boolean; keepExisting?: boolean; userName?: string; + userId: number; } export interface IImportData extends ImportCommon { diff --git a/src/lib/types/stores/client-applications-store.ts b/src/lib/types/stores/client-applications-store.ts index 16783ad05017..45433858e071 100644 --- a/src/lib/types/stores/client-applications-store.ts +++ b/src/lib/types/stores/client-applications-store.ts @@ -12,6 +12,7 @@ export interface IClientApplication { lastSeen: Date; description: string; createdBy: string; + createdByUserId?: number; announced: boolean; url: string; color: string; diff --git a/src/test/e2e/api/admin/api-token.auth.e2e.test.ts b/src/test/e2e/api/admin/api-token.auth.e2e.test.ts index 0c37d9b32b18..167e8a3d860f 100644 --- a/src/test/e2e/api/admin/api-token.auth.e2e.test.ts +++ b/src/test/e2e/api/admin/api-token.auth.e2e.test.ts @@ -9,6 +9,7 @@ import { DELETE_CLIENT_API_TOKEN, READ_CLIENT_API_TOKEN, READ_FRONTEND_API_TOKEN, + SYSTEM_USER_ID, UPDATE_CLIENT_API_TOKEN, } from '../../../../lib/types'; import { addDays } from 'date-fns'; @@ -197,6 +198,7 @@ test('A role with only CREATE_PROJECT_API_TOKEN can create project tokens', asyn description: 'Can create client tokens', permissions: [{ name: CREATE_PROJECT_API_TOKEN }], type: 'root-custom', + createdByUserId: SYSTEM_USER_ID, }); await accessService.addUserToRole( user.id, diff --git a/src/test/e2e/api/admin/config.e2e.test.ts b/src/test/e2e/api/admin/config.e2e.test.ts index d2a3a51caa02..8ef084d5c084 100644 --- a/src/test/e2e/api/admin/config.e2e.test.ts +++ b/src/test/e2e/api/admin/config.e2e.test.ts @@ -56,6 +56,7 @@ test('gets ui config with frontendSettings', async () => { await app.services.proxyService.setFrontendSettings( { frontendApiOrigins }, randomId(), + -9999, ); await app.request .get('/api/admin/ui-config') diff --git a/src/test/e2e/api/admin/event.e2e.test.ts b/src/test/e2e/api/admin/event.e2e.test.ts index 2c5119584b9a..fb07bf9931bc 100644 --- a/src/test/e2e/api/admin/event.e2e.test.ts +++ b/src/test/e2e/api/admin/event.e2e.test.ts @@ -11,7 +11,7 @@ import { EventService } from '../../../../lib/services'; let app: IUnleashTest; let db: ITestDb; let eventService: EventService; - +const TEST_USER_ID = -9999; beforeAll(async () => { db = await dbInit('event_api_serial', getLogger); app = await setupAppWithCustomConfig(db.stores, { @@ -57,6 +57,7 @@ test('Can filter by project', async () => { tags: [], createdBy: 'test-user', environment: 'test', + createdByUserId: TEST_USER_ID, }); await eventService.storeEvent({ type: FEATURE_CREATED, @@ -65,6 +66,7 @@ test('Can filter by project', async () => { tags: [], createdBy: 'test-user', environment: 'test', + createdByUserId: TEST_USER_ID, }); await app.request .get('/api/admin/events?project=default') @@ -83,6 +85,7 @@ test('can search for events', async () => { data: { id: randomId() }, tags: [], createdBy: randomId(), + createdByUserId: TEST_USER_ID, }, { type: FEATURE_CREATED, @@ -91,6 +94,7 @@ test('can search for events', async () => { preData: { id: randomId() }, tags: [{ type: 'simple', value: randomId() }], createdBy: randomId(), + createdByUserId: TEST_USER_ID, }, ]; diff --git a/src/test/e2e/api/admin/state.e2e.test.ts b/src/test/e2e/api/admin/state.e2e.test.ts index 3f353ca6b0ae..fbf9b406165b 100644 --- a/src/test/e2e/api/admin/state.e2e.test.ts +++ b/src/test/e2e/api/admin/state.e2e.test.ts @@ -1,18 +1,19 @@ import dbInit, { ITestDb } from '../../helpers/database-init'; import { IUnleashTest, - setupApp, setupAppWithCustomConfig, } from '../../helpers/test-helper'; import getLogger from '../../../fixtures/no-logger'; import { DEFAULT_ENV } from '../../../../lib/util/constants'; import { collectIds } from '../../../../lib/util/collect-ids'; import { ApiTokenType } from '../../../../lib/types/models/api-token'; +import { IUser, SYSTEM_USER } from '../../../../lib/types'; const importData = require('../../../examples/import.json'); let app: IUnleashTest; let db: ITestDb; +const userId = -9999; beforeAll(async () => { db = await dbInit('state_api_serial', getLogger); @@ -173,6 +174,8 @@ test('Can roundtrip. I.e. export and then import', async () => { await app.services.environmentService.addEnvironmentToProject( environment, projectId, + SYSTEM_USER.username, + SYSTEM_USER.id, ); await app.services.featureToggleServiceV2.createFeatureToggle( projectId, @@ -182,6 +185,7 @@ test('Can roundtrip. I.e. export and then import', async () => { description: 'Feature for export', }, userName, + userId, ); await app.services.featureToggleServiceV2.createStrategy( { @@ -193,6 +197,7 @@ test('Can roundtrip. I.e. export and then import', async () => { }, { projectId, featureName, environment }, userName, + { id: userId } as IUser, ); const data = await app.services.stateService.export({}); await app.services.stateService.import({ @@ -200,6 +205,7 @@ test('Can roundtrip. I.e. export and then import', async () => { dropBeforeImport: true, keepExisting: false, userName: 'export-tester', + userId: -9999, }); }); @@ -221,6 +227,8 @@ test('Roundtrip with tags works', async () => { await app.services.environmentService.addEnvironmentToProject( environment, projectId, + SYSTEM_USER.username, + SYSTEM_USER.id, ); await app.services.featureToggleServiceV2.createFeatureToggle( projectId, @@ -230,6 +238,7 @@ test('Roundtrip with tags works', async () => { description: 'Feature for export', }, userName, + userId, ); await app.services.featureToggleServiceV2.createStrategy( { @@ -250,11 +259,13 @@ test('Roundtrip with tags works', async () => { featureName, { type: 'simple', value: 'export-test' }, userName, + -9999, ); await app.services.featureTagService.addTag( featureName, { type: 'simple', value: 'export-test-2' }, userName, + -9999, ); const data = await app.services.stateService.export({}); await app.services.stateService.import({ @@ -262,6 +273,7 @@ test('Roundtrip with tags works', async () => { dropBeforeImport: true, keepExisting: false, userName: 'export-tester', + userId: -9999, }); const f = await app.services.featureTagService.listTags(featureName); @@ -292,15 +304,20 @@ test('Roundtrip with strategies in multiple environments works', async () => { description: 'Feature for export', }, userName, + userId, ); await app.services.environmentService.addEnvironmentToProject( environment, projectId, + SYSTEM_USER.username, + SYSTEM_USER.id, ); await app.services.environmentService.addEnvironmentToProject( DEFAULT_ENV, projectId, + SYSTEM_USER.username, + SYSTEM_USER.id, ); await app.services.featureToggleServiceV2.createStrategy( { @@ -330,6 +347,7 @@ test('Roundtrip with strategies in multiple environments works', async () => { dropBeforeImport: true, keepExisting: false, userName: 'export-tester', + userId: -9999, }); const f = await app.services.featureToggleServiceV2.getFeature({ featureName, @@ -387,6 +405,8 @@ test(`should not delete api_tokens on import when drop-flag is set`, async () => await app.services.environmentService.addEnvironmentToProject( environment, projectId, + SYSTEM_USER.username, + SYSTEM_USER.id, ); await app.services.featureToggleServiceV2.createFeatureToggle( projectId, @@ -396,6 +416,7 @@ test(`should not delete api_tokens on import when drop-flag is set`, async () => description: 'Feature for export', }, userName, + userId, ); await app.services.apiTokenService.createApiTokenWithProjects({ tokenName: apiTokenName, @@ -410,6 +431,7 @@ test(`should not delete api_tokens on import when drop-flag is set`, async () => dropBeforeImport: true, keepExisting: false, userName: userName, + userId: -9999, }); const apiTokens = await app.services.apiTokenService.getAllTokens(); diff --git a/src/test/e2e/api/client/feature.e2e.test.ts b/src/test/e2e/api/client/feature.e2e.test.ts index 7f5aeef86703..37ee1942ece5 100644 --- a/src/test/e2e/api/client/feature.e2e.test.ts +++ b/src/test/e2e/api/client/feature.e2e.test.ts @@ -6,10 +6,11 @@ import dbInit, { ITestDb } from '../../helpers/database-init'; import getLogger from '../../../fixtures/no-logger'; import { DEFAULT_ENV } from '../../../../lib/util/constants'; import User from '../../../../lib/types/user'; +import { SYSTEM_USER } from '../../../../lib/types'; let app: IUnleashTest; let db: ITestDb; -const testUser = { name: 'test' } as User; +const testUser = { name: 'test', id: -9999 } as User; beforeAll(async () => { db = await dbInit('feature_api_client', getLogger); @@ -32,6 +33,7 @@ beforeAll(async () => { impressionData: true, }, 'test', + testUser.id, ); await app.services.featureToggleServiceV2.createFeatureToggle( 'default', @@ -40,6 +42,7 @@ beforeAll(async () => { description: 'soon to be the #1 feature', }, 'test', + testUser.id, ); await app.services.featureToggleServiceV2.createFeatureToggle( @@ -49,6 +52,7 @@ beforeAll(async () => { description: 'terrible feature', }, 'test', + testUser.id, ); await app.services.featureToggleServiceV2.createFeatureToggle( 'default', @@ -57,12 +61,14 @@ beforeAll(async () => { description: 'the #1 feature', }, 'test', + testUser.id, ); // depend on enabled feature with variant await app.services.dependentFeaturesService.unprotectedUpsertFeatureDependency( { child: 'featureY', projectId: 'default' }, { feature: 'featureX', variants: ['featureXVariant'] }, 'test', + testUser.id, ); await app.services.featureToggleServiceV2.archiveToggle( @@ -77,6 +83,7 @@ beforeAll(async () => { description: 'soon to be the #1 feature', }, 'test', + testUser.id, ); await app.services.featureToggleServiceV2.archiveToggle( @@ -90,6 +97,7 @@ beforeAll(async () => { description: 'terrible feature', }, 'test', + testUser.id, ); await app.services.featureToggleServiceV2.archiveToggle( 'featureArchivedZ', @@ -102,6 +110,7 @@ beforeAll(async () => { description: 'A feature toggle with variants', }, 'test', + testUser.id, ); await app.services.featureToggleServiceV2.saveVariants( 'feature.with.variants', @@ -121,6 +130,7 @@ beforeAll(async () => { }, ], 'ivar', + testUser.id, ); }); @@ -243,6 +253,8 @@ test('Can get strategies for specific environment', async () => { await app.services.environmentService.addEnvironmentToProject( 'testing', 'default', + SYSTEM_USER.username, + SYSTEM_USER.id, ); await app.request diff --git a/src/test/e2e/api/client/feature.env.disabled.e2e.test.ts b/src/test/e2e/api/client/feature.env.disabled.e2e.test.ts index 3f61644528b0..c1cd19222117 100644 --- a/src/test/e2e/api/client/feature.env.disabled.e2e.test.ts +++ b/src/test/e2e/api/client/feature.env.disabled.e2e.test.ts @@ -1,17 +1,18 @@ import { IUnleashTest, - setupApp, setupAppWithCustomConfig, } from '../../helpers/test-helper'; import dbInit, { ITestDb } from '../../helpers/database-init'; import getLogger from '../../../fixtures/no-logger'; import { DEFAULT_ENV } from '../../../../lib/util/constants'; +import { IUser } from '../../../../lib/types'; let app: IUnleashTest; let db: ITestDb; const featureName = 'feature.default.1'; const username = 'test'; +const userId = -9999; const projectId = 'default'; beforeAll(async () => { @@ -25,12 +26,14 @@ beforeAll(async () => { description: 'the #1 feature', }, username, + userId, ); await app.services.featureToggleServiceV2.createStrategy( { name: 'default', constraints: [], parameters: {} }, { projectId, featureName, environment: DEFAULT_ENV }, username, + { id: userId } as IUser, ); }); diff --git a/src/test/e2e/api/client/feature.optimal304.e2e.test.ts b/src/test/e2e/api/client/feature.optimal304.e2e.test.ts index cf4020e118fc..219ecd4032f0 100644 --- a/src/test/e2e/api/client/feature.optimal304.e2e.test.ts +++ b/src/test/e2e/api/client/feature.optimal304.e2e.test.ts @@ -9,7 +9,7 @@ import User from '../../../../lib/types/user'; let app: IUnleashTest; let db: ITestDb; -const testUser = { name: 'test' } as User; +const testUser = { name: 'test', id: -9999 } as User; beforeAll(async () => { db = await dbInit('feature_304_api_client', getLogger); @@ -29,6 +29,7 @@ beforeAll(async () => { impressionData: true, }, 'test', + testUser.id, ); await app.services.featureToggleServiceV2.createFeatureToggle( 'default', @@ -37,6 +38,7 @@ beforeAll(async () => { description: 'soon to be the #1 feature', }, 'test', + testUser.id, ); await app.services.featureToggleServiceV2.createFeatureToggle( 'default', @@ -45,6 +47,7 @@ beforeAll(async () => { description: 'terrible feature', }, 'test', + testUser.id, ); await app.services.featureToggleServiceV2.createFeatureToggle( 'default', @@ -53,6 +56,7 @@ beforeAll(async () => { description: 'the #1 feature', }, 'test', + testUser.id, ); await app.services.featureToggleServiceV2.archiveToggle( @@ -67,6 +71,7 @@ beforeAll(async () => { description: 'soon to be the #1 feature', }, 'test', + testUser.id, ); await app.services.featureToggleServiceV2.archiveToggle( @@ -80,6 +85,7 @@ beforeAll(async () => { description: 'terrible feature', }, 'test', + testUser.id, ); await app.services.featureToggleServiceV2.archiveToggle( 'featureArchivedZ', @@ -92,6 +98,7 @@ beforeAll(async () => { description: 'A feature toggle with variants', }, 'test', + testUser.id, ); await app.services.featureToggleServiceV2.saveVariants( 'feature.with.variants', @@ -111,6 +118,7 @@ beforeAll(async () => { }, ], 'ivar', + testUser.id, ); }); @@ -143,6 +151,7 @@ test('returns 200 when content updates and hash does not match anymore', async ( description: 'the #1 feature', }, 'test', + testUser.id, ); await app.services.configurationRevisionService.updateMaxRevisionId(); diff --git a/src/test/e2e/api/client/feature.token.access.e2e.test.ts b/src/test/e2e/api/client/feature.token.access.e2e.test.ts index 801b24843644..4557562ae12f 100644 --- a/src/test/e2e/api/client/feature.token.access.e2e.test.ts +++ b/src/test/e2e/api/client/feature.token.access.e2e.test.ts @@ -4,6 +4,7 @@ import getLogger from '../../../fixtures/no-logger'; import { ApiTokenService } from '../../../../lib/services/api-token-service'; import { ApiTokenType } from '../../../../lib/types/models/api-token'; import { DEFAULT_ENV } from '../../../../lib/util/constants'; +import { SYSTEM_USER } from '../../../../lib/types'; let app: IUnleashTest; let db: ITestDb; @@ -14,6 +15,7 @@ const environment = 'testing'; const project = 'default'; const project2 = 'some'; const tokenName = 'test'; +const tokenUserId = -9999; const feature1 = 'f1.token.access'; const feature2 = 'f2.token.access'; const feature3 = 'f3.p2.token.access'; @@ -38,8 +40,18 @@ beforeAll(async () => { mode: 'open' as const, }); - await environmentService.addEnvironmentToProject(environment, project); - await environmentService.addEnvironmentToProject(environment, project2); + await environmentService.addEnvironmentToProject( + environment, + project, + SYSTEM_USER.username, + SYSTEM_USER.id, + ); + await environmentService.addEnvironmentToProject( + environment, + project2, + SYSTEM_USER.username, + SYSTEM_USER.id, + ); await featureToggleServiceV2.createFeatureToggle( project, @@ -48,6 +60,7 @@ beforeAll(async () => { description: 'the #1 feature', }, tokenName, + tokenUserId, ); await featureToggleServiceV2.createStrategy( @@ -76,6 +89,7 @@ beforeAll(async () => { name: feature2, }, tokenName, + tokenUserId, ); await featureToggleServiceV2.createStrategy( { @@ -94,6 +108,7 @@ beforeAll(async () => { name: feature3, }, tokenName, + tokenUserId, ); await featureToggleServiceV2.createStrategy( { diff --git a/src/test/e2e/api/client/metricsV2.e2e.test.ts b/src/test/e2e/api/client/metricsV2.e2e.test.ts index 327707e2ca2c..dc8a9b4b9f93 100644 --- a/src/test/e2e/api/client/metricsV2.e2e.test.ts +++ b/src/test/e2e/api/client/metricsV2.e2e.test.ts @@ -8,7 +8,7 @@ let app: IUnleashTest; let db: ITestDb; let defaultToken; - +const TEST_USER_ID = -9999; beforeAll(async () => { db = await dbInit('metrics_two_api_client', getLogger); app = await setupAppWithAuth(db.stores, {}, db.rawDatabase); @@ -104,11 +104,13 @@ test('should set lastSeen for toggles with metrics both for toggle and toggle en 'default', { name: 't1' }, 'tester', + TEST_USER_ID, ); await app.services.featureToggleServiceV2.createFeatureToggle( 'default', { name: 't2' }, 'tester', + TEST_USER_ID, ); const token = await app.services.apiTokenService.createApiToken({ diff --git a/src/test/e2e/api/proxy/proxy.e2e.test.ts b/src/test/e2e/api/proxy/proxy.e2e.test.ts index 531ce811157c..5c4f44bc8aed 100644 --- a/src/test/e2e/api/proxy/proxy.e2e.test.ts +++ b/src/test/e2e/api/proxy/proxy.e2e.test.ts @@ -12,13 +12,14 @@ import { FEATURE_UPDATED, IConstraint, IStrategyConfig, + SYSTEM_USER, } from '../../../../lib/types'; import { ProxyRepository } from '../../../../lib/proxy'; import { Logger } from '../../../../lib/logger'; let app: IUnleashTest; let db: ITestDb; - +const TEST_USER_ID = -9999; beforeAll(async () => { db = await dbInit('proxy', getLogger); app = await setupAppWithAuth( @@ -78,6 +79,7 @@ const createFeatureToggle = async ({ project, { name }, 'userName', + TEST_USER_ID, true, ); const createdStrategies = await Promise.all( @@ -688,10 +690,14 @@ test('should filter features by environment', async () => { await app.services.environmentService.addEnvironmentToProject( environmentA, 'default', + SYSTEM_USER.username, + SYSTEM_USER.id, ); await app.services.environmentService.addEnvironmentToProject( environmentB, 'default', + SYSTEM_USER.username, + SYSTEM_USER.id, ); const frontendTokenEnvironmentDefault = await createApiToken( ApiTokenType.FRONTEND, diff --git a/src/test/e2e/services/access-service.e2e.test.ts b/src/test/e2e/services/access-service.e2e.test.ts index 279cd5f817c5..709ab14d6d25 100644 --- a/src/test/e2e/services/access-service.e2e.test.ts +++ b/src/test/e2e/services/access-service.e2e.test.ts @@ -3,6 +3,7 @@ import getLogger from '../../fixtures/no-logger'; import { AccessService, + IRoleUpdate, PermissionRef, } from '../../../lib/services/access-service'; @@ -34,6 +35,7 @@ let adminRole; let readRole; let userIndex = 0; +const TEST_USER_ID = -9999; const createUser = async (role?: number) => { const name = `User ${userIndex}`; const email = `user-${userIndex}@getunleash.io`; @@ -73,6 +75,7 @@ const createRole = async (rolePermissions: PermissionRef[]) => { name: `Role ${roleIndex}`, description: `Role ${roleIndex++} description`, permissions: rolePermissions, + createdByUserId: TEST_USER_ID, }); }; @@ -737,7 +740,7 @@ test('Should be denied access to delete a role that is in use', async () => { await projectService.addUser(project.id, customRole.id, projectMember.id); try { - await accessService.deleteRole(customRole.id); + await accessService.deleteRole(customRole.id, 'testuser', TEST_USER_ID); } catch (e) { expect(e.toString()).toBe( 'RoleInUseError: Role is in use by users(1) or groups(0). You cannot delete a role that is in use without first removing the role from the users and groups.', @@ -822,7 +825,8 @@ test('Should not be allowed to edit a root role', async () => { expect.assertions(1); const editRole = await accessService.getRoleByName(RoleName.EDITOR); - const roleUpdate = { + const roleUpdate: IRoleUpdate = { + createdByUserId: TEST_USER_ID, id: editRole.id, name: 'NoLongerTheEditor', description: '', @@ -843,7 +847,7 @@ test('Should not be allowed to delete a root role', async () => { const editRole = await accessService.getRoleByName(RoleName.EDITOR); try { - await accessService.deleteRole(editRole.id); + await accessService.deleteRole(editRole.id, 'testuser', TEST_USER_ID); } catch (e) { expect(e.toString()).toBe( 'InvalidOperationError: You cannot change built in roles.', @@ -855,7 +859,8 @@ test('Should not be allowed to edit a project role', async () => { expect.assertions(1); const ownerRole = await accessService.getRoleByName(RoleName.OWNER); - const roleUpdate = { + const roleUpdate: IRoleUpdate = { + createdByUserId: TEST_USER_ID, id: ownerRole.id, name: 'NoLongerTheEditor', description: '', @@ -876,7 +881,7 @@ test('Should not be allowed to delete a project role', async () => { const ownerRole = await accessService.getRoleByName(RoleName.OWNER); try { - await accessService.deleteRole(ownerRole.id); + await accessService.deleteRole(ownerRole.id, 'testuser', TEST_USER_ID); } catch (e) { expect(e.toString()).toBe( 'InvalidOperationError: You cannot change built in roles.', diff --git a/src/test/e2e/services/addon-service.e2e.test.ts b/src/test/e2e/services/addon-service.e2e.test.ts index 085c90b8dfd2..07347a6e562d 100644 --- a/src/test/e2e/services/addon-service.e2e.test.ts +++ b/src/test/e2e/services/addon-service.e2e.test.ts @@ -14,6 +14,7 @@ const addonProvider = { simple: new SimpleAddon() }; let db; let stores: IUnleashStores; let addonService: AddonService; +const TEST_USER_ID = -9999; beforeAll(async () => { const config = createTestConfig({ @@ -77,9 +78,9 @@ test('should only return active addons', async () => { description: '', }; - await addonService.createAddon(config, 'me@mail.com'); - await addonService.createAddon(config2, 'me@mail.com'); - await addonService.createAddon(config3, 'me@mail.com'); + await addonService.createAddon(config, 'me@mail.com', TEST_USER_ID); + await addonService.createAddon(config2, 'me@mail.com', TEST_USER_ID); + await addonService.createAddon(config3, 'me@mail.com', TEST_USER_ID); jest.advanceTimersByTime(61_000); diff --git a/src/test/e2e/services/api-token-service.e2e.test.ts b/src/test/e2e/services/api-token-service.e2e.test.ts index 32e7620f73bc..9a0744f5f794 100644 --- a/src/test/e2e/services/api-token-service.e2e.test.ts +++ b/src/test/e2e/services/api-token-service.e2e.test.ts @@ -117,7 +117,7 @@ test('should update expiry of token', async () => { 'tester', ); - await apiTokenService.updateExpiry(token.secret, newTime, 'tester'); + await apiTokenService.updateExpiry(token.secret, newTime, 'tester', -9999); const [updatedToken] = await apiTokenService.getAllTokens(); diff --git a/src/test/e2e/services/group-service.e2e.test.ts b/src/test/e2e/services/group-service.e2e.test.ts index becbda94dedc..f965568675f1 100644 --- a/src/test/e2e/services/group-service.e2e.test.ts +++ b/src/test/e2e/services/group-service.e2e.test.ts @@ -186,6 +186,7 @@ test('adding a root role to a group with a project role should not fail', async description: 'root_group', }, 'test', + -9999, ); await stores.accessStore.addGroupToRole(group.id, 1, 'test', 'default'); @@ -200,6 +201,7 @@ test('adding a root role to a group with a project role should not fail', async createdBy: 'test', }, 'test', + -9999, ); expect(updatedGroup).toMatchObject({ @@ -256,6 +258,7 @@ test('adding a nonexistent role to a group should fail', async () => { createdBy: 'test', }, 'test', + -9999, ); }).rejects.toThrow( 'Request validation failed: your request body or params contain invalid data: Incorrect role id 100', diff --git a/src/test/e2e/services/project-service.e2e.test.ts b/src/test/e2e/services/project-service.e2e.test.ts index 1f52daa981a3..c1e1cb1435c9 100644 --- a/src/test/e2e/services/project-service.e2e.test.ts +++ b/src/test/e2e/services/project-service.e2e.test.ts @@ -17,7 +17,7 @@ import { createFeatureToggleService, createProjectService, } from '../../../lib/features'; -import { IGroup, IUnleashStores } from 'lib/types'; +import { IGroup, IUnleashStores, SYSTEM_USER, SYSTEM_USER_ID } from 'lib/types'; import { User } from 'lib/server-impl'; let stores: IUnleashStores; @@ -30,6 +30,7 @@ let environmentService: EnvironmentService; let featureToggleService: FeatureToggleService; let user: User; // many methods in this test use User instead of IUser let group: IGroup; +const TEST_USER_ID = -9999; const isProjectUser = async ( userId: number, @@ -487,6 +488,7 @@ test('should remove user from the project', async () => { memberRole.id, projectMember1.id, 'test', + TEST_USER_ID, ); const { users } = await projectService.getAccessToProject(project.id); @@ -511,6 +513,7 @@ test('should not change project if feature toggle project does not match current project.id, toggle, user.email, + TEST_USER_ID, ); try { @@ -542,6 +545,7 @@ test('should return 404 if no project is found with the project id', async () => project.id, toggle, user.email, + TEST_USER_ID, ); try { @@ -585,6 +589,7 @@ test('should fail if user is not authorized', async () => { project.id, toggle, user.email, + TEST_USER_ID, ); try { @@ -621,6 +626,7 @@ test('should change project when checks pass', async () => { projectA.id, toggle, user.email, + TEST_USER_ID, ); await projectService.changeProject( projectB.id, @@ -655,6 +661,7 @@ test('changing project should emit event even if user does not have a username s projectA.id, toggle, user.email, + TEST_USER_ID, ); const eventsBeforeChange = await stores.eventStore.getEvents(); await projectService.changeProject( @@ -689,11 +696,14 @@ test('should require equal project environments to move features', async () => { projectA.id, toggle, user.email, + TEST_USER_ID, ); await stores.environmentStore.create(environment); await environmentService.addEnvironmentToProject( environment.name, projectB.id, + 'test', + TEST_USER_ID, ); await expect(() => @@ -810,6 +820,7 @@ test('should add a user to the project with a custom role', async () => { id: 8, // DELETE_FEATURE }, ], + createdByUserId: SYSTEM_USER_ID, }); await projectService.addUser( @@ -860,6 +871,7 @@ test('should delete role entries when deleting project', async () => { id: 8, // DELETE_FEATURE }, ], + createdByUserId: SYSTEM_USER_ID, }); await projectService.addUser(project.id, customRole.id, user1.id, 'test'); @@ -900,6 +912,7 @@ test('should change a users role in the project', async () => { id: 8, // DELETE_FEATURE }, ], + createdByUserId: SYSTEM_USER_ID, }); const member = await stores.roleStore.getRoleByName(RoleName.MEMBER); @@ -915,6 +928,7 @@ test('should change a users role in the project', async () => { member.id, projectUser.id, 'test', + TEST_USER_ID, ); await projectService.addUser( project.id, @@ -962,6 +976,7 @@ test('should update role for user on project', async () => { ownerRole.id, projectMember1.id, 'test', + TEST_USER_ID, ); const { users } = await projectService.getAccessToProject(project.id); @@ -1006,6 +1021,7 @@ test('should able to assign role without existing members', async () => { testRole.id, projectMember1.id, 'test', + TEST_USER_ID, ); const { users } = await projectService.getAccessToProject(project.id); @@ -1036,13 +1052,19 @@ describe('ensure project has at least one owner', () => { ownerRole.id, user.id, 'test', + TEST_USER_ID, ); }).rejects.toThrowError( new Error('A project must have at least one owner'), ); await expect(async () => { - await projectService.removeUserAccess(project.id, user.id, 'test'); + await projectService.removeUserAccess( + project.id, + user.id, + 'test', + TEST_USER_ID, + ); }).rejects.toThrowError( new Error('A project must have at least one owner'), ); @@ -1073,6 +1095,7 @@ describe('ensure project has at least one owner', () => { [], [memberUser.id], 'test', + TEST_USER_ID, ); const usersBefore = await projectService.getProjectUsers(project.id); @@ -1080,6 +1103,7 @@ describe('ensure project has at least one owner', () => { project.id, memberUser.id, 'test', + TEST_USER_ID, ); const usersAfter = await projectService.getProjectUsers(project.id); expect(usersBefore).toHaveLength(2); @@ -1118,6 +1142,7 @@ describe('ensure project has at least one owner', () => { memberRole.id, user.id, 'test', + TEST_USER_ID, ); }).rejects.toThrowError( new Error('A project must have at least one owner'), @@ -1129,6 +1154,7 @@ describe('ensure project has at least one owner', () => { user.id, [memberRole.id], 'test', + TEST_USER_ID, ); }).rejects.toThrowError( new Error('A project must have at least one owner'), @@ -1153,6 +1179,7 @@ describe('ensure project has at least one owner', () => { ownerRole.id, group.id, 'test', + TEST_USER_ID, ); // this should be fine, leaving the group as the only owner @@ -1162,6 +1189,7 @@ describe('ensure project has at least one owner', () => { ownerRole.id, user.id, 'test', + TEST_USER_ID, ); return { @@ -1182,6 +1210,7 @@ describe('ensure project has at least one owner', () => { ownerRole.id, group.id, 'test', + TEST_USER_ID, ); }).rejects.toThrowError( new Error('A project must have at least one owner'), @@ -1192,6 +1221,7 @@ describe('ensure project has at least one owner', () => { project.id, group.id, 'test', + TEST_USER_ID, ); }).rejects.toThrowError( new Error('A project must have at least one owner'), @@ -1212,6 +1242,7 @@ describe('ensure project has at least one owner', () => { memberRole.id, group.id, 'test', + TEST_USER_ID, ); }).rejects.toThrowError( new Error('A project must have at least one owner'), @@ -1223,6 +1254,7 @@ describe('ensure project has at least one owner', () => { group.id, [memberRole.id], 'test', + TEST_USER_ID, ); }).rejects.toThrowError( new Error('A project must have at least one owner'), @@ -1258,6 +1290,7 @@ test('Should allow bulk update of group permissions', async () => { id: 2, // CREATE_FEATURE }, ], + createdByUserId: SYSTEM_USER_ID, }); await projectService.addAccess( @@ -1266,6 +1299,7 @@ test('Should allow bulk update of group permissions', async () => { [group1.id], [user1.id], 'some-admin-user', + TEST_USER_ID, ); }); @@ -1285,6 +1319,7 @@ test('Should bulk update of only users', async () => { id: 2, // CREATE_FEATURE }, ], + createdByUserId: SYSTEM_USER_ID, }); await projectService.addAccess( @@ -1293,6 +1328,7 @@ test('Should bulk update of only users', async () => { [], [user1.id], 'some-admin-user', + TEST_USER_ID, ); }); @@ -1320,6 +1356,7 @@ test('Should allow bulk update of only groups', async () => { id: 2, // CREATE_FEATURE }, ], + createdByUserId: SYSTEM_USER_ID, }); await projectService.addAccess( @@ -1328,6 +1365,7 @@ test('Should allow bulk update of only groups', async () => { [group1.id], [], 'some-admin-user', + TEST_USER_ID, ); }); @@ -1369,6 +1407,7 @@ test('Should allow permutations of roles, groups and users when adding a new acc id: 2, // CREATE_FEATURE }, ], + createdByUserId: SYSTEM_USER_ID, }); const role2 = await accessService.createRole({ @@ -1379,6 +1418,7 @@ test('Should allow permutations of roles, groups and users when adding a new acc id: 7, // UPDATE_FEATURE }, ], + createdByUserId: SYSTEM_USER_ID, }); await projectService.addAccess( @@ -1387,6 +1427,7 @@ test('Should allow permutations of roles, groups and users when adding a new acc [group1.id, group2.id], [user1.id, user2.id], 'some-admin-user', + TEST_USER_ID, ); const { users, groups } = await projectService.getAccessToProject( @@ -1487,6 +1528,7 @@ test('should calculate average time to production', async () => { project.id, toggle, user.email, + TEST_USER_ID, ); }), ); @@ -1500,6 +1542,7 @@ test('should calculate average time to production', async () => { featureName: toggle.name, environment: 'default', createdBy: 'Fredrik', + createdByUserId: TEST_USER_ID, }), ); }), @@ -1534,6 +1577,7 @@ test('should calculate average time to production ignoring some items', async () featureName, environment: 'default', createdBy: 'Fredrik', + createdByUserId: TEST_USER_ID, tags: [], }); @@ -1542,7 +1586,12 @@ test('should calculate average time to production ignoring some items', async () name: 'customEnv', type: 'development', }); - await environmentService.addEnvironmentToProject('customEnv', project.id); + await environmentService.addEnvironmentToProject( + 'customEnv', + project.id, + SYSTEM_USER.username, + SYSTEM_USER.id, + ); // actual toggle we take for calculations const toggle = { name: 'main-toggle' }; @@ -1550,6 +1599,7 @@ test('should calculate average time to production ignoring some items', async () project.id, toggle, user.email, + TEST_USER_ID, ); await updateFeature(toggle.name, { created_at: subDays(new Date(), 20), @@ -1569,6 +1619,7 @@ test('should calculate average time to production ignoring some items', async () project.id, devToggle, user.email, + TEST_USER_ID, ); await eventService.storeEvent( new FeatureEnvironmentEvent({ @@ -1583,6 +1634,7 @@ test('should calculate average time to production ignoring some items', async () 'default', otherProjectToggle, user.email, + TEST_USER_ID, ); await eventService.storeEvent( new FeatureEnvironmentEvent(makeEvent(otherProjectToggle.name)), @@ -1594,6 +1646,7 @@ test('should calculate average time to production ignoring some items', async () project.id, nonReleaseToggle, user.email, + TEST_USER_ID, ); await eventService.storeEvent( new FeatureEnvironmentEvent(makeEvent(nonReleaseToggle.name)), @@ -1605,6 +1658,7 @@ test('should calculate average time to production ignoring some items', async () project.id, previouslyDeleteToggle, user.email, + TEST_USER_ID, ); await eventService.storeEvent( new FeatureEnvironmentEvent(makeEvent(previouslyDeleteToggle.name)), @@ -1641,6 +1695,7 @@ test('should get correct amount of features created in current and past window', project.id, toggle, user.email, + TEST_USER_ID, ); }), ); @@ -1678,6 +1733,7 @@ test('should get correct amount of features archived in current and past window' project.id, toggle, user.email, + TEST_USER_ID, ); }), ); @@ -1770,6 +1826,7 @@ test('should return average time to production per toggle', async () => { project.id, toggle, user.email, + TEST_USER_ID, ); }), ); @@ -1783,6 +1840,7 @@ test('should return average time to production per toggle', async () => { featureName: toggle.name, environment: 'default', createdBy: 'Fredrik', + createdByUserId: TEST_USER_ID, }), ); }), @@ -1838,6 +1896,7 @@ test('should return average time to production per toggle for a specific project project1.id, toggle, user.email, + TEST_USER_ID, ); }), ); @@ -1848,6 +1907,7 @@ test('should return average time to production per toggle for a specific project project2.id, toggle, user.email, + TEST_USER_ID, ); }), ); @@ -1861,6 +1921,7 @@ test('should return average time to production per toggle for a specific project featureName: toggle.name, environment: 'default', createdBy: 'Fredrik', + createdByUserId: TEST_USER_ID, }), ); }), @@ -1875,6 +1936,7 @@ test('should return average time to production per toggle for a specific project featureName: toggle.name, environment: 'default', createdBy: 'Fredrik', + createdByUserId: TEST_USER_ID, }), ); }), @@ -1925,6 +1987,7 @@ test('should return average time to production per toggle and include archived t project1.id, toggle, user.email, + TEST_USER_ID, ); }), ); @@ -1938,6 +2001,7 @@ test('should return average time to production per toggle and include archived t featureName: toggle.name, environment: 'default', createdBy: 'Fredrik', + createdByUserId: TEST_USER_ID, }), ); }), diff --git a/src/test/e2e/services/setting-service.test.ts b/src/test/e2e/services/setting-service.test.ts index e39e3b9ac62c..409cbfc49c9c 100644 --- a/src/test/e2e/services/setting-service.test.ts +++ b/src/test/e2e/services/setting-service.test.ts @@ -13,6 +13,7 @@ import { property } from 'fast-check'; let stores: IUnleashStores; let db; let service: SettingService; +const TEST_USER_ID = -9999; beforeAll(async () => { const config = createTestConfig(); @@ -30,7 +31,13 @@ afterAll(async () => { test('Can create new setting', async () => { const someData = { some: 'blob' }; - await service.insert('some-setting', someData, 'test-user', false); + await service.insert( + 'some-setting', + someData, + 'test-user', + TEST_USER_ID, + false, + ); const actual = await service.get('some-setting'); expect(actual).toStrictEqual(someData); @@ -44,8 +51,8 @@ test('Can create new setting', async () => { test('Can delete setting', async () => { const someData = { some: 'blob' }; - await service.insert('some-setting', someData, 'test-user'); - await service.delete('some-setting', 'test-user'); + await service.insert('some-setting', someData, 'test-user', TEST_USER_ID); + await service.delete('some-setting', 'test-user', TEST_USER_ID); const actual = await service.get('some-setting'); expect(actual).toBeUndefined(); @@ -59,9 +66,14 @@ test('Can delete setting', async () => { test('Sentitive SSO settings are redacted in event log', async () => { const someData = { password: 'mySecretPassword' }; const property = 'unleash.enterprise.auth.oidc'; - await service.insert(property, someData, 'a-user-in-places'); + await service.insert(property, someData, 'a-user-in-places', TEST_USER_ID); - await service.insert(property, { password: 'changed' }, 'a-user-in-places'); + await service.insert( + property, + { password: 'changed' }, + 'a-user-in-places', + TEST_USER_ID, + ); const actual = await service.get(property); const { eventStore } = stores; @@ -69,17 +81,24 @@ test('Sentitive SSO settings are redacted in event log', async () => { type: SETTING_UPDATED, }); expect(updatedEvents[0].preData).toEqual({ hideEventDetails: true }); - await service.delete(property, 'test-user'); + await service.delete(property, 'test-user', TEST_USER_ID); }); test('Can update setting', async () => { const { eventStore } = stores; const someData = { some: 'blob' }; - await service.insert('updated-setting', someData, 'test-user', false); + await service.insert( + 'updated-setting', + someData, + 'test-user', + TEST_USER_ID, + false, + ); await service.insert( 'updated-setting', { ...someData, test: 'fun' }, 'test-user', + TEST_USER_ID, false, ); const updatedEvents = await eventStore.searchEvents({ diff --git a/src/test/e2e/services/state-service.e2e.test.ts b/src/test/e2e/services/state-service.e2e.test.ts index 8ef3a4d64322..4fceb0c5952f 100644 --- a/src/test/e2e/services/state-service.e2e.test.ts +++ b/src/test/e2e/services/state-service.e2e.test.ts @@ -131,7 +131,7 @@ test('Exporting featureEnvironmentVariants should work', async () => { expect( exportedData.featureEnvironments.find( (fE) => fE.featureName === 'Some-feature', - ).variants, + )!.variants, ).toHaveLength(3); }); @@ -140,6 +140,7 @@ test('Should import variants from old format and convert to new format (per envi data: oldFormat, keepExisting: false, dropBeforeImport: true, + userId: -9999, }); const featureEnvironments = await stores.featureEnvironmentStore.getAll(); expect(featureEnvironments).toHaveLength(6); // There are 3 environments enabled and 2 features @@ -154,12 +155,14 @@ test('Should import variants in new format (per environment)', async () => { data: oldFormat, keepExisting: false, dropBeforeImport: true, + userId: -9999, }); const exportedJson = await stateService.export({}); await stateService.import({ data: exportedJson, keepExisting: false, dropBeforeImport: true, + userId: -9999, }); const featureEnvironments = await stores.featureEnvironmentStore.getAll(); expect(featureEnvironments).toHaveLength(6); // 3 environments, 2 features === 6 rows @@ -187,6 +190,7 @@ test('Importing states with deprecated strategies should keep their deprecated s userName: 'strategy-importer', dropBeforeImport: true, keepExisting: false, + userId: -9999, }); const deprecatedStrategy = await stores.strategyStore.get('deprecatedstrat'); @@ -199,6 +203,7 @@ test('Exporting a deprecated strategy and then importing it should keep correct keepExisting: false, dropBeforeImport: true, userName: 'strategy importer', + userId: -9999, }); const rolloutRandom = await stores.strategyStore.get( 'gradualRolloutRandom', diff --git a/src/test/e2e/services/user-service.e2e.test.ts b/src/test/e2e/services/user-service.e2e.test.ts index b6e702be4bc9..c030ddd0a26a 100644 --- a/src/test/e2e/services/user-service.e2e.test.ts +++ b/src/test/e2e/services/user-service.e2e.test.ts @@ -211,6 +211,7 @@ test('should not login user if simple auth is disabled', async () => { simpleAuthSettingsKey, { disabled: true }, randomId(), + -9999, true, ); diff --git a/src/test/e2e/stores/event-store.e2e.test.ts b/src/test/e2e/stores/event-store.e2e.test.ts index 916fdf611007..cc64b1624284 100644 --- a/src/test/e2e/stores/event-store.e2e.test.ts +++ b/src/test/e2e/stores/event-store.e2e.test.ts @@ -15,6 +15,7 @@ import { IUnleashStores } from '../../../lib/types'; let db; let stores: IUnleashStores; let eventStore: IEventStore; +const TEST_USER_ID = -9999; beforeAll(async () => { db = await dbInit('event_store_serial', getLogger); @@ -35,6 +36,7 @@ test('Should include id and createdAt when saving', async () => { const event1 = { type: APPLICATION_CREATED, createdBy: '127.0.0.1', + createdByUserId: TEST_USER_ID, data: { clientIp: '127.0.0.1', appName: 'test1', @@ -57,6 +59,7 @@ test('Should include empty tags array for new event', async () => { const event = { type: FEATURE_CREATED, createdBy: 'me@mail.com', + createdByUserId: TEST_USER_ID, data: { name: 'someName', enabled: true, @@ -83,6 +86,7 @@ test('Should be able to store multiple events at once', async () => { jest.useFakeTimers(); const event1 = { type: APPLICATION_CREATED, + createdByUserId: TEST_USER_ID, createdBy: '127.0.0.1', data: { clientIp: '127.0.0.1', @@ -91,6 +95,7 @@ test('Should be able to store multiple events at once', async () => { }; const event2 = { type: APPLICATION_CREATED, + createdByUserId: TEST_USER_ID, createdBy: '127.0.0.1', data: { clientIp: '127.0.0.1', @@ -99,6 +104,7 @@ test('Should be able to store multiple events at once', async () => { }; const event3 = { type: APPLICATION_CREATED, + createdByUserId: TEST_USER_ID, createdBy: '127.0.0.1', data: { clientIp: '127.0.0.1', @@ -122,6 +128,7 @@ test('Should get all stored events', async () => { const event = { type: FEATURE_CREATED, createdBy: 'me@mail.com', + createdByUserId: TEST_USER_ID, data: { name: 'someName', enabled: true, @@ -139,6 +146,7 @@ test('Should get all stored events', async () => { test('Should delete stored event', async () => { const event = { type: FEATURE_CREATED, + createdByUserId: TEST_USER_ID, createdBy: 'me@mail.com', data: { name: 'someName', @@ -163,6 +171,7 @@ test('Should get stored event by id', async () => { const event = { type: FEATURE_CREATED, createdBy: 'me@mail.com', + createdByUserId: TEST_USER_ID, data: { name: 'someName', enabled: true, @@ -197,6 +206,8 @@ test('Should get all events of type', async () => { project: data.project, featureName: data.name, createdBy: 'test-user', + createdByUserId: TEST_USER_ID, + data, }) : new FeatureDeletedEvent({ @@ -204,6 +215,8 @@ test('Should get all events of type', async () => { preData: data, featureName: data.name, createdBy: 'test-user', + createdByUserId: TEST_USER_ID, + tags: [], }); return eventStore.store(event);