From bfa82d79bf245058612081062345e0ca7c38b7b4 Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Thu, 14 Dec 2023 13:45:25 +0100 Subject: [PATCH] feat: adds created_by_user_id to all events (#5619) ### What Adds `createdByUserId` to all events exposed by unleash. In addition this PR updates all tests and usages of the methods in this codebase to include the required number. --- src/lib/addons/datadog.test.ts | 7 + .../addons/feature-event-formatter-md.test.ts | 17 ++ src/lib/addons/slack-app.test.ts | 2 + src/lib/addons/slack.test.ts | 9 + src/lib/addons/teams.test.ts | 6 + src/lib/addons/webhook.test.ts | 5 + src/lib/db/event-store.ts | 4 + .../dependent-features-service.ts | 11 + .../export-import-controller.ts | 6 +- .../export-import-service.ts | 9 + .../export-import.e2e.test.ts | 4 +- .../archive-feature-toggle-controller.ts | 3 +- .../feature-toggle-controller.ts | 7 + .../feature-toggle/feature-toggle-service.ts | 82 +++++- .../feature-toggle-legacy-controller.ts | 62 ++++- .../tests/feature-toggle-service.e2e.test.ts | 57 ++++- .../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 | 69 ++++- .../environment-service.ts | 18 +- .../project-environments/environments.ts | 4 + .../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/openapi/spec/event-schema.ts | 6 + 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 | 7 +- 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 | 115 +++++---- src/lib/services/pat-service.ts | 2 + src/lib/services/project-service.ts | 34 ++- 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/services/user-service.ts | 3 + src/lib/types/core.ts | 13 +- src/lib/types/events.ts | 241 ++++++++++++++---- 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 | 75 +++++- 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 + 88 files changed, 1340 insertions(+), 287 deletions(-) diff --git a/src/lib/addons/datadog.test.ts b/src/lib/addons/datadog.test.ts index 2d55e76ce9..2f57c263bb 100644 --- a/src/lib/addons/datadog.test.ts +++ b/src/lib/addons/datadog.test.ts @@ -45,6 +45,7 @@ test('Should call datadog webhook', async () => { createdAt: new Date(), type: FEATURE_CREATED, createdBy: 'some@user.com', + createdByUserId: -1337, featureName: 'some-toggle', data: { name: 'some-toggle', @@ -74,6 +75,7 @@ test('Should call datadog webhook for archived toggle', async () => { createdAt: new Date(), type: FEATURE_ARCHIVED, createdBy: 'some@user.com', + createdByUserId: -1337, featureName: 'some-toggle', data: { name: 'some-toggle', @@ -102,6 +104,7 @@ test('Should call datadog webhook for archived toggle with project info', async type: FEATURE_ARCHIVED, createdBy: 'some@user.com', featureName: 'some-toggle', + createdByUserId: -1337, project: 'some-project', data: { name: 'some-toggle', @@ -129,6 +132,7 @@ test('Should call datadog webhook for toggled environment', async () => { createdAt: new Date(), type: FEATURE_ENVIRONMENT_DISABLED, createdBy: 'some@user.com', + createdByUserId: -1337, environment: 'development', project: 'default', featureName: 'some-toggle', @@ -160,6 +164,7 @@ test('Should include customHeaders in headers when calling service', async () => type: FEATURE_ENVIRONMENT_DISABLED, createdBy: 'some@user.com', environment: 'development', + createdByUserId: -1337, project: 'default', featureName: 'some-toggle', data: { @@ -190,6 +195,7 @@ test('Should not include source_type_name when included in the config', async () createdAt: new Date(), type: FEATURE_ENVIRONMENT_DISABLED, createdBy: 'some@user.com', + createdByUserId: -1337, environment: 'development', project: 'default', featureName: 'some-toggle', @@ -224,6 +230,7 @@ test('Should call datadog webhook with JSON when template set', async () => { createdAt: new Date(), type: FEATURE_CREATED, createdBy: 'some@user.com', + createdByUserId: -1337, featureName: 'some-toggle', data: { name: 'some-toggle', diff --git a/src/lib/addons/feature-event-formatter-md.test.ts b/src/lib/addons/feature-event-formatter-md.test.ts index f64eaf2b64..5f2aad1e5b 100644 --- a/src/lib/addons/feature-event-formatter-md.test.ts +++ b/src/lib/addons/feature-event-formatter-md.test.ts @@ -6,6 +6,7 @@ import { FEATURE_STRATEGY_REMOVE, FEATURE_STRATEGY_UPDATE, IEvent, + SYSTEM_USER_ID, } from '../types'; import { FeatureEventFormatterMd } from './feature-event-formatter-md'; @@ -34,6 +35,7 @@ const testCases: [string, IEvent][] = [ id: 920, type: FEATURE_STRATEGY_UPDATE, createdBy: 'user@company.com', + createdByUserId: SYSTEM_USER_ID, createdAt: new Date('2022-06-01T10:03:11.549Z'), data: { id: '3f4bf713-696c-43a4-8ce7-d6c607108858', @@ -67,6 +69,7 @@ const testCases: [string, IEvent][] = [ id: 920, type: FEATURE_STRATEGY_UPDATE, createdBy: 'user@company.com', + createdByUserId: SYSTEM_USER_ID, createdAt: new Date('2022-06-01T10:03:11.549Z'), data: { id: '3f4bf713-696c-43a4-8ce7-d6c607108858', @@ -100,6 +103,7 @@ const testCases: [string, IEvent][] = [ id: 920, type: FEATURE_STRATEGY_UPDATE, createdBy: 'user@company.com', + createdByUserId: SYSTEM_USER_ID, createdAt: new Date('2022-06-01T10:03:11.549Z'), data: { id: '3f4bf713-696c-43a4-8ce7-d6c607108858', @@ -133,6 +137,7 @@ const testCases: [string, IEvent][] = [ id: 920, type: FEATURE_STRATEGY_UPDATE, createdBy: 'user@company.com', + createdByUserId: SYSTEM_USER_ID, createdAt: new Date('2022-06-01T10:03:11.549Z'), data: { id: '3f4bf713-696c-43a4-8ce7-d6c607108858', @@ -174,6 +179,7 @@ const testCases: [string, IEvent][] = [ id: 920, type: FEATURE_STRATEGY_UPDATE, createdBy: 'user@company.com', + createdByUserId: SYSTEM_USER_ID, createdAt: new Date('2022-06-01T10:03:11.549Z'), data: { id: '3f4bf713-696c-43a4-8ce7-d6c607108858', @@ -207,6 +213,7 @@ const testCases: [string, IEvent][] = [ id: 919, type: FEATURE_STRATEGY_ADD, createdBy: 'user@company.com', + createdByUserId: SYSTEM_USER_ID, createdAt: new Date('2022-06-01T10:03:08.290Z'), data: { id: '3f4bf713-696c-43a4-8ce7-d6c607108858', @@ -231,6 +238,7 @@ const testCases: [string, IEvent][] = [ id: 918, type: FEATURE_STRATEGY_REMOVE, createdBy: 'user@company.com', + createdByUserId: SYSTEM_USER_ID, createdAt: new Date('2022-06-01T10:03:00.229Z'), data: null, preData: { @@ -253,6 +261,7 @@ const testCases: [string, IEvent][] = [ id: 39, type: FEATURE_STRATEGY_UPDATE, createdBy: 'admin', + createdByUserId: SYSTEM_USER_ID, createdAt: new Date('2023-02-20T20:23:28.791Z'), data: { id: 'f2d34aac-52ec-49d2-82d3-08d710e89eaa', @@ -310,6 +319,7 @@ const testCases: [string, IEvent][] = [ type: FEATURE_STRATEGY_UPDATE, createdBy: 'admin', createdAt: new Date('2023-02-20T20:23:28.791Z'), + createdByUserId: SYSTEM_USER_ID, data: { id: 'f2d34aac-52ec-49d2-82d3-08d710e89eaa', name: 'default', @@ -346,6 +356,7 @@ const testCases: [string, IEvent][] = [ id: 920, type: FEATURE_STRATEGY_UPDATE, createdBy: 'user@company.com', + createdByUserId: SYSTEM_USER_ID, createdAt: new Date('2022-06-01T10:03:11.549Z'), data: { name: 'userWithId', @@ -385,6 +396,7 @@ const testCases: [string, IEvent][] = [ id: 920, type: FEATURE_STRATEGY_UPDATE, createdBy: 'user@company.com', + createdByUserId: SYSTEM_USER_ID, createdAt: new Date('2022-06-01T10:03:11.549Z'), data: { name: 'remoteAddress', @@ -421,6 +433,7 @@ const testCases: [string, IEvent][] = [ type: FEATURE_STRATEGY_UPDATE, createdBy: 'user@company.com', createdAt: new Date('2022-06-01T10:03:11.549Z'), + createdByUserId: SYSTEM_USER_ID, data: { name: 'applicationHostname', constraints: [ @@ -456,6 +469,7 @@ const testCases: [string, IEvent][] = [ type: FEATURE_STRATEGY_UPDATE, createdBy: 'user@company.com', createdAt: new Date('2022-06-01T10:03:11.549Z'), + createdByUserId: SYSTEM_USER_ID, data: { name: 'newStrategy', constraints: [ @@ -491,6 +505,7 @@ const testCases: [string, IEvent][] = [ type: CHANGE_REQUEST_SCHEDULED, createdBy: 'user@company.com', createdAt: new Date('2022-06-01T10:03:11.549Z'), + createdByUserId: SYSTEM_USER_ID, data: { changeRequestId: 1, }, @@ -508,6 +523,7 @@ const testCases: [string, IEvent][] = [ type: CHANGE_REQUEST_SCHEDULED_APPLICATION_SUCCESS, createdBy: 'user@company.com', createdAt: new Date('2022-06-01T10:03:11.549Z'), + createdByUserId: SYSTEM_USER_ID, data: { changeRequestId: 1, }, @@ -524,6 +540,7 @@ const testCases: [string, IEvent][] = [ id: 920, type: CHANGE_REQUEST_SCHEDULED_APPLICATION_FAILURE, createdBy: 'user@company.com', + createdByUserId: SYSTEM_USER_ID, createdAt: new Date('2022-06-01T10:03:11.549Z'), data: { changeRequestId: 1, diff --git a/src/lib/addons/slack-app.test.ts b/src/lib/addons/slack-app.test.ts index abfaa69a30..4c49646483 100644 --- a/src/lib/addons/slack-app.test.ts +++ b/src/lib/addons/slack-app.test.ts @@ -1,6 +1,7 @@ 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[] = []; @@ -44,6 +45,7 @@ describe('SlackAppAddon', () => { id: 1, createdAt: new Date(), type: FEATURE_ENVIRONMENT_ENABLED, + createdByUserId: SYSTEM_USER_ID, createdBy: 'some@user.com', project: 'default', featureName: 'some-toggle', diff --git a/src/lib/addons/slack.test.ts b/src/lib/addons/slack.test.ts index 416ffc1d70..06d03de2e9 100644 --- a/src/lib/addons/slack.test.ts +++ b/src/lib/addons/slack.test.ts @@ -9,6 +9,7 @@ import { Logger } from '../logger'; import SlackAddon from './slack'; import noLogger from '../../test/fixtures/no-logger'; +import { SYSTEM_USER_ID } from '../types'; let fetchRetryCalls: any[] = []; @@ -44,6 +45,7 @@ test('Should call slack webhook', async () => { id: 1, createdAt: new Date(), type: FEATURE_CREATED, + createdByUserId: SYSTEM_USER_ID, createdBy: 'some@user.com', project: 'default', featureName: 'some-toggle', @@ -74,6 +76,7 @@ test('Should call slack webhook for archived toggle', async () => { const event: IEvent = { id: 2, createdAt: new Date(), + createdByUserId: SYSTEM_USER_ID, type: FEATURE_ARCHIVED, featureName: 'some-toggle', createdBy: 'some@user.com', @@ -101,6 +104,7 @@ test('Should call slack webhook for archived toggle with project info', async () const event: IEvent = { id: 2, createdAt: new Date(), + createdByUserId: SYSTEM_USER_ID, type: FEATURE_ARCHIVED, featureName: 'some-toggle', project: 'some-project', @@ -129,6 +133,7 @@ test(`Should call webhook for toggled environment`, async () => { const event: IEvent = { id: 2, createdAt: new Date(), + createdByUserId: SYSTEM_USER_ID, type: FEATURE_ENVIRONMENT_DISABLED, createdBy: 'some@user.com', environment: 'development', @@ -159,6 +164,7 @@ test('Should use default channel', async () => { const event: IEvent = { id: 3, createdAt: new Date(), + createdByUserId: SYSTEM_USER_ID, type: FEATURE_CREATED, createdBy: 'some@user.com', featureName: 'some-toggle', @@ -189,6 +195,7 @@ test('Should override default channel with data from tag', async () => { const event: IEvent = { id: 4, createdAt: new Date(), + createdByUserId: SYSTEM_USER_ID, type: FEATURE_CREATED, createdBy: 'some@user.com', featureName: 'some-toggle', @@ -225,6 +232,7 @@ test('Should post to all channels in tags', async () => { const event: IEvent = { id: 5, createdAt: new Date(), + createdByUserId: SYSTEM_USER_ID, type: FEATURE_CREATED, createdBy: 'some@user.com', featureName: 'some-toggle', @@ -269,6 +277,7 @@ test('Should include custom headers from parameters in call to service', async ( id: 2, createdAt: new Date(), type: FEATURE_ENVIRONMENT_DISABLED, + createdByUserId: SYSTEM_USER_ID, createdBy: 'some@user.com', environment: 'development', project: 'default', diff --git a/src/lib/addons/teams.test.ts b/src/lib/addons/teams.test.ts index 61f43f8e0f..8bbd239e40 100644 --- a/src/lib/addons/teams.test.ts +++ b/src/lib/addons/teams.test.ts @@ -10,6 +10,7 @@ import { import TeamsAddon from './teams'; import noLogger from '../../test/fixtures/no-logger'; +import { SYSTEM_USER_ID } from '../types'; let fetchRetryCalls: any[]; @@ -45,6 +46,7 @@ test('Should call teams webhook', async () => { id: 1, createdAt: new Date(), type: FEATURE_CREATED, + createdByUserId: SYSTEM_USER_ID, createdBy: 'some@user.com', featureName: 'some-toggle', data: { @@ -72,6 +74,7 @@ test('Should call teams webhook for archived toggle', async () => { const event: IEvent = { id: 1, createdAt: new Date(), + createdByUserId: SYSTEM_USER_ID, type: FEATURE_ARCHIVED, createdBy: 'some@user.com', featureName: 'some-toggle', @@ -98,6 +101,7 @@ test('Should call teams webhook for archived toggle with project info', async () const event: IEvent = { id: 1, createdAt: new Date(), + createdByUserId: SYSTEM_USER_ID, type: FEATURE_ARCHIVED, createdBy: 'some@user.com', featureName: 'some-toggle', @@ -125,6 +129,7 @@ test(`Should call teams webhook for toggled environment`, async () => { const event: IEvent = { id: 2, createdAt: new Date(), + createdByUserId: SYSTEM_USER_ID, type: FEATURE_ENVIRONMENT_DISABLED, createdBy: 'some@user.com', environment: 'development', @@ -154,6 +159,7 @@ test('Should include custom headers in call to teams', async () => { const event: IEvent = { id: 2, createdAt: new Date(), + createdByUserId: SYSTEM_USER_ID, type: FEATURE_ENVIRONMENT_DISABLED, createdBy: 'some@user.com', environment: 'development', diff --git a/src/lib/addons/webhook.test.ts b/src/lib/addons/webhook.test.ts index 43174b95df..95d281319d 100644 --- a/src/lib/addons/webhook.test.ts +++ b/src/lib/addons/webhook.test.ts @@ -5,6 +5,7 @@ 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[] = []; @@ -36,6 +37,7 @@ test('Should handle event without "bodyTemplate"', () => { const event: IEvent = { id: 1, createdAt: new Date(), + createdByUserId: SYSTEM_USER_ID, type: FEATURE_CREATED, createdBy: 'some@user.com', featureName: 'some-toggle', @@ -61,6 +63,7 @@ test('Should format event with "bodyTemplate"', () => { const event: IEvent = { id: 1, createdAt: new Date(), + createdByUserId: SYSTEM_USER_ID, type: FEATURE_CREATED, createdBy: 'some@user.com', featureName: 'some-toggle', @@ -90,6 +93,7 @@ test('Should format event with "authorization"', () => { const event: IEvent = { id: 1, createdAt: new Date(), + createdByUserId: SYSTEM_USER_ID, type: FEATURE_CREATED, createdBy: 'some@user.com', featureName: 'some-toggle', @@ -120,6 +124,7 @@ test('Should handle custom headers', async () => { const event: IEvent = { id: 1, createdAt: new Date(), + createdByUserId: SYSTEM_USER_ID, type: FEATURE_CREATED, createdBy: 'some@user.com', featureName: 'some-toggle', diff --git a/src/lib/db/event-store.ts b/src/lib/db/event-store.ts index 524d016e32..d3132efe3a 100644 --- a/src/lib/db/event-store.ts +++ b/src/lib/db/event-store.ts @@ -20,6 +20,7 @@ const EVENT_COLUMNS = [ 'type', 'created_by', 'created_at', + 'created_by_user_id', 'data', 'pre_data', 'tags', @@ -74,6 +75,7 @@ export interface IEventTable { type: string; created_by: string; created_at: Date; + created_by_user_id: number; data?: any; pre_data?: any; feature_name?: string; @@ -364,6 +366,7 @@ class EventStore implements IEventStore { type: row.type as IEventType, createdBy: row.created_by, createdAt: row.created_at, + createdByUserId: row.created_by_user_id, data: row.data, preData: row.pre_data, tags: row.tags || [], @@ -377,6 +380,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/dependent-features/dependent-features-service.ts b/src/lib/features/dependent-features/dependent-features-service.ts index 830b7f1a37..8ff19bfd9f 100644 --- a/src/lib/features/dependent-features/dependent-features-service.ts +++ b/src/lib/features/dependent-features/dependent-features-service.ts @@ -50,6 +50,7 @@ export class DependentFeaturesService { projectId, }: { featureName: string; newFeatureName: string; projectId: string }, user: string, + userId: number, ) { const parents = await this.dependentFeaturesReadModel.getParents(featureName); @@ -63,6 +64,7 @@ export class DependentFeaturesService { variants: parent.variants, }, user, + userId, ), ), ); @@ -79,6 +81,7 @@ export class DependentFeaturesService { { child, projectId }, dependentFeature, extractUsernameFromUser(user), + user.id, ); } @@ -86,6 +89,7 @@ export class DependentFeaturesService { { child, projectId }: { child: string; projectId: string }, dependentFeature: CreateDependentFeatureSchema, user: string, + userId: number, ): Promise { const { enabled, feature: parent, variants } = dependentFeature; @@ -146,6 +150,7 @@ export class DependentFeaturesService { project: projectId, featureName: child, createdBy: user, + createdByUserId: userId, data: { feature: parent, enabled: featureDependency.enabled, @@ -165,6 +170,7 @@ export class DependentFeaturesService { dependency, projectId, extractUsernameFromUser(user), + user.id, ); } @@ -172,6 +178,7 @@ export class DependentFeaturesService { dependency: FeatureDependencyId, projectId: string, user: string, + userId: number, ): Promise { await this.dependentFeaturesStore.delete(dependency); await this.eventService.storeEvent({ @@ -179,6 +186,7 @@ export class DependentFeaturesService { project: projectId, featureName: dependency.child, createdBy: user, + createdByUserId: userId, data: { feature: dependency.parent }, }); } @@ -194,6 +202,7 @@ export class DependentFeaturesService { features, projectId, extractUsernameFromUser(user), + user.id, ); } @@ -201,6 +210,7 @@ export class DependentFeaturesService { features: string[], projectId: string, user: string, + userId: number, ): Promise { await this.dependentFeaturesStore.deleteAll(features); await this.eventService.storeEvents( @@ -209,6 +219,7 @@ export class DependentFeaturesService { project: projectId, featureName: feature, createdBy: user, + createdByUserId: userId, })), ); } diff --git a/src/lib/features/export-import-toggles/export-import-controller.ts b/src/lib/features/export-import-toggles/export-import-controller.ts index 1bf097d6e3..eef65ef4f6 100644 --- a/src/lib/features/export-import-toggles/export-import-controller.ts +++ b/src/lib/features/export-import-toggles/export-import-controller.ts @@ -120,7 +120,11 @@ class ExportImportController extends Controller { const query = req.body; const userName = extractUsername(req); - const data = await this.exportService.export(query, userName); + const data = await this.exportService.export( + query, + userName, + req.user.id, + ); this.openApiService.respondWithValidation( 200, 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 729db717cb..b8e74c95cc 100644 --- a/src/lib/features/export-import-toggles/export-import-service.ts +++ b/src/lib/features/export-import-toggles/export-import-service.ts @@ -67,6 +67,7 @@ export type IExportService = { export( query: ExportQuerySchema, userName: string, + userId: number, ): Promise; }; @@ -287,6 +288,7 @@ export default class ExportImportService environment: cleanedDto.environment, type: FEATURES_IMPORTED, createdBy: extractUsernameFromUser(user), + createdByUserId: user.id, }); } @@ -387,6 +389,7 @@ export default class ExportImportService value: tag.tagValue, }, extractUsernameFromUser(user), + user.id, ); } } @@ -404,6 +407,7 @@ export default class ExportImportService stickiness: contextField.stickiness, }, extractUsernameFromUser(user), + user.id, ), ), ); @@ -417,6 +421,7 @@ export default class ExportImportService ? this.tagTypeService.createTagType( tagType, extractUsernameFromUser(user), + user.id, ) : Promise.resolve(); }), @@ -457,6 +462,7 @@ export default class ExportImportService rest as FeatureToggleDTO, username, feature.name, + user.id, ); } else { await this.featureToggleService.validateName(feature.name); @@ -465,6 +471,7 @@ export default class ExportImportService dto.project, rest as FeatureToggleDTO, username, + user.id, ); } } @@ -777,6 +784,7 @@ export default class ExportImportService async export( query: ExportQuerySchema, userName: string, + userId: number, ): Promise { const featureNames = typeof query.tag === 'string' @@ -901,6 +909,7 @@ export default class ExportImportService await this.eventService.storeEvent({ type: FEATURES_EXPORTED, createdBy: userName, + createdByUserId: userId, data: result, }); 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 13ec831f0b..f5001a6ad4 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 @@ -20,7 +20,6 @@ import { import { DEFAULT_ENV } from '../../util'; import { ContextFieldSchema, - CreateDependentFeatureSchema, ImportTogglesSchema, UpsertSegmentSchema, VariantsSchema, @@ -63,11 +62,13 @@ const createToggle = async ( tags: string[] = [], projectId: string = 'default', username: string = 'test', + userId: number = -9999, ) => { await app.services.featureToggleServiceV2.createFeatureToggle( projectId, toggle, username, + -9999, ); if (strategy) { await app.services.featureToggleServiceV2.createStrategy( @@ -89,6 +90,7 @@ const createToggle = async ( value: tag, }, username, + userId, ); }), ); diff --git a/src/lib/features/feature-toggle/archive-feature-toggle-controller.ts b/src/lib/features/feature-toggle/archive-feature-toggle-controller.ts index ba530f5d53..8d3312c07d 100644 --- a/src/lib/features/feature-toggle/archive-feature-toggle-controller.ts +++ b/src/lib/features/feature-toggle/archive-feature-toggle-controller.ts @@ -176,7 +176,7 @@ export default class ArchiveController extends Controller { ): Promise { const { featureName } = req.params; const user = extractUsername(req); - await this.featureService.deleteFeature(featureName, user); + await this.featureService.deleteFeature(featureName, user, req.user.id); res.status(200).end(); } @@ -191,6 +191,7 @@ export default class ArchiveController extends Controller { this.transactionalFeatureToggleService(tx).reviveFeature( featureName, userName, + req.user.id, ), ); res.status(200).end(); diff --git a/src/lib/features/feature-toggle/feature-toggle-controller.ts b/src/lib/features/feature-toggle/feature-toggle-controller.ts index 04fd67fd8f..e6860fd272 100644 --- a/src/lib/features/feature-toggle/feature-toggle-controller.ts +++ b/src/lib/features/feature-toggle/feature-toggle-controller.ts @@ -659,6 +659,7 @@ export default class ProjectFeaturesController extends Controller { projectId, name, userName, + req.user.id, replaceGroupId, ); @@ -684,6 +685,7 @@ export default class ProjectFeaturesController extends Controller { description: req.body.description || undefined, }, userName, + req.user.id, ); this.openApiService.respondWithValidation( @@ -733,6 +735,7 @@ export default class ProjectFeaturesController extends Controller { }, userName, featureName, + req.user.id, ); this.openApiService.respondWithValidation( @@ -758,6 +761,7 @@ export default class ProjectFeaturesController extends Controller { featureName, extractUsername(req), req.body, + req.user.id, ); this.openApiService.respondWithValidation( 200, @@ -800,6 +804,7 @@ export default class ProjectFeaturesController extends Controller { stale, userName, projectId, + req.user.id, ); res.status(202).end(); } @@ -1108,6 +1113,7 @@ export default class ProjectFeaturesController extends Controller { value, { environment, projectId, featureName }, userName, + req.user.id, ); res.status(200).json(updatedStrategy); } @@ -1123,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/feature-toggle-service.ts b/src/lib/features/feature-toggle/feature-toggle-service.ts index 7e993bb877..8f67997d76 100644 --- a/src/lib/features/feature-toggle/feature-toggle-service.ts +++ b/src/lib/features/feature-toggle/feature-toggle-service.ts @@ -44,6 +44,7 @@ import { SKIP_CHANGE_REQUEST, StrategiesOrderChangedEvent, StrategyIds, + SYSTEM_USER_ID, Unsaved, WeightType, } from '../../types'; @@ -403,6 +404,7 @@ class FeatureToggleService { featureName: string, createdBy: string, operations: Operation[], + createdByUserId: number, ): Promise { const featureToggle = await this.getFeatureMetadata(featureName); @@ -421,6 +423,7 @@ class FeatureToggleService { newDocument, createdBy, featureName, + createdByUserId, ); if (featureToggle.stale !== newDocument.stale) { @@ -430,6 +433,7 @@ class FeatureToggleService { project, featureName, createdBy, + createdByUserId, }), ); } @@ -472,6 +476,7 @@ class FeatureToggleService { context, sortOrders, createdBy, + user?.id || SYSTEM_USER_ID, ); } @@ -479,6 +484,7 @@ class FeatureToggleService { context: IFeatureStrategyContext, sortOrders: SetStrategySortOrderSchema, createdBy: string, + createdByUserId: number, ): Promise> { const { featureName, environment, projectId: project } = context; const existingOrder = ( @@ -536,6 +542,7 @@ class FeatureToggleService { createdBy, preData: eventPreData, data: eventData, + createdByUserId, }); await this.eventService.storeEvent(event); } @@ -555,6 +562,7 @@ class FeatureToggleService { strategyConfig, context, createdBy, + user?.id || SYSTEM_USER_ID, ); } @@ -562,6 +570,7 @@ class FeatureToggleService { strategyConfig: Unsaved, context: IFeatureStrategyContext, createdBy: string, + createdByUserId: number, ): Promise> { const { featureName, projectId, environment } = context; await this.validateFeatureBelongsToProject(context); @@ -638,6 +647,7 @@ class FeatureToggleService { createdBy, environment, data: strategy, + createdByUserId, }), ); return strategy; @@ -674,7 +684,13 @@ class FeatureToggleService { context.environment, user, ); - return this.unprotectedUpdateStrategy(id, updates, context, userName); + return this.unprotectedUpdateStrategy( + id, + updates, + context, + userName, + user, + ); } async optionallyDisableFeature( @@ -682,6 +698,7 @@ class FeatureToggleService { environment: string, projectId: string, userName: string, + user?: IUser, ): Promise { const feature = await this.getFeature({ featureName }); @@ -696,6 +713,7 @@ class FeatureToggleService { environment, false, userName, + user, ); } } @@ -705,6 +723,7 @@ class FeatureToggleService { updates: Partial, context: IFeatureStrategyContext, userName: string, + user?: IUser, ): Promise> { const { projectId, environment, featureName } = context; const existingStrategy = await this.featureStrategiesStore.get(id); @@ -760,6 +779,7 @@ class FeatureToggleService { createdBy: userName, data, preData, + createdByUserId: user?.id || SYSTEM_USER_ID, }), ); await this.optionallyDisableFeature( @@ -767,6 +787,7 @@ class FeatureToggleService { environment, projectId, userName, + user, ); return data; } @@ -779,6 +800,7 @@ class FeatureToggleService { value: string | number, context: IFeatureStrategyContext, userName: string, + createdByUserId: number, ): Promise> { const { projectId, environment, featureName } = context; @@ -809,6 +831,7 @@ class FeatureToggleService { createdBy: userName, data, preData, + createdByUserId, }), ); return data; @@ -837,13 +860,14 @@ class FeatureToggleService { context.environment, user, ); - return this.unprotectedDeleteStrategy(id, context, createdBy); + return this.unprotectedDeleteStrategy(id, context, createdBy, user); } async unprotectedDeleteStrategy( id: string, context: IFeatureStrategyContext, createdBy: string, + createdByUser?: IUser, ): Promise { const existingStrategy = await this.featureStrategiesStore.get(id); const { featureName, projectId, environment } = context; @@ -870,6 +894,7 @@ class FeatureToggleService { environment, false, createdBy, + createdByUser, ); } @@ -881,6 +906,7 @@ class FeatureToggleService { project: projectId, environment, createdBy, + createdByUserId: createdByUser?.id || SYSTEM_USER_ID, preData, }), ); @@ -1053,6 +1079,7 @@ class FeatureToggleService { * * Used to retrieve metadata of all feature toggles defined in Unleash. * @param query - Allow you to limit search based on criteria such as project, tags, namePrefix. See @IFeatureToggleQuery + * @param userId - Used to find / mark features as favorite based on users preferences * @param archived - Return archived or active toggles * @returns */ @@ -1106,6 +1133,7 @@ class FeatureToggleService { projectId: string, value: FeatureToggleDTO, createdBy: string, + createdByUserId: number, isValidated: boolean = false, ): Promise { this.logger.info(`${createdBy} creates feature toggle ${value.name}`); @@ -1155,6 +1183,7 @@ class FeatureToggleService { createdBy, project: projectId, data: createdToggle, + createdByUserId, }), ); @@ -1232,6 +1261,7 @@ class FeatureToggleService { projectId: string, newFeatureName: string, userName: string, + userId: number, replaceGroupId: boolean = true, ): Promise { const changeRequestEnabled = @@ -1262,6 +1292,7 @@ class FeatureToggleService { projectId, newToggle, userName, + userId, ); const variantTasks = newToggle.environments.map((e) => { @@ -1294,6 +1325,7 @@ class FeatureToggleService { this.dependentFeaturesService.cloneDependencies( { featureName, newFeatureName, projectId }, userName, + userId, ); await Promise.all([ @@ -1310,6 +1342,7 @@ class FeatureToggleService { updatedFeature: FeatureToggleDTO, userName: string, featureName: string, + userId: number, ): Promise { await this.validateFeatureBelongsToProject({ featureName, projectId }); @@ -1328,6 +1361,7 @@ class FeatureToggleService { await this.eventService.storeEvent( new FeatureMetadataUpdateEvent({ createdBy: userName, + createdByUserId: userId, data: featureToggle, preData, featureName, @@ -1452,6 +1486,7 @@ class FeatureToggleService { featureName: string, isStale: boolean, createdBy: string, + createdByUserId: number, ): Promise { const feature = await this.featureToggleStore.get(featureName); const { project } = feature; @@ -1464,6 +1499,7 @@ class FeatureToggleService { project, featureName, createdBy, + createdByUserId, }), ); @@ -1485,6 +1521,7 @@ class FeatureToggleService { await this.unprotectedArchiveToggle( featureName, extractUsernameFromUser(user), + user.id, projectId, ); } @@ -1492,6 +1529,7 @@ class FeatureToggleService { async unprotectedArchiveToggle( featureName: string, createdBy: string, + createdByUserId: number, projectId?: string, ): Promise { const feature = await this.featureToggleStore.get(featureName); @@ -1512,6 +1550,7 @@ class FeatureToggleService { [featureName], projectId, createdBy, + createdByUserId, ); } @@ -1519,6 +1558,7 @@ class FeatureToggleService { new FeatureArchivedEvent({ featureName, createdBy, + createdByUserId, project: feature.project, }), ); @@ -1534,6 +1574,7 @@ class FeatureToggleService { featureNames, extractUsernameFromUser(user), projectId, + user.id, ); } @@ -1559,6 +1600,7 @@ class FeatureToggleService { featureNames: string[], createdBy: string, projectId: string, + createdByUserId: number, ): Promise { await Promise.all([ this.validateFeaturesContext(featureNames, projectId), @@ -1572,6 +1614,7 @@ class FeatureToggleService { featureNames, projectId, createdBy, + createdByUserId, ); await this.eventService.storeEvents( @@ -1580,6 +1623,7 @@ class FeatureToggleService { new FeatureArchivedEvent({ featureName: feature.name, createdBy, + createdByUserId, project: feature.project, }), ), @@ -1591,6 +1635,7 @@ class FeatureToggleService { stale: boolean, createdBy: string, projectId: string, + createdByUserId: number, ): Promise { await this.validateFeaturesContext(featureNames, projectId); @@ -1612,6 +1657,7 @@ class FeatureToggleService { project: projectId, featureName: feature.name, createdBy, + createdByUserId, }), ), ); @@ -1666,6 +1712,7 @@ class FeatureToggleService { environment, enabled, createdBy, + user, shouldActivateDisabledStrategies, ); } @@ -1676,6 +1723,7 @@ class FeatureToggleService { environment: string, enabled: boolean, createdBy: string, + user?: IUser, shouldActivateDisabledStrategies = false, ): Promise { const hasEnvironment = @@ -1712,6 +1760,7 @@ class FeatureToggleService { featureName, }, createdBy, + user, ), ), ); @@ -1746,6 +1795,7 @@ class FeatureToggleService { featureName, }, createdBy, + user?.id || SYSTEM_USER_ID, ); } } @@ -1765,6 +1815,7 @@ class FeatureToggleService { featureName, environment, createdBy, + createdByUserId: user?.id || SYSTEM_USER_ID, }), ); } @@ -1775,6 +1826,7 @@ class FeatureToggleService { async storeFeatureUpdatedEventLegacy( featureName: string, createdBy: string, + createdByUserId: number, ): Promise { const feature = await this.getFeatureToggleLegacy(featureName); @@ -1783,6 +1835,7 @@ class FeatureToggleService { await this.eventService.storeEvent({ type: FEATURE_UPDATED, createdBy, + createdByUserId, featureName, data: feature, project: feature.project, @@ -1831,6 +1884,7 @@ class FeatureToggleService { featureName: string, newProject: string, createdBy: string, + createdByUserId: number, ): Promise { const changeRequestEnabled = await this.changeRequestAccessReadModel.isChangeRequestsEnabledForProject( @@ -1858,6 +1912,7 @@ class FeatureToggleService { await this.eventService.storeEvent( new FeatureChangeProjectEvent({ createdBy, + createdByUserId, oldProject, newProject, featureName, @@ -1870,7 +1925,11 @@ class FeatureToggleService { } // TODO: add project id. - async deleteFeature(featureName: string, createdBy: string): Promise { + async deleteFeature( + featureName: string, + createdBy: string, + createdByUserId: number, + ): Promise { await this.validateNoChildren(featureName); const toggle = await this.featureToggleStore.get(featureName); const tags = await this.tagStore.getAllTagsForFeature(featureName); @@ -1881,6 +1940,7 @@ class FeatureToggleService { featureName, project: toggle.project, createdBy, + createdByUserId, preData: toggle, tags, }), @@ -1891,6 +1951,7 @@ class FeatureToggleService { featureNames: string[], projectId: string, createdBy: string, + createdByUserId: number, ): Promise { await this.validateFeaturesContext(featureNames, projectId); await this.validateNoOrphanParents(featureNames); @@ -1912,6 +1973,7 @@ class FeatureToggleService { new FeatureDeletedEvent({ featureName: feature.name, createdBy, + createdByUserId, project: feature.project, preData: feature, tags: tags @@ -1929,6 +1991,7 @@ class FeatureToggleService { featureNames: string[], projectId: string, createdBy: string, + createdByUserId: number, ): Promise { await this.validateFeaturesContext(featureNames, projectId); @@ -1952,6 +2015,7 @@ class FeatureToggleService { new FeatureRevivedEvent({ featureName: feature.name, createdBy, + createdByUserId, project: feature.project, }), ), @@ -1959,7 +2023,11 @@ class FeatureToggleService { } // TODO: add project id. - async reviveFeature(featureName: string, createdBy: string): Promise { + async reviveFeature( + featureName: string, + createdBy: string, + createdByUserId: number, + ): Promise { const toggle = await this.featureToggleStore.revive(featureName); await this.featureToggleStore.disableAllEnvironmentsForFeatures([ featureName, @@ -1967,6 +2035,7 @@ class FeatureToggleService { await this.eventService.storeEvent( new FeatureRevivedEvent({ createdBy, + createdByUserId, featureName, project: toggle.project, }), @@ -2072,6 +2141,7 @@ class FeatureToggleService { project: string, newVariants: IVariant[], createdBy: string, + createdByUserId: number, ): Promise { await variantsArraySchema.validateAsync(newVariants); const fixedVariants = this.fixVariantWeights(newVariants); @@ -2088,6 +2158,7 @@ class FeatureToggleService { project, featureName, createdBy, + createdByUserId, oldVariants, newVariants: featureToggle.variants as IVariant[], }), @@ -2119,6 +2190,7 @@ class FeatureToggleService { new EnvironmentVariantEvent({ featureName, environment, + createdByUserId: user.id, project: projectId, createdBy: user, oldVariants: theOldVariants, @@ -2199,6 +2271,7 @@ class FeatureToggleService { createdBy: user, oldVariants: oldVariants[environment], newVariants: fixedVariants, + createdByUserId: user.id, }), ), ); @@ -2317,6 +2390,7 @@ class FeatureToggleService { ({ name, project }) => new PotentiallyStaleOnEvent({ featureName: name, + createdByUserId: SYSTEM_USER_ID, project, }), ), 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 8caffa6f90..472c9159b9 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 9864394efe..c36534d603 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,8 @@ import { IUnleashStores, IVariant, SKIP_CHANGE_REQUEST, + SYSTEM_USER, + SYSTEM_USER_ID, } from '../../../types'; import EnvironmentService from '../../project-environments/environment-service'; import { ForbiddenError, PatternError, PermissionError } from '../../../error'; @@ -27,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'], @@ -62,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'; @@ -78,6 +79,7 @@ test('Should create feature toggle strategy configuration', async () => { name: 'Demo', }, 'test', + TEST_USER_ID, ); const createdConfig = await service.createStrategy( @@ -106,6 +108,7 @@ test('Should be able to update existing strategy configuration', async () => { name: featureName, }, 'test', + TEST_USER_ID, ); const createdConfig = await service.createStrategy( @@ -142,6 +145,7 @@ test('Should be able to get strategy by id', async () => { name: featureName, }, userName, + TEST_USER_ID, ); const createdConfig = await service.createStrategy( @@ -167,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( @@ -176,6 +181,7 @@ test('should ignore name in the body when updating feature toggle', async () => description: 'Second toggle', }, userName, + TEST_USER_ID, ); const update = { @@ -183,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, @@ -205,6 +217,7 @@ test('should not get empty rows as features', async () => { description: 'First toggle', }, userName, + TEST_USER_ID, ); await service.createFeatureToggle( @@ -214,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; @@ -254,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 @@ -269,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, @@ -292,6 +322,7 @@ test('cloning a feature toggle copies variant environments correctly', async () name: newToggleName, }, 'test', + TEST_USER_ID, ); await stores.environmentStore.create({ @@ -322,6 +353,7 @@ test('cloning a feature toggle copies variant environments correctly', async () 'default', clonedToggleName, 'test-user', + SYSTEM_USER_ID, true, ); @@ -335,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 () => { @@ -350,6 +382,7 @@ test('cloning a feature toggle not allowed for change requests enabled', async ( 'default', 'clonedToggleName', 'test-user', + SYSTEM_USER_ID, true, ), ).rejects.toEqual( @@ -365,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.`, @@ -393,6 +426,7 @@ test('Cloning a feature toggle also clones segments correctly', async () => { name: featureName, }, 'test-user', + TEST_USER_ID, ); const config: Omit = { @@ -413,6 +447,7 @@ test('Cloning a feature toggle also clones segments correctly', async () => { 'default', clonedFeatureName, 'test-user', + SYSTEM_USER_ID, true, ); @@ -431,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 @@ -510,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 = { @@ -580,6 +617,7 @@ test('getPlaygroundFeatures should return ids and titles (if they exist) on clie name: featureName, }, userName, + TEST_USER_ID, ); await service.createStrategy( @@ -673,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 a26742919b..5e9bc5fa76 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 9ba5286069..b8cab5483e 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 892970a017..bbbaadb82e 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 4e8d89af2f..8686762ede 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 4f92cd509f..50cd32f8e7 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', }); @@ -76,7 +81,8 @@ test('Can connect environment to project', async () => { type: 'project-environment-added', project: 'default', environment: 'test-connection', - createdBy: 'user', + createdBy: SYSTEM_USER.username, + createdByUserId: SYSTEM_USER.id, }); }); @@ -88,8 +94,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 +127,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', @@ -125,7 +142,8 @@ test('Can remove environment from project', async () => { type: 'project-environment-removed', project: 'default', environment: 'removal-test', - createdBy: 'user', + createdBy: SYSTEM_USER.username, + createdByUserId: SYSTEM_USER.id, }); }); @@ -134,13 +152,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 +191,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 +287,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 311df68e60..616fddf715 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 7a875e3d5e..71f1470b31 100644 --- a/src/lib/features/project-environments/environments.ts +++ b/src/lib/features/project-environments/environments.ts @@ -4,6 +4,7 @@ import { IUnleashConfig, IUnleashServices, serializeDates, + SYSTEM_USER_ID, UPDATE_PROJECT, } from '../../types'; import { Logger } from '../../logger'; @@ -145,6 +146,7 @@ export default class EnvironmentsController extends Controller { environment, projectId, extractUsername(req), + req.user.id, ), ); @@ -162,6 +164,7 @@ export default class EnvironmentsController extends Controller { environment, projectId, extractUsername(req), + req.user.id, ), ); @@ -184,6 +187,7 @@ export default class EnvironmentsController extends Controller { projectId, strategy, extractUsername(req), + req.user.id || SYSTEM_USER_ID, ), ); diff --git a/src/lib/features/scheduler/scheduler-service.test.ts b/src/lib/features/scheduler/scheduler-service.test.ts index afb4fc035f..87a55938d5 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 e04464db0f..b330620af8 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 157484223f..8ec5c25f1d 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 abd44622da..f5c6c73532 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/openapi/spec/event-schema.ts b/src/lib/openapi/spec/event-schema.ts index fa11b61602..15f999784b 100644 --- a/src/lib/openapi/spec/event-schema.ts +++ b/src/lib/openapi/spec/event-schema.ts @@ -52,6 +52,12 @@ export const eventSchema = { description: 'Which user created this event', example: 'johndoe', }, + createdByUserId: { + type: 'number', + description: 'The is of the user that created this event', + example: 1337, + nullable: true, + }, environment: { type: 'string', description: diff --git a/src/lib/routes/admin-api/addon.ts b/src/lib/routes/admin-api/addon.ts index 581e4b09c5..7ded8039c5 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 6dc1b9b02d..a494c28590 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 2cfd9127f6..9c4aca8c8d 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 10db5ee4a5..45ffaa079f 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 db37ba9321..6f63decc1e 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 62d6fd80f5..3a1631794f 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 8fd915396f..63f2d1730d 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 f027f2d6c2..8d85d33775 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 3ae135e9da..efefd19e6d 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 64561307bd..aeea572cdf 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 b358f26656..652a7fa0a6 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 65afe240ec..a7b6b71535 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 93d99c2750..26a5382e34 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 fd0a544a68..2ac299a282 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, @@ -198,6 +200,7 @@ test('user with custom root role should get a user root role', async () => { expect(events[0]).toEqual({ type: ROLE_CREATED, createdBy: 'unknown', + createdByUserId: -9999, data: { id: 0, name: 'custom-root-role', @@ -259,7 +262,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 10585a0719..5252857b9b 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 8c7451500e..535081985e 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 622c34afa6..c063e43f8f 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 cf8c5ab1ba..49c8af45f7 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 da968332da..a7bb939700 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 7b31d891a6..c350afb7c8 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 91614c1c46..226f88df61 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 6ad41b3325..e934718a79 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 5639a823dd..515038bd35 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 96380562f6..0205729563 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 1827e28a45..b5d5b8d539 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 c0264f14e7..26920c7bd9 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,6 +231,57 @@ export class GroupService { return this.groupStore.getProjectGroupRoles(projectId); } + async syncExternalGroups( + userId: number, + externalGroups: string[], + createdBy?: string, + createdByUserId?: number, + ): Promise { + if (Array.isArray(externalGroups)) { + const newGroups = await this.groupStore.getNewGroupsForExternalUser( + userId, + externalGroups, + ); + await this.groupStore.addUserToGroups( + userId, + newGroups.map((g) => g.id), + createdBy, + ); + const oldGroups = await this.groupStore.getOldGroupsForExternalUser( + userId, + externalGroups, + ); + await this.groupStore.deleteUsersFromGroup(oldGroups); + + const events: IBaseEvent[] = []; + for (const group of newGroups) { + events.push({ + type: GROUP_USER_ADDED, + createdBy: createdBy ?? 'unknown', + createdByUserId: createdByUserId ?? -9999, + data: { + groupId: group.id, + userId, + }, + }); + } + + for (const group of oldGroups) { + events.push({ + type: GROUP_USER_REMOVED, + createdBy: createdBy ?? 'unknown', + createdByUserId: createdByUserId ?? -9999, + preData: { + groupId: group.groupId, + userId, + }, + }); + } + + await this.eventService.storeEvents(events); + } + } + private mapGroupWithUsers( group: IGroup, allGroupUsers: IGroupUser[], @@ -242,54 +305,6 @@ export class GroupService { return { ...group, users: finalUsers }; } - async syncExternalGroups( - userId: number, - externalGroups: string[], - createdBy?: string, - ): Promise { - if (Array.isArray(externalGroups)) { - const newGroups = await this.groupStore.getNewGroupsForExternalUser( - userId, - externalGroups, - ); - await this.groupStore.addUserToGroups( - userId, - newGroups.map((g) => g.id), - createdBy, - ); - const oldGroups = await this.groupStore.getOldGroupsForExternalUser( - userId, - externalGroups, - ); - await this.groupStore.deleteUsersFromGroup(oldGroups); - - const events: IBaseEvent[] = []; - for (const group of newGroups) { - events.push({ - type: GROUP_USER_ADDED, - createdBy: createdBy ?? 'unknown', - data: { - groupId: group.id, - userId, - }, - }); - } - - for (const group of oldGroups) { - events.push({ - type: GROUP_USER_REMOVED, - createdBy: createdBy ?? 'unknown', - preData: { - groupId: group.groupId, - userId, - }, - }); - } - - await this.eventService.storeEvents(events); - } - } - 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 f02bfedbaa..3bdcb108d0 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 7249347123..d83a2e2c76 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, }); @@ -273,10 +275,11 @@ export default class ProjectService { // updated project contains instructions to update the project but it may not represent a whole project const afterData = await this.projectStore.get(updatedProject.id); - await this.eventStore.store({ + await this.eventService.storeEvent({ 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 7bcb64862d..3345d92af7 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 5701029797..f7f3246cd9 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 7565a29ab2..f4c8441174 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 04711d859e..6cc45c4c61 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 366d745993..c068d396ea 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 7b4fdb82c5..fa562f2a62 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 970a8f69d5..20d9b67871 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 fd41b38a60..e7165efcb6 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/services/user-service.ts b/src/lib/services/user-service.ts index 49866ce8d3..bf56b75437 100644 --- a/src/lib/services/user-service.ts +++ b/src/lib/services/user-service.ts @@ -239,6 +239,7 @@ class UserService { await this.eventService.storeEvent( new UserCreatedEvent({ createdBy: this.getCreatedBy(updatedBy), + createdByUserId: user.id, userCreated, }), ); @@ -281,6 +282,7 @@ class UserService { createdBy: this.getCreatedBy(updatedBy), preUser: preUser, postUser: storedUser, + createdByUserId: user.id, }), ); @@ -298,6 +300,7 @@ class UserService { new UserDeletedEvent({ createdBy: this.getCreatedBy(updatedBy), deletedUser: user, + createdByUserId: updatedBy?.id || -1337, }), ); } diff --git a/src/lib/types/core.ts b/src/lib/types/core.ts index 70d1b07697..1475d07972 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 2cc34a588f..501b2dad02 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; @@ -306,12 +308,14 @@ export const IEventTypes = [ PROJECT_ENVIRONMENT_ADDED, PROJECT_ENVIRONMENT_REMOVED, DEFAULT_STRATEGY_UPDATED, + SEGMENT_IMPORT, ] as const; export type IEventType = (typeof IEventTypes)[number]; export interface IBaseEvent { type: IEventType; createdBy: string; + createdByUserId: number; project?: string; environment?: string; featureName?: string; @@ -335,15 +339,24 @@ class BaseEvent implements IBaseEvent { readonly createdBy: string; + readonly createdByUserId: number; + /** + * @param type the type of the event we're creating. * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization + * @param createdByUserId accepts a number representing the internal id of the user creating this event */ - constructor(type: IEventType, createdBy: string | IUser) { + constructor( + type: IEventType, + createdBy: string | IUser, + createdByUserId: number, + ) { this.type = type; this.createdBy = typeof createdBy === 'string' ? createdBy : extractUsernameFromUser(createdBy); + this.createdByUserId = createdByUserId; } } @@ -360,8 +373,13 @@ export class FeatureStaleEvent extends BaseEvent { project: string; featureName: string; createdBy: string | IUser; + createdByUserId: number; }) { - super(p.stale ? FEATURE_STALE_ON : FEATURE_STALE_OFF, p.createdBy); + super( + p.stale ? FEATURE_STALE_ON : FEATURE_STALE_OFF, + p.createdBy, + p.createdByUserId, + ); this.project = p.project; this.featureName = p.featureName; } @@ -383,12 +401,14 @@ export class FeatureEnvironmentEvent extends BaseEvent { featureName: string; environment: string; createdBy: string | IUser; + createdByUserId: number; }) { super( p.enabled ? FEATURE_ENVIRONMENT_ENABLED : FEATURE_ENVIRONMENT_DISABLED, p.createdBy, + p.createdByUserId, ); this.project = p.project; this.featureName = p.featureName; @@ -417,8 +437,9 @@ export class StrategiesOrderChangedEvent extends BaseEvent { createdBy: string | IUser; data: StrategyIds; preData: StrategyIds; + createdByUserId: number; }) { - super(STRATEGY_ORDER_CHANGED, p.createdBy); + super(STRATEGY_ORDER_CHANGED, p.createdBy, p.createdByUserId); const { project, featureName, environment, data, preData } = p; this.project = project; this.featureName = featureName; @@ -446,8 +467,9 @@ export class FeatureVariantEvent extends BaseEvent { createdBy: string | IUser; newVariants: IVariant[]; oldVariants: IVariant[]; + createdByUserId: number; }) { - super(FEATURE_VARIANTS_UPDATED, p.createdBy); + super(FEATURE_VARIANTS_UPDATED, p.createdBy, p.createdByUserId); this.project = p.project; this.featureName = p.featureName; this.data = { variants: p.newVariants }; @@ -476,8 +498,13 @@ export class EnvironmentVariantEvent extends BaseEvent { createdBy: string | IUser; newVariants: IVariant[]; oldVariants: IVariant[]; + createdByUserId: number; }) { - super(FEATURE_ENVIRONMENT_VARIANTS_UPDATED, p.createdBy); + super( + FEATURE_ENVIRONMENT_VARIANTS_UPDATED, + p.createdBy, + p.createdByUserId, + ); this.featureName = p.featureName; this.environment = p.environment; this.project = p.project; @@ -504,8 +531,9 @@ export class FeatureChangeProjectEvent extends BaseEvent { newProject: string; featureName: string; createdBy: string | IUser; + createdByUserId: number; }) { - super(FEATURE_PROJECT_CHANGE, p.createdBy); + super(FEATURE_PROJECT_CHANGE, p.createdBy, p.createdByUserId); const { newProject, oldProject, featureName } = p; this.project = newProject; this.featureName = featureName; @@ -528,8 +556,9 @@ export class FeatureCreatedEvent extends BaseEvent { featureName: string; createdBy: string | IUser; data: FeatureToggle; + createdByUserId: number; }) { - super(FEATURE_CREATED, p.createdBy); + super(FEATURE_CREATED, p.createdBy, p.createdByUserId); const { project, featureName, data } = p; this.project = project; this.featureName = featureName; @@ -549,8 +578,9 @@ export class FeatureArchivedEvent extends BaseEvent { project: string; featureName: string; createdBy: string | IUser; + createdByUserId: number; }) { - super(FEATURE_ARCHIVED, p.createdBy); + super(FEATURE_ARCHIVED, p.createdBy, p.createdByUserId); const { project, featureName } = p; this.project = project; this.featureName = featureName; @@ -569,8 +599,9 @@ export class FeatureRevivedEvent extends BaseEvent { project: string; featureName: string; createdBy: string | IUser; + createdByUserId: number; }) { - super(FEATURE_REVIVED, p.createdBy); + super(FEATURE_REVIVED, p.createdBy, p.createdByUserId); const { project, featureName } = p; this.project = project; this.featureName = featureName; @@ -595,8 +626,9 @@ export class FeatureDeletedEvent extends BaseEvent { preData: FeatureToggle; createdBy: string | IUser; tags: ITag[]; + createdByUserId: number; }) { - super(FEATURE_DELETED, p.createdBy); + super(FEATURE_DELETED, p.createdBy, p.createdByUserId); const { project, featureName, preData } = p; this.project = project; this.featureName = featureName; @@ -623,8 +655,9 @@ export class FeatureMetadataUpdateEvent extends BaseEvent { project: string; data: FeatureToggle; preData: FeatureToggle; + createdByUserId: number; }) { - super(FEATURE_METADATA_UPDATED, p.createdBy); + super(FEATURE_METADATA_UPDATED, p.createdBy, p.createdByUserId); const { project, featureName, data, preData } = p; this.project = project; this.featureName = featureName; @@ -651,8 +684,9 @@ export class FeatureStrategyAddEvent extends BaseEvent { environment: string; createdBy: string | IUser; data: IStrategyConfig; + createdByUserId: number; }) { - super(FEATURE_STRATEGY_ADD, p.createdBy); + super(FEATURE_STRATEGY_ADD, p.createdBy, p.createdByUserId); const { project, featureName, environment, data } = p; this.project = project; this.featureName = featureName; @@ -682,8 +716,9 @@ export class FeatureStrategyUpdateEvent extends BaseEvent { createdBy: string | IUser; data: IStrategyConfig; preData: IStrategyConfig; + createdByUserId: number; }) { - super(FEATURE_STRATEGY_UPDATE, p.createdBy); + super(FEATURE_STRATEGY_UPDATE, p.createdBy, p.createdByUserId); const { project, featureName, environment, data, preData } = p; this.project = project; this.featureName = featureName; @@ -711,8 +746,9 @@ export class FeatureStrategyRemoveEvent extends BaseEvent { environment: string; createdBy: string | IUser; preData: IStrategyConfig; + createdByUserId: number; }) { - super(FEATURE_STRATEGY_REMOVE, p.createdBy); + super(FEATURE_STRATEGY_REMOVE, p.createdBy, p.createdByUserId); const { project, featureName, environment, preData } = p; this.project = project; this.featureName = featureName; @@ -731,8 +767,13 @@ export class ProjectUserAddedEvent extends BaseEvent { /** * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization */ - constructor(p: { project: string; createdBy: string | IUser; data: any }) { - super(PROJECT_USER_ADDED, p.createdBy); + constructor(p: { + project: string; + createdBy: string | IUser; + data: any; + createdByUserId: number; + }) { + super(PROJECT_USER_ADDED, p.createdBy, p.createdByUserId); const { project, data } = p; this.project = project; this.data = data; @@ -754,8 +795,9 @@ export class ProjectUserRemovedEvent extends BaseEvent { project: string; createdBy: string | IUser; preData: any; + createdByUserId: number; }) { - super(PROJECT_USER_REMOVED, p.createdBy); + super(PROJECT_USER_REMOVED, p.createdBy, p.createdByUserId); const { project, preData } = p; this.project = project; this.data = null; @@ -778,8 +820,13 @@ export class ProjectUserUpdateRoleEvent extends BaseEvent { createdBy: string | IUser; data: any; preData: any; + createdByUserId: number; }) { - super(PROJECT_USER_ROLE_CHANGED, eventData.createdBy); + super( + PROJECT_USER_ROLE_CHANGED, + eventData.createdBy, + eventData.createdByUserId, + ); const { project, data, preData } = eventData; this.project = project; this.data = data; @@ -797,8 +844,13 @@ export class ProjectGroupAddedEvent extends BaseEvent { /** * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization */ - constructor(p: { project: string; createdBy: string | IUser; data: any }) { - super(PROJECT_GROUP_ADDED, p.createdBy); + constructor(p: { + project: string; + createdBy: string | IUser; + data: any; + createdByUserId: number; + }) { + super(PROJECT_GROUP_ADDED, p.createdBy, p.createdByUserId); const { project, data } = p; this.project = project; this.data = data; @@ -820,8 +872,9 @@ export class ProjectGroupRemovedEvent extends BaseEvent { project: string; createdBy: string | IUser; preData: any; + createdByUserId: number; }) { - super(PROJECT_GROUP_REMOVED, p.createdBy); + super(PROJECT_GROUP_REMOVED, p.createdBy, p.createdByUserId); const { project, preData } = p; this.project = project; this.data = null; @@ -844,8 +897,13 @@ export class ProjectGroupUpdateRoleEvent extends BaseEvent { createdBy: string | IUser; data: any; preData: any; + createdByUserId: number; }) { - super(PROJECT_GROUP_ROLE_CHANGED, eventData.createdBy); + super( + PROJECT_GROUP_ROLE_CHANGED, + eventData.createdBy, + eventData.createdByUserId, + ); const { project, data, preData } = eventData; this.project = project; this.data = data; @@ -863,8 +921,13 @@ export class ProjectAccessAddedEvent extends BaseEvent { /** * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization */ - constructor(p: { project: string; createdBy: string | IUser; data: any }) { - super(PROJECT_ACCESS_ADDED, p.createdBy); + constructor(p: { + project: string; + createdBy: string | IUser; + data: any; + createdByUserId: number; + }) { + super(PROJECT_ACCESS_ADDED, p.createdBy, p.createdByUserId); const { project, data } = p; this.project = project; this.data = data; @@ -887,8 +950,13 @@ export class ProjectAccessUserRolesUpdated extends BaseEvent { createdBy: string | IUser; data: any; preData: any; + createdByUserId: number; }) { - super(PROJECT_ACCESS_USER_ROLES_UPDATED, p.createdBy); + super( + PROJECT_ACCESS_USER_ROLES_UPDATED, + p.createdBy, + p.createdByUserId, + ); const { project, data, preData } = p; this.project = project; this.data = data; @@ -911,8 +979,13 @@ export class ProjectAccessGroupRolesUpdated extends BaseEvent { createdBy: string | IUser; data: any; preData: any; + createdByUserId: number; }) { - super(PROJECT_ACCESS_GROUP_ROLES_UPDATED, p.createdBy); + super( + PROJECT_ACCESS_GROUP_ROLES_UPDATED, + p.createdBy, + p.createdByUserId, + ); const { project, data, preData } = p; this.project = project; this.data = data; @@ -934,8 +1007,13 @@ export class ProjectAccessUserRolesDeleted extends BaseEvent { project: string; createdBy: string | IUser; preData: any; + createdByUserId: number; }) { - super(PROJECT_ACCESS_USER_ROLES_DELETED, p.createdBy); + super( + PROJECT_ACCESS_USER_ROLES_DELETED, + p.createdBy, + p.createdByUserId, + ); const { project, preData } = p; this.project = project; this.data = null; @@ -957,8 +1035,13 @@ export class ProjectAccessGroupRolesDeleted extends BaseEvent { project: string; createdBy: string | IUser; preData: any; + createdByUserId: number; }) { - super(PROJECT_ACCESS_GROUP_ROLES_DELETED, p.createdBy); + super( + PROJECT_ACCESS_GROUP_ROLES_DELETED, + p.createdBy, + p.createdByUserId, + ); const { project, preData } = p; this.project = project; this.data = null; @@ -972,8 +1055,12 @@ export class SettingCreatedEvent extends BaseEvent { /** * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization */ - constructor(eventData: { createdBy: string | IUser; data: any }) { - super(SETTING_CREATED, eventData.createdBy); + constructor(eventData: { + createdBy: string | IUser; + data: any; + createdByUserId: number; + }) { + super(SETTING_CREATED, eventData.createdBy, eventData.createdByUserId); this.data = eventData.data; } } @@ -984,8 +1071,12 @@ export class SettingDeletedEvent extends BaseEvent { /** * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization */ - constructor(eventData: { createdBy: string | IUser; data: any }) { - super(SETTING_DELETED, eventData.createdBy); + constructor(eventData: { + createdBy: string | IUser; + data: any; + createdByUserId: number; + }) { + super(SETTING_DELETED, eventData.createdBy, eventData.createdByUserId); this.data = eventData.data; } } @@ -998,10 +1089,14 @@ export class SettingUpdatedEvent extends BaseEvent { * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization */ constructor( - eventData: { createdBy: string | IUser; data: any }, + eventData: { + createdBy: string | IUser; + data: any; + createdByUserId: number; + }, preData: any, ) { - super(SETTING_UPDATED, eventData.createdBy); + super(SETTING_UPDATED, eventData.createdBy, eventData.createdByUserId); this.data = eventData.data; this.preData = preData; } @@ -1013,8 +1108,16 @@ export class PublicSignupTokenCreatedEvent extends BaseEvent { /** * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization */ - constructor(eventData: { createdBy: string | IUser; data: any }) { - super(PUBLIC_SIGNUP_TOKEN_CREATED, eventData.createdBy); + constructor(eventData: { + createdBy: string | IUser; + data: any; + createdByUserId: number; + }) { + super( + PUBLIC_SIGNUP_TOKEN_CREATED, + eventData.createdBy, + eventData.createdByUserId, + ); this.data = eventData.data; } } @@ -1025,8 +1128,16 @@ export class PublicSignupTokenUpdatedEvent extends BaseEvent { /** * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization */ - constructor(eventData: { createdBy: string | IUser; data: any }) { - super(PUBLIC_SIGNUP_TOKEN_TOKEN_UPDATED, eventData.createdBy); + constructor(eventData: { + createdBy: string | IUser; + data: any; + createdByUserId: number; + }) { + super( + PUBLIC_SIGNUP_TOKEN_TOKEN_UPDATED, + eventData.createdBy, + eventData.createdByUserId, + ); this.data = eventData.data; } } @@ -1037,8 +1148,16 @@ export class PublicSignupTokenUserAddedEvent extends BaseEvent { /** * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization */ - constructor(eventData: { createdBy: string | IUser; data: any }) { - super(PUBLIC_SIGNUP_TOKEN_USER_ADDED, eventData.createdBy); + constructor(eventData: { + createdBy: string | IUser; + data: any; + createdByUserId: number; + }) { + super( + PUBLIC_SIGNUP_TOKEN_USER_ADDED, + eventData.createdBy, + eventData.createdByUserId, + ); this.data = eventData.data; } } @@ -1056,8 +1175,13 @@ export class ApiTokenCreatedEvent extends BaseEvent { constructor(eventData: { createdBy: string | IUser; apiToken: Omit; + createdByUserId: number; }) { - super(API_TOKEN_CREATED, eventData.createdBy); + super( + API_TOKEN_CREATED, + eventData.createdBy, + eventData.createdByUserId, + ); this.data = eventData.apiToken; this.environment = eventData.apiToken.environment; this.project = eventData.apiToken.project; @@ -1077,8 +1201,13 @@ export class ApiTokenDeletedEvent extends BaseEvent { constructor(eventData: { createdBy: string | IUser; apiToken: Omit; + createdByUserId: number; }) { - super(API_TOKEN_DELETED, eventData.createdBy); + super( + API_TOKEN_DELETED, + eventData.createdBy, + eventData.createdByUserId, + ); this.preData = eventData.apiToken; this.environment = eventData.apiToken.environment; this.project = eventData.apiToken.project; @@ -1101,8 +1230,13 @@ export class ApiTokenUpdatedEvent extends BaseEvent { createdBy: string | IUser; previousToken: Omit; apiToken: Omit; + createdByUserId: number; }) { - super(API_TOKEN_UPDATED, eventData.createdBy); + super( + API_TOKEN_UPDATED, + eventData.createdBy, + eventData.createdByUserId, + ); this.preData = eventData.previousToken; this.data = eventData.apiToken; this.environment = eventData.apiToken.environment; @@ -1115,8 +1249,16 @@ export class PotentiallyStaleOnEvent extends BaseEvent { readonly project: string; - constructor(eventData: { featureName: string; project: string }) { - super(FEATURE_POTENTIALLY_STALE_ON, 'unleash-system'); + constructor(eventData: { + featureName: string; + project: string; + createdByUserId: number; + }) { + super( + FEATURE_POTENTIALLY_STALE_ON, + 'unleash-system', + eventData.createdByUserId, + ); this.featureName = eventData.featureName; this.project = eventData.project; } @@ -1128,8 +1270,9 @@ export class UserCreatedEvent extends BaseEvent { constructor(eventData: { createdBy: string | IUser; userCreated: IUserWithRootRole; + createdByUserId: number; }) { - super(USER_CREATED, eventData.createdBy); + super(USER_CREATED, eventData.createdBy, eventData.createdByUserId); this.data = mapUserToData(eventData.userCreated); } } @@ -1142,8 +1285,9 @@ export class UserUpdatedEvent extends BaseEvent { createdBy: string | IUser; preUser: IUserWithRootRole; postUser: IUserWithRootRole; + createdByUserId: number; }) { - super(USER_UPDATED, eventData.createdBy); + super(USER_UPDATED, eventData.createdBy, eventData.createdByUserId); this.preData = mapUserToData(eventData.preUser); this.data = mapUserToData(eventData.postUser); } @@ -1155,8 +1299,9 @@ export class UserDeletedEvent extends BaseEvent { constructor(eventData: { createdBy: string | IUser; deletedUser: IUserWithRootRole; + createdByUserId: number; }) { - super(USER_DELETED, eventData.createdBy); + super(USER_DELETED, eventData.createdBy, eventData.createdByUserId); this.preData = mapUserToData(eventData.deletedUser); } } diff --git a/src/lib/types/model.ts b/src/lib/types/model.ts index 670863b0a7..5e0c46da42 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 16783ad050..45433858e0 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 0c37d9b32b..167e8a3d86 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 d2a3a51caa..8ef084d5c0 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 2c5119584b..fb07bf9931 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 3f353ca6b0..fbf9b40616 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 7f5aeef867..37ee1942ec 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 3f61644528..c1cd192221 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 cf4020e118..219ecd4032 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 801b248436..4557562ae1 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 327707e2ca..dc8a9b4b9f 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 531ce81115..5c4f44bc8a 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 279cd5f817..709ab14d6d 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 085c90b8df..07347a6e56 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 32e7620f73..9a0744f5f7 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 becbda94de..f965568675 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 1f52daa981..bff66f5df0 100644 --- a/src/test/e2e/services/project-service.e2e.test.ts +++ b/src/test/e2e/services/project-service.e2e.test.ts @@ -17,7 +17,12 @@ 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 +35,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 +493,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 +518,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 +550,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 +594,7 @@ test('should fail if user is not authorized', async () => { project.id, toggle, user.email, + TEST_USER_ID, ); try { @@ -621,6 +631,7 @@ test('should change project when checks pass', async () => { projectA.id, toggle, user.email, + TEST_USER_ID, ); await projectService.changeProject( projectB.id, @@ -655,6 +666,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 +701,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 +825,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 +876,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 +917,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 +933,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 +981,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 +1026,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 +1057,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 +1100,7 @@ describe('ensure project has at least one owner', () => { [], [memberUser.id], 'test', + TEST_USER_ID, ); const usersBefore = await projectService.getProjectUsers(project.id); @@ -1080,6 +1108,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 +1147,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 +1159,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 +1184,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 +1194,7 @@ describe('ensure project has at least one owner', () => { ownerRole.id, user.id, 'test', + TEST_USER_ID, ); return { @@ -1182,6 +1215,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 +1226,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 +1247,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 +1259,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 +1295,7 @@ test('Should allow bulk update of group permissions', async () => { id: 2, // CREATE_FEATURE }, ], + createdByUserId: SYSTEM_USER_ID, }); await projectService.addAccess( @@ -1266,6 +1304,7 @@ test('Should allow bulk update of group permissions', async () => { [group1.id], [user1.id], 'some-admin-user', + TEST_USER_ID, ); }); @@ -1285,6 +1324,7 @@ test('Should bulk update of only users', async () => { id: 2, // CREATE_FEATURE }, ], + createdByUserId: SYSTEM_USER_ID, }); await projectService.addAccess( @@ -1293,6 +1333,7 @@ test('Should bulk update of only users', async () => { [], [user1.id], 'some-admin-user', + TEST_USER_ID, ); }); @@ -1320,6 +1361,7 @@ test('Should allow bulk update of only groups', async () => { id: 2, // CREATE_FEATURE }, ], + createdByUserId: SYSTEM_USER_ID, }); await projectService.addAccess( @@ -1328,6 +1370,7 @@ test('Should allow bulk update of only groups', async () => { [group1.id], [], 'some-admin-user', + TEST_USER_ID, ); }); @@ -1369,6 +1412,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 +1423,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 +1432,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 +1533,7 @@ test('should calculate average time to production', async () => { project.id, toggle, user.email, + TEST_USER_ID, ); }), ); @@ -1500,6 +1547,7 @@ test('should calculate average time to production', async () => { featureName: toggle.name, environment: 'default', createdBy: 'Fredrik', + createdByUserId: TEST_USER_ID, }), ); }), @@ -1534,6 +1582,7 @@ test('should calculate average time to production ignoring some items', async () featureName, environment: 'default', createdBy: 'Fredrik', + createdByUserId: TEST_USER_ID, tags: [], }); @@ -1542,7 +1591,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 +1604,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 +1624,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 +1639,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 +1651,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 +1663,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 +1700,7 @@ test('should get correct amount of features created in current and past window', project.id, toggle, user.email, + TEST_USER_ID, ); }), ); @@ -1678,6 +1738,7 @@ test('should get correct amount of features archived in current and past window' project.id, toggle, user.email, + TEST_USER_ID, ); }), ); @@ -1770,6 +1831,7 @@ test('should return average time to production per toggle', async () => { project.id, toggle, user.email, + TEST_USER_ID, ); }), ); @@ -1783,6 +1845,7 @@ test('should return average time to production per toggle', async () => { featureName: toggle.name, environment: 'default', createdBy: 'Fredrik', + createdByUserId: TEST_USER_ID, }), ); }), @@ -1838,6 +1901,7 @@ test('should return average time to production per toggle for a specific project project1.id, toggle, user.email, + TEST_USER_ID, ); }), ); @@ -1848,6 +1912,7 @@ test('should return average time to production per toggle for a specific project project2.id, toggle, user.email, + TEST_USER_ID, ); }), ); @@ -1861,6 +1926,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 +1941,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 +1992,7 @@ test('should return average time to production per toggle and include archived t project1.id, toggle, user.email, + TEST_USER_ID, ); }), ); @@ -1938,6 +2006,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 e39e3b9ac6..409cbfc49c 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 8ef3a4d643..4fceb0c595 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 b6e702be4b..c030ddd0a2 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 916fdf6110..cc64b16242 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);