From cf2bd28ff635057c464381cde2aa694e2abe16b8 Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Thu, 18 Apr 2024 16:32:35 +0200 Subject: [PATCH] feat: use audit info in events (#6872) I've tried to use/add the audit info to all events I could see/find. This makes this PR necessarily huge, because we do store quite a few events. I realise it might not be complete yet, but tests run green, and I think we now have a pattern to follow for other events. --- .../tests/client-feature-toggles.e2e.test.ts | 17 +- .../dependent-features-controller.ts | 9 +- .../dependent-features-service.ts | 92 +- src/lib/features/events/event-store.ts | 9 +- .../export-import-controller.ts | 10 +- .../export-import-service.ts | 136 +-- .../export-import.e2e.test.ts | 16 +- .../archive-feature-toggle-controller.ts | 6 +- .../feature-toggle-controller.ts | 52 +- .../feature-toggle/feature-toggle-service.ts | 255 +++-- .../feature-toggle-legacy-controller.ts | 92 +- .../tests/feature-toggle-service.e2e.test.ts | 93 +- .../tests/feature-toggles.auth.e2e.test.ts | 42 +- .../tests/feature-toggles.e2e.test.ts | 21 +- .../frontend-api-service.e2e.test.ts | 12 +- .../frontend-api/frontend-api-service.ts | 7 +- .../frontend-api/frontend-api.e2e.test.ts | 21 +- .../maintenance/maintenance-controller.ts | 4 +- .../maintenance/maintenance-service.test.ts | 4 +- .../maintenance/maintenance-service.ts | 8 +- .../tests/last-seen-service.e2e.test.ts | 7 +- .../environment-service.test.ts | 36 +- .../environment-service.ts | 67 +- .../project-environments/environments.ts | 15 +- .../project-insights-service.e2e.test.ts | 61 +- .../project/project-service.e2e.test.ts | 660 +++++++------ .../features/project/project-service.test.ts | 16 +- src/lib/features/project/project-service.ts | 172 ++-- .../scheduler/scheduler-service.test.ts | 4 +- .../segment/client-segment.e2e.test.ts | 27 +- .../features/segment/segment-controller.ts | 17 +- .../segment/segment-service-interface.ts | 19 +- src/lib/features/segment/segment-service.ts | 88 +- .../features/segment/segment-store-type.ts | 4 +- .../features/segment/segment-store.test.ts | 8 +- src/lib/features/segment/segment-store.ts | 6 +- src/lib/features/tag-type/tag-type-service.ts | 56 +- src/lib/features/tag-type/tag-type.ts | 14 +- src/lib/middleware/audit-middleware.ts | 6 +- .../middleware/cors-origin-middleware.test.ts | 20 +- src/lib/routes/admin-api/addon.ts | 19 +- src/lib/routes/admin-api/api-token.ts | 13 +- src/lib/routes/admin-api/config.ts | 4 +- src/lib/routes/admin-api/context.ts | 21 +- src/lib/routes/admin-api/events.test.ts | 21 +- src/lib/routes/admin-api/favorites.ts | 44 +- src/lib/routes/admin-api/feature-type.ts | 2 +- src/lib/routes/admin-api/project/api-token.ts | 10 +- .../admin-api/project/project-archive.ts | 10 +- src/lib/routes/admin-api/project/variants.ts | 7 +- src/lib/routes/admin-api/public-signup.ts | 8 +- src/lib/routes/admin-api/strategy.ts | 28 +- src/lib/routes/admin-api/tag.ts | 8 +- src/lib/routes/admin-api/user-admin.ts | 6 +- src/lib/routes/admin-api/user/pat.ts | 10 +- src/lib/routes/public-invite.ts | 1 + src/lib/routes/unleash-types.ts | 4 +- src/lib/services/access-service.test.ts | 36 +- src/lib/services/access-service.ts | 91 +- src/lib/services/addon-service.test.ts | 87 +- src/lib/services/addon-service.ts | 72 +- src/lib/services/api-token-service.test.ts | 12 +- src/lib/services/api-token-service.ts | 85 +- src/lib/services/context-service.ts | 45 +- src/lib/services/favorites-service.ts | 108 +-- src/lib/services/feature-tag-service.ts | 69 +- src/lib/services/feature-type-service.ts | 19 +- src/lib/services/group-service.ts | 57 +- src/lib/services/pat-service.ts | 39 +- .../services/public-signup-token-service.ts | 32 +- src/lib/services/setting-service.ts | 19 +- src/lib/services/strategy-service.ts | 95 +- src/lib/services/tag-service.ts | 35 +- src/lib/services/user-service.test.ts | 3 +- src/lib/services/user-service.ts | 42 +- src/lib/types/core.ts | 14 +- src/lib/types/events.ts | 887 ++++++++++++------ src/lib/types/user.ts | 2 +- .../inactive/inactive-users-controller.ts | 2 +- .../users/inactive/inactive-users-service.ts | 4 +- src/lib/util/extract-user.ts | 10 +- ...418140646-add-ip-column-to-events-table.js | 7 + .../e2e/api/admin/api-token.auth.e2e.test.ts | 292 ++++-- src/test/e2e/api/admin/config.e2e.test.ts | 5 +- src/test/e2e/api/admin/event.e2e.test.ts | 3 +- .../admin/project/project.health.e2e.test.ts | 25 +- src/test/e2e/api/admin/state.e2e.test.ts | 47 +- .../reset-password-controller.e2e.test.ts | 26 +- .../auth/simple-password-provider.e2e.test.ts | 21 +- .../api/client/feature.auth-none.e2e.test.ts | 10 +- src/test/e2e/api/client/feature.e2e.test.ts | 35 +- .../client/feature.env.disabled.e2e.test.ts | 9 +- .../api/client/feature.optimal304.e2e.test.ts | 31 +- .../client/feature.token.access.e2e.test.ts | 25 +- src/test/e2e/api/client/metricsV2.e2e.test.ts | 8 +- src/test/e2e/custom-auth.test.ts | 15 +- src/test/e2e/seed/segment.seed.ts | 5 +- .../e2e/services/access-service.e2e.test.ts | 107 ++- .../e2e/services/addon-service.e2e.test.ts | 8 +- .../services/api-token-service.e2e.test.ts | 8 +- .../e2e/services/group-service.e2e.test.ts | 16 +- .../project-health-service.e2e.test.ts | 20 +- .../services/reset-token-service.e2e.test.ts | 26 +- src/test/e2e/services/setting-service.test.ts | 35 +- .../e2e/services/user-service.e2e.test.ts | 247 +++-- src/test/e2e/stores/event-store.e2e.test.ts | 18 +- .../inactive/inactive-users-service.test.ts | 11 +- 107 files changed, 2877 insertions(+), 2473 deletions(-) create mode 100644 src/migrations/20240418140646-add-ip-column-to-events-table.js diff --git a/src/lib/features/client-feature-toggles/tests/client-feature-toggles.e2e.test.ts b/src/lib/features/client-feature-toggles/tests/client-feature-toggles.e2e.test.ts index 1cb7de9a1b8..4431ed95bc3 100644 --- a/src/lib/features/client-feature-toggles/tests/client-feature-toggles.e2e.test.ts +++ b/src/lib/features/client-feature-toggles/tests/client-feature-toggles.e2e.test.ts @@ -8,7 +8,7 @@ import { } from '../../../../test/e2e/helpers/test-helper'; import getLogger from '../../../../test/fixtures/no-logger'; import { DEFAULT_ENV } from '../../../util/constants'; -import type { IUserWithRootRole } from '../../../types'; +import { type IUserWithRootRole, TEST_AUDIT_USER } from '../../../types'; let app: IUnleashTest; let db: ITestDb; @@ -73,11 +73,14 @@ beforeAll(async () => { db.rawDatabase, ); - dummyAdmin = await app.services.userService.createUser({ - name: 'Some Name', - email: 'test@getunleash.io', - rootRole: RoleName.ADMIN, - }); + dummyAdmin = await app.services.userService.createUser( + { + name: 'Some Name', + email: 'test@getunleash.io', + rootRole: RoleName.ADMIN, + }, + TEST_AUDIT_USER, + ); }); afterEach(async () => { @@ -137,10 +140,12 @@ test('should support filtering on project', async () => { await app.services.projectService.createProject( { name: 'projectA', id: 'projecta' }, dummyAdmin, + TEST_AUDIT_USER, ); await app.services.projectService.createProject( { name: 'projectB', id: 'projectb' }, dummyAdmin, + TEST_AUDIT_USER, ); await app.createFeature('ab_test1', 'projecta'); await app.createFeature('bd_test2', 'projectb'); diff --git a/src/lib/features/dependent-features/dependent-features-controller.ts b/src/lib/features/dependent-features/dependent-features-controller.ts index eacb5bd123b..ebe79caed7f 100644 --- a/src/lib/features/dependent-features/dependent-features-controller.ts +++ b/src/lib/features/dependent-features/dependent-features-controller.ts @@ -218,6 +218,7 @@ export default class DependentFeaturesController extends Controller { feature, }, req.user, + req.audit, ), ); @@ -238,6 +239,7 @@ export default class DependentFeaturesController extends Controller { }, projectId, req.user, + req.audit, ), ); res.status(200).end(); @@ -250,7 +252,12 @@ export default class DependentFeaturesController extends Controller { const { child, projectId } = req.params; await this.dependentFeaturesService.transactional((service) => - service.deleteFeaturesDependencies([child], projectId, req.user), + service.deleteFeaturesDependencies( + [child], + projectId, + req.user, + req.audit, + ), ); res.status(200).end(); } diff --git a/src/lib/features/dependent-features/dependent-features-service.ts b/src/lib/features/dependent-features/dependent-features-service.ts index 88e46e9d21b..ce6bf2929c9 100644 --- a/src/lib/features/dependent-features/dependent-features-service.ts +++ b/src/lib/features/dependent-features/dependent-features-service.ts @@ -7,10 +7,14 @@ import type { } from './dependent-features'; import type { IDependentFeaturesReadModel } from './dependent-features-read-model-type'; import type { EventService } from '../../services'; -import type { IUser } from '../../server-impl'; -import { SKIP_CHANGE_REQUEST } from '../../types'; +import type { IAuditUser, IUser } from '../../server-impl'; +import { + FeatureDependenciesRemovedEvent, + FeatureDependencyAddedEvent, + FeatureDependencyRemovedEvent, + SKIP_CHANGE_REQUEST, +} from '../../types'; import type { IChangeRequestAccessReadModel } from '../change-request-access-service/change-request-access-read-model'; -import { extractUsernameFromUser } from '../../util'; import type { IFeaturesReadModel } from '../feature-toggle/types/features-read-model-type'; interface IDependentFeaturesServiceDeps { @@ -52,8 +56,7 @@ export class DependentFeaturesService { newFeatureName, projectId, }: { featureName: string; newFeatureName: string; projectId: string }, - user: string, - userId: number, + auditUser: IAuditUser, ) { const parents = await this.dependentFeaturesReadModel.getParents(featureName); @@ -66,8 +69,7 @@ export class DependentFeaturesService { enabled: parent.enabled, variants: parent.variants, }, - user, - userId, + auditUser, ), ), ); @@ -77,22 +79,21 @@ export class DependentFeaturesService { { child, projectId }: { child: string; projectId: string }, dependentFeature: CreateDependentFeatureSchema, user: IUser, + auditUser: IAuditUser, ): Promise { await this.stopWhenChangeRequestsEnabled(projectId, user); return this.unprotectedUpsertFeatureDependency( { child, projectId }, dependentFeature, - extractUsernameFromUser(user), - user.id, + auditUser, ); } async unprotectedUpsertFeatureDependency( { child, projectId }: { child: string; projectId: string }, dependentFeature: CreateDependentFeatureSchema, - user: string, - userId: number, + auditUser: IAuditUser, ): Promise { const { enabled, feature: parent, variants } = dependentFeature; @@ -148,82 +149,81 @@ export class DependentFeaturesService { variants, }; await this.dependentFeaturesStore.upsert(featureDependency); - await this.eventService.storeEvent({ - type: 'feature-dependency-added', - project: projectId, - featureName: child, - createdBy: user, - createdByUserId: userId, - data: { - feature: parent, - enabled: featureDependency.enabled, - ...(variants !== undefined && { variants }), - }, - }); + await this.eventService.storeEvent( + new FeatureDependencyAddedEvent({ + project: projectId, + featureName: child, + auditUser, + data: { + feature: parent, + enabled: featureDependency.enabled, + ...(variants !== undefined && { variants }), + }, + }), + ); } async deleteFeatureDependency( dependency: FeatureDependencyId, projectId: string, user: IUser, + auditUser: IAuditUser, ): Promise { await this.stopWhenChangeRequestsEnabled(projectId, user); return this.unprotectedDeleteFeatureDependency( dependency, projectId, - extractUsernameFromUser(user), - user.id, + auditUser, ); } async unprotectedDeleteFeatureDependency( dependency: FeatureDependencyId, projectId: string, - user: string, - userId: number, + auditUser: IAuditUser, ): Promise { await this.dependentFeaturesStore.delete(dependency); - await this.eventService.storeEvent({ - type: 'feature-dependency-removed', - project: projectId, - featureName: dependency.child, - createdBy: user, - createdByUserId: userId, - data: { feature: dependency.parent }, - }); + await this.eventService.storeEvent( + new FeatureDependencyRemovedEvent({ + project: projectId, + featureName: dependency.child, + auditUser, + data: { feature: dependency.parent }, + }), + ); } async deleteFeaturesDependencies( features: string[], projectId: string, user: IUser, + auditUser: IAuditUser, ): Promise { await this.stopWhenChangeRequestsEnabled(projectId, user); return this.unprotectedDeleteFeaturesDependencies( features, projectId, - extractUsernameFromUser(user), - user.id, + auditUser, ); } async unprotectedDeleteFeaturesDependencies( features: string[], projectId: string, - user: string, - userId: number, + auditUser: IAuditUser, ): Promise { await this.dependentFeaturesStore.deleteAll(features); await this.eventService.storeEvents( - features.map((feature) => ({ - type: 'feature-dependencies-removed', - project: projectId, - featureName: feature, - createdBy: user, - createdByUserId: userId, - })), + features.map( + (feature) => + new FeatureDependenciesRemovedEvent({ + project: projectId, + featureName: feature, + auditUser, + }), + ), ); } diff --git a/src/lib/features/events/event-store.ts b/src/lib/features/events/event-store.ts index b3d70986261..e71677759a5 100644 --- a/src/lib/features/events/event-store.ts +++ b/src/lib/features/events/event-store.ts @@ -1,12 +1,12 @@ import { - type IEvent, - type IBaseEvent, - SEGMENT_UPDATED, FEATURE_IMPORT, FEATURES_IMPORTED, + type IBaseEvent, + type IEvent, type IEventType, + SEGMENT_UPDATED, } from '../../types/events'; -import type { LogProvider, Logger } from '../../logger'; +import type { Logger, LogProvider } from '../../logger'; import type { IEventStore } from '../../types/stores/event-store'; import type { ITag } from '../../types/model'; import type { SearchEventsSchema } from '../../openapi/spec/search-events-schema'; @@ -394,6 +394,7 @@ class EventStore implements IEventStore { feature_name: e.featureName, project: e.project, environment: e.environment, + ip: e.ip, }; } 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 53f787eecc5..5b2ea651026 100644 --- a/src/lib/features/export-import-toggles/export-import-controller.ts +++ b/src/lib/features/export-import-toggles/export-import-controller.ts @@ -120,11 +120,7 @@ class ExportImportController extends Controller { const query = req.body; const userName = extractUsername(req); - const data = await this.exportService.export( - query, - userName, - req.user.id, - ); + const data = await this.exportService.export(query, req.audit); this.openApiService.respondWithValidation( 200, @@ -159,7 +155,7 @@ class ExportImportController extends Controller { res: Response, ): Promise { this.verifyExportImportEnabled(); - const { user } = req; + const { user, audit } = req; if (user instanceof ApiUser && user.type === 'admin') { throw new BadDataError( @@ -170,7 +166,7 @@ class ExportImportController extends Controller { const dto = req.body; await this.importService.transactional((service) => - service.import(dto, user), + service.import(dto, user, audit), ); res.status(200).end(); 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 15f12d911ca..1bc4b3c19f8 100644 --- a/src/lib/features/export-import-toggles/export-import-service.ts +++ b/src/lib/features/export-import-toggles/export-import-service.ts @@ -3,9 +3,10 @@ import type { IStrategy } from '../../types/stores/strategy-store'; import type { IFeatureToggleStore } from '../feature-toggle/types/feature-toggle-store-type'; import type { IFeatureStrategiesStore } from '../feature-toggle/types/feature-toggle-strategies-store-type'; import { - FEATURES_EXPORTED, - FEATURES_IMPORTED, + FeaturesExportedEvent, + FeaturesImportedEvent, type FeatureToggleDTO, + type IAuditUser, type IContextFieldStore, type IFeatureEnvironmentStore, type IFeatureStrategy, @@ -28,7 +29,6 @@ import type { } from '../../openapi'; import type { IUser } from '../../types/user'; import { BadDataError } from '../../error'; -import { extractUsernameFromUser } from '../../util'; import type { AccessService, ContextService, @@ -62,14 +62,17 @@ export type IImportService = { user: IUser, ): Promise; - import(dto: ImportTogglesSchema, user: IUser): Promise; + import( + dto: ImportTogglesSchema, + user: IUser, + auditUser: IAuditUser, + ): Promise; }; export type IExportService = { export( query: ExportQuerySchema, - userName: string, - userId: number, + auditUser: IAuditUser, ): Promise; }; @@ -263,43 +266,52 @@ export default class ExportImportService async importFeatureData( dto: ImportTogglesSchema, - user: IUser, + auditUser: IAuditUser, ): Promise { - await this.createOrUpdateToggles(dto, user); - await this.importToggleVariants(dto, user); - await this.importTagTypes(dto, user); - await this.importTags(dto, user); - await this.importContextFields(dto, user); + await this.createOrUpdateToggles(dto, auditUser); + await this.importToggleVariants(dto, auditUser); + await this.importTagTypes(dto, auditUser); + await this.importTags(dto, auditUser); + await this.importContextFields(dto, auditUser); } - async import(dto: ImportTogglesSchema, user: IUser): Promise { + async import( + dto: ImportTogglesSchema, + user: IUser, + auditUser: IAuditUser, + ): Promise { const cleanedDto = await this.cleanData(dto); await this.importVerify(cleanedDto, user); - await this.importFeatureData(cleanedDto, user); + await this.importFeatureData(cleanedDto, auditUser); - await this.importEnvironmentData(cleanedDto, user); - await this.eventService.storeEvent({ - project: cleanedDto.project, - environment: cleanedDto.environment, - type: FEATURES_IMPORTED, - createdBy: extractUsernameFromUser(user), - createdByUserId: user.id, - }); + await this.importEnvironmentData(cleanedDto, user, auditUser); + await this.eventService.storeEvent( + new FeaturesImportedEvent({ + project: cleanedDto.project, + environment: cleanedDto.environment, + auditUser, + }), + ); } async importEnvironmentData( dto: ImportTogglesSchema, user: IUser, + auditUser: IAuditUser, ): Promise { await this.deleteStrategies(dto); - await this.importStrategies(dto, user); - await this.importToggleStatuses(dto, user); - await this.importDependencies(dto, user); + await this.importStrategies(dto, auditUser); + await this.importToggleStatuses(dto, user, auditUser); + await this.importDependencies(dto, user, auditUser); } - private async importDependencies(dto: ImportTogglesSchema, user: IUser) { + private async importDependencies( + dto: ImportTogglesSchema, + user: IUser, + auditUser: IAuditUser, + ) { await Promise.all( (dto.data.dependencies || []).flatMap((dependency) => { const feature = dto.data.features.find( @@ -318,13 +330,18 @@ export default class ExportImportService }, parentDependency, user, + auditUser, ), ); }), ); } - private async importToggleStatuses(dto: ImportTogglesSchema, user: IUser) { + private async importToggleStatuses( + dto: ImportTogglesSchema, + user: IUser, + auditUser: IAuditUser, + ) { await Promise.all( (dto.data.featureEnvironments || []).map((featureEnvironment) => this.featureToggleService.updateEnabled( @@ -332,14 +349,17 @@ export default class ExportImportService featureEnvironment.name, dto.environment, featureEnvironment.enabled, - extractUsernameFromUser(user), + auditUser, user, ), ), ); } - private async importStrategies(dto: ImportTogglesSchema, user: IUser) { + private async importStrategies( + dto: ImportTogglesSchema, + auditUser: IAuditUser, + ) { const hasFeatureName = ( featureStrategy: FeatureStrategySchema, ): featureStrategy is WithRequired< @@ -357,7 +377,7 @@ export default class ExportImportService environment: dto.environment, projectId: dto.project, }, - extractUsernameFromUser(user), + auditUser, ), ), ); @@ -370,7 +390,7 @@ export default class ExportImportService ); } - private async importTags(dto: ImportTogglesSchema, user: IUser) { + private async importTags(dto: ImportTogglesSchema, auditUser: IAuditUser) { await this.importTogglesStore.deleteTagsForFeatures( dto.data.features.map((feature) => feature.name), ); @@ -384,14 +404,16 @@ export default class ExportImportService type: tag.tagType, value: tag.tagValue, }, - extractUsernameFromUser(user), - user.id, + auditUser, ); } } } - private async importContextFields(dto: ImportTogglesSchema, user: IUser) { + private async importContextFields( + dto: ImportTogglesSchema, + auditUser: IAuditUser, + ) { const newContextFields = (await this.getNewContextFields(dto)) || []; await Promise.all( newContextFields.map((contextField) => @@ -402,29 +424,30 @@ export default class ExportImportService legalValues: contextField.legalValues, stickiness: contextField.stickiness, }, - extractUsernameFromUser(user), - user.id, + auditUser, ), ), ); } - private async importTagTypes(dto: ImportTogglesSchema, user: IUser) { + private async importTagTypes( + dto: ImportTogglesSchema, + auditUser: IAuditUser, + ) { const newTagTypes = await this.getNewTagTypes(dto); return Promise.all( newTagTypes.map((tagType) => { return tagType - ? this.tagTypeService.createTagType( - tagType, - extractUsernameFromUser(user), - user.id, - ) + ? this.tagTypeService.createTagType(tagType, auditUser) : Promise.resolve(); }), ); } - private async importToggleVariants(dto: ImportTogglesSchema, user: IUser) { + private async importToggleVariants( + dto: ImportTogglesSchema, + auditUser: IAuditUser, + ) { const featureEnvsWithVariants = dto.data.featureEnvironments?.filter( (featureEnvironment) => @@ -439,16 +462,18 @@ export default class ExportImportService featureEnvironment.featureName, dto.environment, featureEnvironment.variants as IVariant[], - user, + auditUser, ) : Promise.resolve(); }), ); } - private async createOrUpdateToggles(dto: ImportTogglesSchema, user: IUser) { + private async createOrUpdateToggles( + dto: ImportTogglesSchema, + auditUser: IAuditUser, + ) { const existingFeatures = await this.getExistingProjectFeatures(dto); - const username = extractUsernameFromUser(user); for (const feature of dto.data.features) { if (existingFeatures.includes(feature.name)) { @@ -456,9 +481,8 @@ export default class ExportImportService await this.featureToggleService.updateFeatureToggle( dto.project, rest as FeatureToggleDTO, - username, feature.name, - user.id, + auditUser, ); } else { await this.featureToggleService.validateName(feature.name); @@ -466,8 +490,7 @@ export default class ExportImportService await this.featureToggleService.createFeatureToggle( dto.project, rest as FeatureToggleDTO, - username, - user.id, + auditUser, ); } } @@ -779,8 +802,7 @@ export default class ExportImportService async export( query: ExportQuerySchema, - userName: string, - userId: number, + auditUser: IAuditUser, ): Promise { let featureNames: string[] = []; if (typeof query.tag === 'string') { @@ -913,12 +935,9 @@ export default class ExportImportService tagTypes: filteredTagTypes, dependencies: mappedFeatureDependencies, }; - await this.eventService.storeEvent({ - type: FEATURES_EXPORTED, - createdBy: userName, - createdByUserId: userId, - data: result, - }); + await this.eventService.storeEvent( + new FeaturesExportedEvent({ data: result, auditUser }), + ); return result; } @@ -937,5 +956,4 @@ export default class ExportImportService }); } } - module.exports = ExportImportService; 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 b339277a704..5293285d5d6 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 @@ -16,6 +16,7 @@ import { type IStrategyConfig, type ITagStore, type IVariant, + TEST_AUDIT_USER, } from '../../types'; import { DEFAULT_ENV } from '../../util'; import type { @@ -24,7 +25,6 @@ import type { UpsertSegmentSchema, VariantsSchema, } from '../../openapi'; -import User from '../../types/user'; import type { IContextFieldDto } from '../../types/stores/context-field-store'; let app: IUnleashTest; @@ -67,8 +67,7 @@ const createToggle = async ( await app.services.featureToggleServiceV2.createFeatureToggle( projectId, toggle, - username, - -9999, + TEST_AUDIT_USER, ); if (strategy) { await app.services.featureToggleServiceV2.createStrategy( @@ -78,7 +77,7 @@ const createToggle = async ( featureName: toggle.name, environment: DEFAULT_ENV, }, - username, + TEST_AUDIT_USER, ); } await Promise.all( @@ -89,8 +88,7 @@ const createToggle = async ( type: 'simple', value: tag, }, - username, - userId, + TEST_AUDIT_USER, ); }), ); @@ -110,7 +108,7 @@ const createVariants = async (feature: string, variants: IVariant[]) => { feature, DEFAULT_ENV, variants, - new User({ id: 1 }), + TEST_AUDIT_USER, ); }; @@ -138,9 +136,7 @@ const createProjects = async ( }; const createSegment = (postData: UpsertSegmentSchema): Promise => { - return app.services.segmentService.create(postData, { - email: 'test@example.com', - }); + return app.services.segmentService.create(postData, TEST_AUDIT_USER); }; const unArchiveFeature = async (featureName: string) => { 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 e2ed118ea98..1b621dcc1df 100644 --- a/src/lib/features/feature-toggle/archive-feature-toggle-controller.ts +++ b/src/lib/features/feature-toggle/archive-feature-toggle-controller.ts @@ -182,7 +182,7 @@ export default class ArchiveController extends Controller { ): Promise { const { featureName } = req.params; const user = extractUsername(req); - await this.featureService.deleteFeature(featureName, user, req.user.id); + await this.featureService.deleteFeature(featureName, req.audit); res.status(200).end(); } @@ -190,14 +190,12 @@ export default class ArchiveController extends Controller { req: IAuthRequest<{ featureName: string }>, res: Response, ): Promise { - const userName = extractUsername(req); const { featureName } = req.params; await this.startTransaction(async (tx) => this.transactionalFeatureToggleService(tx).reviveFeature( featureName, - userName, - req.user.id, + req.audit, ), ); 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 1995a7eb94b..208cab5315f 100644 --- a/src/lib/features/feature-toggle/feature-toggle-controller.ts +++ b/src/lib/features/feature-toggle/feature-toggle-controller.ts @@ -16,7 +16,6 @@ import { UPDATE_FEATURE_STRATEGY, } from '../../types'; import type { Logger } from '../../logger'; -import { extractUsername } from '../../util'; import type { IAuthRequest } from '../../routes/unleash-types'; import { type AdminFeaturesQuerySchema, @@ -656,13 +655,11 @@ export default class ProjectFeaturesController extends Controller { ): Promise { const { projectId, featureName } = req.params; const { name, replaceGroupId } = req.body; - const userName = extractUsername(req); const created = await this.featureService.cloneFeatureToggle( featureName, projectId, name, - userName, - req.user.id, + req.audit, replaceGroupId, ); @@ -680,15 +677,13 @@ export default class ProjectFeaturesController extends Controller { ): Promise { const { projectId } = req.params; - const userName = extractUsername(req); const created = await this.featureService.createFeatureToggle( projectId, { ...req.body, description: req.body.description || undefined, }, - userName, - req.user.id, + req.audit, ); this.openApiService.respondWithValidation( @@ -726,7 +721,6 @@ export default class ProjectFeaturesController extends Controller { ): Promise { const { projectId, featureName } = req.params; const { createdAt, ...data } = req.body; - const userName = extractUsername(req); if (data.name && data.name !== featureName) { throw new BadDataError('Cannot change name of feature toggle'); } @@ -736,9 +730,8 @@ export default class ProjectFeaturesController extends Controller { ...data, name: featureName, }, - userName, featureName, - req.user.id, + req.audit, ); this.openApiService.respondWithValidation( @@ -762,9 +755,8 @@ export default class ProjectFeaturesController extends Controller { const updated = await this.featureService.patchFeature( projectId, featureName, - extractUsername(req), req.body, - req.user.id, + req.audit, ); this.openApiService.respondWithValidation( 200, @@ -788,6 +780,7 @@ export default class ProjectFeaturesController extends Controller { this.transactionalFeatureToggleService(tx).archiveToggle( featureName, req.user, + req.audit, projectId, ), ); @@ -800,14 +793,12 @@ export default class ProjectFeaturesController extends Controller { ): Promise { const { features, stale } = req.body; const { projectId } = req.params; - const userName = extractUsername(req); await this.featureService.setToggleStaleness( features, stale, - userName, projectId, - req.user.id, + req.audit, ); res.status(202).end(); } @@ -862,7 +853,7 @@ export default class ProjectFeaturesController extends Controller { featureName, environment, true, - extractUsername(req), + req.audit, req.user, shouldActivateDisabledStrategies === 'true', ); @@ -893,7 +884,7 @@ export default class ProjectFeaturesController extends Controller { features, environment, true, - extractUsername(req), + req.audit, req.user, shouldActivateDisabledStrategies === 'true', ), @@ -925,7 +916,7 @@ export default class ProjectFeaturesController extends Controller { features, environment, false, - extractUsername(req), + req.audit, req.user, shouldActivateDisabledStrategies === 'true', ), @@ -943,7 +934,7 @@ export default class ProjectFeaturesController extends Controller { featureName, environment, false, - extractUsername(req), + req.audit, req.user, ); res.status(200).end(); @@ -964,11 +955,10 @@ export default class ProjectFeaturesController extends Controller { strategyConfig.segmentIds = []; } - const userName = extractUsername(req); const strategy = await this.featureService.createStrategy( strategyConfig, { environment, projectId, featureName }, - userName, + req.audit, req.user, ); @@ -1002,7 +992,6 @@ export default class ProjectFeaturesController extends Controller { res: Response, ): Promise { const { featureName, projectId, environment } = req.params; - const createdBy = extractUsername(req); await this.startTransaction(async (tx) => this.transactionalFeatureToggleService( tx, @@ -1013,7 +1002,7 @@ export default class ProjectFeaturesController extends Controller { projectId, }, req.body, - createdBy, + req.audit, ), ); @@ -1025,7 +1014,6 @@ export default class ProjectFeaturesController extends Controller { res: Response, ): Promise { const { strategyId, environment, projectId, featureName } = req.params; - const userName = extractUsername(req); if (!req.body.segmentIds) { req.body.segmentIds = []; @@ -1036,7 +1024,7 @@ export default class ProjectFeaturesController extends Controller { strategyId, req.body, { environment, projectId, featureName }, - userName, + req.audit, req.user, ), ); @@ -1049,7 +1037,6 @@ export default class ProjectFeaturesController extends Controller { res: Response, ): Promise { const { strategyId, projectId, environment, featureName } = req.params; - const userName = extractUsername(req); const patch = req.body; const strategy = await this.featureService.getStrategy(strategyId); @@ -1059,7 +1046,7 @@ export default class ProjectFeaturesController extends Controller { strategyId, newDocument, { environment, projectId, featureName }, - userName, + req.audit, req.user, ), ); @@ -1084,13 +1071,12 @@ export default class ProjectFeaturesController extends Controller { ): Promise { this.logger.info('Deleting strategy'); const { environment, projectId, featureName } = req.params; - const userName = extractUsername(req); const { strategyId } = req.params; this.logger.info(strategyId); const strategy = await this.featureService.deleteStrategy( strategyId, { environment, projectId, featureName }, - userName, + req.audit, req.user, ); res.status(200).json(strategy); @@ -1106,7 +1092,6 @@ export default class ProjectFeaturesController extends Controller { res: Response, ): Promise { const { strategyId, environment, projectId, featureName } = req.params; - const userName = extractUsername(req); const { name, value } = req.body; const updatedStrategy = @@ -1115,8 +1100,7 @@ export default class ProjectFeaturesController extends Controller { name, value, { environment, projectId, featureName }, - userName, - req.user.id, + req.audit, ); res.status(200).json(updatedStrategy); } @@ -1126,13 +1110,11 @@ export default class ProjectFeaturesController extends Controller { res: Response, ): Promise { const { features, tags } = req.body; - const userName = extractUsername(req); await this.featureTagService.updateTags( features, tags.addedTags, tags.removedTags, - userName, - req.user.id, + req.audit, ); 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 90861baffef..7abd421bb3d 100644 --- a/src/lib/features/feature-toggle/feature-toggle-service.ts +++ b/src/lib/features/feature-toggle/feature-toggle-service.ts @@ -1,7 +1,6 @@ import { CREATE_FEATURE_STRATEGY, EnvironmentVariantEvent, - FEATURE_UPDATED, FeatureArchivedEvent, FeatureChangeProjectEvent, FeatureCreatedEvent, @@ -18,7 +17,9 @@ import { type FeatureToggleLegacy, type FeatureToggleWithDependencies, type FeatureToggleWithEnvironment, + FeatureUpdatedEvent, FeatureVariantEvent, + type IAuditUser, type IConstraint, type IDependency, type IFeatureEnvironmentInfo, @@ -44,7 +45,7 @@ import { SKIP_CHANGE_REQUEST, StrategiesOrderChangedEvent, type StrategyIds, - SYSTEM_USER_ID, + SYSTEM_USER_AUDIT, type Unsaved, WeightType, } from '../../types'; @@ -73,7 +74,6 @@ import type { import { DATE_OPERATORS, DEFAULT_ENV, - extractUsernameFromUser, NUM_OPERATORS, SEMVER_OPERATORS, STRING_OPERATORS, @@ -106,8 +106,8 @@ import type { DependentFeaturesService } from '../dependent-features/dependent-f import type { FeatureToggleInsert } from './feature-toggle-store'; import ArchivedFeatureError from '../../error/archivedfeature-error'; import { FEATURES_CREATED_BY_PROCESSED } from '../../metric-events'; -import type { EventEmitter } from 'stream'; import { allSettledWithRejection } from '../../util/allSettledWithRejection'; +import type EventEmitter from 'node:events'; interface IFeatureContext { featureName: string; @@ -424,9 +424,8 @@ class FeatureToggleService { async patchFeature( project: string, featureName: string, - createdBy: string, operations: Operation[], - createdByUserId: number, + auditUser: IAuditUser, ): Promise { const featureToggle = await this.getFeatureMetadata(featureName); @@ -443,9 +442,8 @@ class FeatureToggleService { const updated = await this.updateFeatureToggle( project, newDocument, - createdBy, featureName, - createdByUserId, + auditUser, ); if (featureToggle.stale !== newDocument.stale) { @@ -454,8 +452,7 @@ class FeatureToggleService { stale: newDocument.stale, project, featureName, - createdBy, - createdByUserId, + auditUser, }), ); } @@ -485,7 +482,7 @@ class FeatureToggleService { async updateStrategiesSortOrder( context: IFeatureStrategyContext, sortOrders: SetStrategySortOrderSchema, - createdBy: string, + auditUser: IAuditUser, user?: IUser, ): Promise> { await this.stopWhenChangeRequestsEnabled( @@ -497,16 +494,14 @@ class FeatureToggleService { return this.unprotectedUpdateStrategiesSortOrder( context, sortOrders, - createdBy, - user?.id || SYSTEM_USER_ID, + auditUser, ); } async unprotectedUpdateStrategiesSortOrder( context: IFeatureStrategyContext, sortOrders: SetStrategySortOrderSchema, - createdBy: string, - createdByUserId: number, + auditUser: IAuditUser, ): Promise> { const { featureName, environment, projectId: project } = context; const existingOrder = ( @@ -561,10 +556,9 @@ class FeatureToggleService { featureName, environment, project, - createdBy, preData: eventPreData, data: eventData, - createdByUserId, + auditUser, }); await this.eventService.storeEvent(event); } @@ -572,7 +566,7 @@ class FeatureToggleService { async createStrategy( strategyConfig: Unsaved, context: IFeatureStrategyContext, - createdBy: string, + auditUser: IAuditUser, user?: IUser, ): Promise> { await this.stopWhenChangeRequestsEnabled( @@ -583,16 +577,14 @@ class FeatureToggleService { return this.unprotectedCreateStrategy( strategyConfig, context, - createdBy, - user?.id || SYSTEM_USER_ID, + auditUser, ); } async unprotectedCreateStrategy( strategyConfig: Unsaved, context: IFeatureStrategyContext, - createdBy: string, - createdByUserId: number, + auditUser: IAuditUser, ): Promise> { const { featureName, projectId, environment } = context; await this.validateFeatureBelongsToProject(context); @@ -666,10 +658,9 @@ class FeatureToggleService { new FeatureStrategyAddEvent({ project: projectId, featureName, - createdBy, environment, data: strategy, - createdByUserId, + auditUser, }), ); return strategy; @@ -691,14 +682,14 @@ class FeatureToggleService { * @param id * @param updates * @param context - Which context does this strategy live in (projectId, featureName, environment) - * @param userName - Human readable id of the user performing the update + * @param auditUser - Audit info about the user performing the update * @param user - Optional User object performing the action */ async updateStrategy( id: string, updates: Partial, context: IFeatureStrategyContext, - userName: string, + auditUser: IAuditUser, user?: IUser, ): Promise> { await this.stopWhenChangeRequestsEnabled( @@ -710,7 +701,7 @@ class FeatureToggleService { id, updates, context, - userName, + auditUser, user, ); } @@ -719,7 +710,7 @@ class FeatureToggleService { featureName: string, environment: string, projectId: string, - userName: string, + auditUser: IAuditUser, user?: IUser, ): Promise { const feature = await this.getFeature({ featureName }); @@ -734,7 +725,7 @@ class FeatureToggleService { featureName, environment, false, - userName, + auditUser, user, ); } @@ -744,7 +735,7 @@ class FeatureToggleService { id: string, updates: Partial, context: IFeatureStrategyContext, - userName: string, + auditUser: IAuditUser, user?: IUser, ): Promise> { const { projectId, environment, featureName } = context; @@ -798,17 +789,16 @@ class FeatureToggleService { project: projectId, featureName, environment, - createdBy: userName, data, preData, - createdByUserId: user?.id || SYSTEM_USER_ID, + auditUser, }), ); await this.optionallyDisableFeature( featureName, environment, projectId, - userName, + auditUser, user, ); return data; @@ -821,8 +811,7 @@ class FeatureToggleService { name: string, value: string | number, context: IFeatureStrategyContext, - userName: string, - createdByUserId: number, + auditUser: IAuditUser, ): Promise> { const { projectId, environment, featureName } = context; @@ -850,10 +839,9 @@ class FeatureToggleService { featureName, project: projectId, environment, - createdBy: userName, data, preData, - createdByUserId, + auditUser, }), ); return data; @@ -868,13 +856,13 @@ class FeatureToggleService { * } * @param id - strategy id * @param context - Which context does this strategy live in (projectId, featureName, environment) - * @param createdBy - Which user does this strategy belong to + * @param auditUser - Audit information about user performing the action (userid, username, ip) * @param user */ async deleteStrategy( id: string, context: IFeatureStrategyContext, - createdBy: string, + auditUser: IAuditUser, user?: IUser, ): Promise { await this.stopWhenChangeRequestsEnabled( @@ -882,14 +870,13 @@ class FeatureToggleService { context.environment, user, ); - return this.unprotectedDeleteStrategy(id, context, createdBy, user); + return this.unprotectedDeleteStrategy(id, context, auditUser); } async unprotectedDeleteStrategy( id: string, context: IFeatureStrategyContext, - createdBy: string, - createdByUser?: IUser, + auditUser: IAuditUser, ): Promise { const existingStrategy = await this.featureStrategiesStore.get(id); const { featureName, projectId, environment } = context; @@ -915,8 +902,7 @@ class FeatureToggleService { featureName, environment, false, - createdBy, - createdByUser, + auditUser, ); } @@ -927,8 +913,7 @@ class FeatureToggleService { featureName, project: projectId, environment, - createdBy, - createdByUserId: createdByUser?.id || SYSTEM_USER_ID, + auditUser, preData, }), ); @@ -1162,11 +1147,12 @@ class FeatureToggleService { async createFeatureToggle( projectId: string, value: FeatureToggleDTO, - createdBy: string, - createdByUserId: number, + auditUser: IAuditUser, isValidated: boolean = false, ): Promise { - this.logger.info(`${createdBy} creates feature toggle ${value.name}`); + this.logger.info( + `${auditUser.username} creates feature toggle ${value.name}`, + ); await this.validateName(value.name); await this.validateFeatureFlagNameAgainstPattern(value.name, projectId); @@ -1180,12 +1166,12 @@ class FeatureToggleService { if (exists) { let featureData: FeatureToggleInsert; if (isValidated) { - featureData = { createdByUserId, ...value }; + featureData = { createdByUserId: auditUser.id, ...value }; } else { const validated = await featureMetadataSchema.validateAsync(value); featureData = { - createdByUserId, + createdByUserId: auditUser.id, ...validated, }; } @@ -1215,10 +1201,9 @@ class FeatureToggleService { await this.eventService.storeEvent( new FeatureCreatedEvent({ featureName, - createdBy, project: projectId, data: createdToggle, - createdByUserId, + auditUser, }), ); @@ -1298,8 +1283,7 @@ class FeatureToggleService { featureName: string, projectId: string, newFeatureName: string, - userName: string, - userId: number, + auditUser: IAuditUser, replaceGroupId: boolean = true, ): Promise { const changeRequestEnabled = @@ -1312,7 +1296,7 @@ class FeatureToggleService { ); } this.logger.info( - `${userName} clones feature toggle ${featureName} to ${newFeatureName}`, + `${auditUser.username} clones feature toggle ${featureName} to ${newFeatureName}`, ); await this.validateName(newFeatureName); @@ -1330,8 +1314,7 @@ class FeatureToggleService { const created = await this.createFeatureToggle( projectId, newToggle, - userName, - userId, + auditUser, ); const variantTasks = newToggle.environments.map((e) => { @@ -1356,7 +1339,7 @@ class FeatureToggleService { featureName: newFeatureName, environment: e.name, }; - return this.createStrategy(s, context, userName); + return this.createStrategy(s, context, auditUser); }), ); @@ -1367,8 +1350,7 @@ class FeatureToggleService { newFeatureName, projectId, }, - userName, - userId, + auditUser, ); await Promise.all([ @@ -1383,16 +1365,17 @@ class FeatureToggleService { async updateFeatureToggle( projectId: string, updatedFeature: FeatureToggleDTO, - userName: string, featureName: string, - userId: number, + auditUser: IAuditUser, ): Promise { await this.validateFeatureBelongsToProject({ featureName, projectId, }); - this.logger.info(`${userName} updates feature toggle ${featureName}`); + this.logger.info( + `${auditUser.username} updates feature toggle ${featureName}`, + ); const featureData = await featureMetadataSchema.validateAsync(updatedFeature); @@ -1406,8 +1389,7 @@ class FeatureToggleService { await this.eventService.storeEvent( new FeatureMetadataUpdateEvent({ - createdBy: userName, - createdByUserId: userId, + auditUser, data: featureToggle, preData, featureName, @@ -1531,8 +1513,7 @@ class FeatureToggleService { async updateStale( featureName: string, isStale: boolean, - createdBy: string, - createdByUserId: number, + auditUser: IAuditUser, ): Promise { const feature = await this.featureToggleStore.get(featureName); const { project } = feature; @@ -1544,8 +1525,7 @@ class FeatureToggleService { stale: isStale, project, featureName, - createdBy, - createdByUserId, + auditUser, }), ); @@ -1555,6 +1535,7 @@ class FeatureToggleService { async archiveToggle( featureName: string, user: IUser, + auditUser: IAuditUser, projectId?: string, ): Promise { if (projectId) { @@ -1564,18 +1545,12 @@ class FeatureToggleService { user, ); } - await this.unprotectedArchiveToggle( - featureName, - extractUsernameFromUser(user), - user.id, - projectId, - ); + await this.unprotectedArchiveToggle(featureName, auditUser, projectId); } async unprotectedArchiveToggle( featureName: string, - createdBy: string, - createdByUserId: number, + auditUser: IAuditUser, projectId?: string, ): Promise { const feature = await this.featureToggleStore.get(featureName); @@ -1595,16 +1570,14 @@ class FeatureToggleService { await this.dependentFeaturesService.unprotectedDeleteFeaturesDependencies( [featureName], projectId, - createdBy, - createdByUserId, + auditUser, ); } await this.eventService.storeEvent( new FeatureArchivedEvent({ featureName, - createdBy, - createdByUserId, + auditUser, project: feature.project, }), ); @@ -1613,14 +1586,14 @@ class FeatureToggleService { async archiveToggles( featureNames: string[], user: IUser, + auditUser: IAuditUser, projectId: string, ): Promise { await this.stopWhenChangeRequestsEnabled(projectId, undefined, user); await this.unprotectedArchiveToggles( featureNames, - extractUsernameFromUser(user), projectId, - user.id, + auditUser, ); } @@ -1644,9 +1617,8 @@ class FeatureToggleService { async unprotectedArchiveToggles( featureNames: string[], - createdBy: string, projectId: string, - createdByUserId: number, + auditUser: IAuditUser, ): Promise { await Promise.all([ this.validateFeaturesContext(featureNames, projectId), @@ -1659,8 +1631,7 @@ class FeatureToggleService { await this.dependentFeaturesService.unprotectedDeleteFeaturesDependencies( featureNames, projectId, - createdBy, - createdByUserId, + auditUser, ); await this.eventService.storeEvents( @@ -1668,9 +1639,8 @@ class FeatureToggleService { (feature) => new FeatureArchivedEvent({ featureName: feature.name, - createdBy, - createdByUserId, project: feature.project, + auditUser, }), ), ); @@ -1679,9 +1649,8 @@ class FeatureToggleService { async setToggleStaleness( featureNames: string[], stale: boolean, - createdBy: string, projectId: string, - createdByUserId: number, + auditUser: IAuditUser, ): Promise { await this.validateFeaturesContext(featureNames, projectId); @@ -1702,8 +1671,7 @@ class FeatureToggleService { stale: stale, project: projectId, featureName: feature.name, - createdBy, - createdByUserId, + auditUser, }), ), ); @@ -1714,7 +1682,7 @@ class FeatureToggleService { featureNames: string[], environment: string, enabled: boolean, - createdBy: string, + auditUser: IAuditUser, user?: IUser, shouldActivateDisabledStrategies = false, ): Promise { @@ -1725,7 +1693,7 @@ class FeatureToggleService { featureName, environment, enabled, - createdBy, + auditUser, user, shouldActivateDisabledStrategies, ), @@ -1738,7 +1706,7 @@ class FeatureToggleService { featureName: string, environment: string, enabled: boolean, - createdBy: string, + auditUser: IAuditUser, user?: IUser, shouldActivateDisabledStrategies = false, ): Promise { @@ -1757,7 +1725,7 @@ class FeatureToggleService { featureName, environment, enabled, - createdBy, + auditUser, user, shouldActivateDisabledStrategies, ); @@ -1768,7 +1736,7 @@ class FeatureToggleService { featureName: string, environment: string, enabled: boolean, - createdBy: string, + auditUser: IAuditUser, user?: IUser, shouldActivateDisabledStrategies = false, ): Promise { @@ -1811,7 +1779,7 @@ class FeatureToggleService { projectId: project, featureName, }, - createdBy, + auditUser, user, ), ), @@ -1846,8 +1814,7 @@ class FeatureToggleService { projectId: project, featureName, }, - createdBy, - user?.id || SYSTEM_USER_ID, + auditUser, ); } } @@ -1866,8 +1833,7 @@ class FeatureToggleService { project, featureName, environment, - createdBy, - createdByUserId: user?.id || SYSTEM_USER_ID, + auditUser, }), ); } @@ -1877,21 +1843,20 @@ class FeatureToggleService { // @deprecated async storeFeatureUpdatedEventLegacy( featureName: string, - createdBy: string, - createdByUserId: number, + auditUser: IAuditUser, ): Promise { const feature = await this.getFeatureToggleLegacy(featureName); // Legacy event. Will not be used from v4.3. // We do not include 'preData' on purpose. - await this.eventService.storeEvent({ - type: FEATURE_UPDATED, - createdBy, - createdByUserId, - featureName, - data: feature, - project: feature.project, - }); + await this.eventService.storeEvent( + new FeatureUpdatedEvent({ + featureName, + data: feature, + project: feature.project, + auditUser, + }), + ); return feature; } @@ -1900,7 +1865,7 @@ class FeatureToggleService { projectId: string, featureName: string, environment: string, - userName: string, + auditUser: IAuditUser, ): Promise { await this.featureToggleStore.get(featureName); const isEnabled = @@ -1913,7 +1878,7 @@ class FeatureToggleService { featureName, environment, !isEnabled, - userName, + auditUser, ); } @@ -1939,8 +1904,7 @@ class FeatureToggleService { async changeProject( featureName: string, newProject: string, - createdBy: string, - createdByUserId: number, + auditUser: IAuditUser, ): Promise { const changeRequestEnabled = await this.changeRequestAccessReadModel.isChangeRequestsEnabledForProject( @@ -1967,8 +1931,7 @@ class FeatureToggleService { await this.eventService.storeEvent( new FeatureChangeProjectEvent({ - createdBy, - createdByUserId, + auditUser, oldProject, newProject, featureName, @@ -1983,8 +1946,7 @@ class FeatureToggleService { // TODO: add project id. async deleteFeature( featureName: string, - createdBy: string, - createdByUserId: number, + auditUser: IAuditUser, ): Promise { await this.validateNoChildren(featureName); const toggle = await this.featureToggleStore.get(featureName); @@ -1995,8 +1957,7 @@ class FeatureToggleService { new FeatureDeletedEvent({ featureName, project: toggle.project, - createdBy, - createdByUserId, + auditUser, preData: toggle, tags, }), @@ -2006,8 +1967,7 @@ class FeatureToggleService { async deleteFeatures( featureNames: string[], projectId: string, - createdBy: string, - createdByUserId: number, + auditUser: IAuditUser, ): Promise { await this.validateFeaturesContext(featureNames, projectId); await this.validateNoOrphanParents(featureNames); @@ -2028,8 +1988,7 @@ class FeatureToggleService { (feature) => new FeatureDeletedEvent({ featureName: feature.name, - createdBy, - createdByUserId, + auditUser, project: feature.project, preData: feature, tags: tags @@ -2046,8 +2005,7 @@ class FeatureToggleService { async reviveFeatures( featureNames: string[], projectId: string, - createdBy: string, - createdByUserId: number, + auditUser: IAuditUser, ): Promise { await this.validateFeaturesContext(featureNames, projectId); @@ -2070,8 +2028,7 @@ class FeatureToggleService { (feature) => new FeatureRevivedEvent({ featureName: feature.name, - createdBy, - createdByUserId, + auditUser, project: feature.project, }), ), @@ -2081,8 +2038,7 @@ class FeatureToggleService { // TODO: add project id. async reviveFeature( featureName: string, - createdBy: string, - createdByUserId: number, + auditUser: IAuditUser, ): Promise { const toggle = await this.featureToggleStore.revive(featureName); await this.featureToggleStore.disableAllEnvironmentsForFeatures([ @@ -2090,8 +2046,7 @@ class FeatureToggleService { ]); await this.eventService.storeEvent( new FeatureRevivedEvent({ - createdBy, - createdByUserId, + auditUser, featureName, project: toggle.project, }), @@ -2141,6 +2096,7 @@ class FeatureToggleService { project: string, newVariants: Operation[], user: IUser, + auditUser: IAuditUser, ): Promise { const ft = await this.featureStrategiesStore.getFeatureToggleWithVariantEnvs( @@ -2153,6 +2109,7 @@ class FeatureToggleService { env.name, newVariants, user, + auditUser, ).then((resultingVariants) => { env.variants = resultingVariants; }), @@ -2168,6 +2125,7 @@ class FeatureToggleService { environment: string, newVariants: Operation[], user: IUser, + auditUser: IAuditUser, ): Promise { const oldVariants = await this.getVariantsForEnv( featureName, @@ -2183,6 +2141,7 @@ class FeatureToggleService { environment, newDocument, user, + auditUser, oldVariants, ); } @@ -2191,8 +2150,7 @@ class FeatureToggleService { featureName: string, project: string, newVariants: IVariant[], - createdBy: string, - createdByUserId: number, + auditUser: IAuditUser, ): Promise { await variantsArraySchema.validateAsync(newVariants); const fixedVariants = this.fixVariantWeights(newVariants); @@ -2208,8 +2166,7 @@ class FeatureToggleService { new FeatureVariantEvent({ project, featureName, - createdBy, - createdByUserId, + auditUser, oldVariants, newVariants: featureToggle.variants as IVariant[], }), @@ -2222,7 +2179,7 @@ class FeatureToggleService { featureName: string, environment: string, newVariants: IVariant[], - user: IUser, + auditUser: IAuditUser, oldVariants?: IVariant[], ): Promise { await variantsArraySchema.validateAsync(newVariants); @@ -2241,11 +2198,10 @@ class FeatureToggleService { new EnvironmentVariantEvent({ featureName, environment, - createdByUserId: user.id, project: projectId, - createdBy: user, oldVariants: theOldVariants, newVariants: fixedVariants, + auditUser, }), ); await this.featureEnvironmentStore.setVariantsToFeatureEnvironments( @@ -2262,6 +2218,7 @@ class FeatureToggleService { environment: string, newVariants: IVariant[], user: IUser, + auditUser: IAuditUser, oldVariants?: IVariant[], ): Promise { await this.stopWhenChangeRequestsEnabled(projectId, environment, user); @@ -2270,7 +2227,7 @@ class FeatureToggleService { featureName, environment, newVariants, - user, + auditUser, oldVariants, ); } @@ -2281,6 +2238,7 @@ class FeatureToggleService { environments: string[], newVariants: IVariant[], user: IUser, + auditUser: IAuditUser, ): Promise { for (const env of environments) { await this.stopWhenChangeRequestsEnabled(projectId, env); @@ -2290,7 +2248,7 @@ class FeatureToggleService { featureName, environments, newVariants, - user, + auditUser, ); } @@ -2299,7 +2257,7 @@ class FeatureToggleService { featureName: string, environments: string[], newVariants: IVariant[], - user: IUser, + auditUser: IAuditUser, ): Promise { await variantsArraySchema.validateAsync(newVariants); const fixedVariants = this.fixVariantWeights(newVariants); @@ -2321,10 +2279,9 @@ class FeatureToggleService { featureName, environment, project: projectId, - createdBy: user, oldVariants: oldVariants[environment], newVariants: fixedVariants, - createdByUserId: user.id, + auditUser, }), ), ); @@ -2443,7 +2400,7 @@ class FeatureToggleService { ({ name, project }) => new PotentiallyStaleOnEvent({ featureName: name, - createdByUserId: SYSTEM_USER_ID, + auditUser: SYSTEM_USER_AUDIT, 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 1df8d8aa84f..deb913ba95a 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 @@ -1,7 +1,6 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import type { Request, Response } from 'express'; import Controller from '../../../routes/controller'; -import { extractUsername } from '../../../util/extract-user'; import { NONE, UPDATE_FEATURE } from '../../../types/permissions'; import type { IUnleashConfig } from '../../../types/option'; import type { IUnleashServices } from '../../../types'; @@ -255,12 +254,10 @@ class FeatureController extends Controller { res: Response, ): Promise { const { featureName } = req.params; - const userName = extractUsername(req); const tag = await this.tagService.addTag( featureName, req.body, - userName, - req.user.id, + req.audit, ); res.status(201).header('location', `${featureName}/tags`).json(tag); } @@ -276,27 +273,16 @@ class FeatureController extends Controller { ): Promise { const { featureName } = req.params; const { addedTags, removedTags } = req.body; - const userName = extractUsername(req); await Promise.all( addedTags.map((addedTag) => - this.tagService.addTag( - featureName, - addedTag, - userName, - req.user.id, - ), + this.tagService.addTag(featureName, addedTag, req.audit), ), ); await Promise.all( removedTags.map((removedTag) => - this.tagService.removeTag( - featureName, - removedTag, - userName, - req.user.id, - ), + this.tagService.removeTag(featureName, removedTag, req.audit), ), ); @@ -310,12 +296,10 @@ class FeatureController extends Controller { res: Response, ): Promise { const { featureName, type, value } = req.params; - const userName = extractUsername(req); await this.tagService.removeTag( featureName, { type, value }, - userName, - req.user.id, + req.audit, ); res.status(200).end(); } @@ -335,7 +319,6 @@ class FeatureController extends Controller { } async createToggle(req: IAuthRequest, res: Response): Promise { - const userName = extractUsername(req); const toggle = req.body; const validatedToggle = await featureSchema.validateAsync(toggle); @@ -343,8 +326,7 @@ class FeatureController extends Controller { const createdFeature = await this.service.createFeatureToggle( project, validatedToggle, - userName, - req.user.id, + req.audit, true, ); const strategies = await Promise.all( @@ -356,7 +338,7 @@ class FeatureController extends Controller { featureName: name, environment: DEFAULT_ENV, }, - userName, + req.audit, req.user, ), ), @@ -366,15 +348,9 @@ class FeatureController extends Controller { name, DEFAULT_ENV, enabled, - userName, - ); - await this.service.saveVariants( - name, - project, - variants, - userName, - req.user.id, + req.audit, ); + await this.service.saveVariants(name, project, variants, req.audit); res.status(201).json({ ...createdFeature, @@ -386,7 +362,6 @@ class FeatureController extends Controller { async updateToggle(req: IAuthRequest, res: Response): Promise { const { featureName } = req.params; - const userName = extractUsername(req); const updatedFeature = req.body; updatedFeature.name = featureName; @@ -397,9 +372,8 @@ class FeatureController extends Controller { await this.service.updateFeatureToggle( projectId, value, - userName, featureName, - req.user.id, + req.audit, ); await this.service.removeAllStrategiesForEnv(featureName); @@ -414,7 +388,7 @@ class FeatureController extends Controller { featureName, environment: DEFAULT_ENV, }, - userName, + req.audit, req.user, ), ), @@ -425,21 +399,19 @@ class FeatureController extends Controller { featureName, DEFAULT_ENV, updatedFeature.enabled, - userName, + req.audit, req.user, ); await this.service.saveVariants( featureName, projectId!!, value.variants || [], - userName, - req.user.id, + req.audit, ); const feature = await this.service.storeFeatureUpdatedEventLegacy( featureName, - userName, - req.user.id, + req.audit, ); res.status(200).json(feature); @@ -451,83 +423,67 @@ class FeatureController extends Controller { * Kept to keep backward compatibility */ async toggle(req: IAuthRequest, res: Response): Promise { - const userName = extractUsername(req); const { featureName } = req.params; const projectId = await this.service.getProjectId(featureName); const feature = await this.service.toggle( projectId, featureName, DEFAULT_ENV, - userName, + req.audit, ); await this.service.storeFeatureUpdatedEventLegacy( featureName, - userName, - req.user.id, + req.audit, ); res.status(200).json(feature); } async toggleOn(req: IAuthRequest, res: Response): Promise { const { featureName } = req.params; - const userName = extractUsername(req); const projectId = await this.service.getProjectId(featureName); const feature = await this.service.updateEnabled( projectId, featureName, DEFAULT_ENV, true, - userName, + req.audit, + req.user, ); await this.service.storeFeatureUpdatedEventLegacy( featureName, - userName, - req.user.id, + req.audit, ); res.json(feature); } async toggleOff(req: IAuthRequest, res: Response): Promise { const { featureName } = req.params; - const userName = extractUsername(req); const projectId = await this.service.getProjectId(featureName); const feature = await this.service.updateEnabled( projectId, featureName, DEFAULT_ENV, false, - userName, + req.audit, + req.user, ); await this.service.storeFeatureUpdatedEventLegacy( featureName, - userName, - req.user.id, + req.audit, ); res.json(feature); } async staleOn(req: IAuthRequest, res: Response): Promise { const { featureName } = req.params; - const userName = extractUsername(req); - await this.service.updateStale( - featureName, - true, - userName, - req.user.id, - ); + await this.service.updateStale(featureName, true, req.audit); const feature = await this.service.getFeatureToggleLegacy(featureName); res.json(feature); } async staleOff(req: IAuthRequest, res: Response): Promise { const { featureName } = req.params; - const userName = extractUsername(req); - await this.service.updateStale( - featureName, - false, - userName, - req.user.id, - ); + await this.service.updateStale(featureName, false, req.audit); const feature = await this.service.getFeatureToggleLegacy(featureName); res.json(feature); } @@ -535,7 +491,7 @@ class FeatureController extends Controller { async archiveToggle(req: IAuthRequest, res: Response): Promise { const { featureName } = req.params; - await this.service.archiveToggle(featureName, req.user); + await this.service.archiveToggle(featureName, req.user, req.audit); res.status(200).end(); } } 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 1d81ce37026..b0b9c61b586 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 @@ -3,7 +3,7 @@ import { createTestConfig } from '../../../../test/config/test-config'; import dbInit, { type ITestDb, } from '../../../../test/e2e/helpers/database-init'; -import { DEFAULT_ENV } from '../../../util'; +import { DEFAULT_ENV, extractAuditInfoFromUser } from '../../../util'; import type { FeatureStrategySchema } from '../../../openapi'; import type User from '../../../types/user'; import { @@ -12,8 +12,8 @@ import { type IUnleashStores, type IVariant, SKIP_CHANGE_REQUEST, - SYSTEM_USER, - SYSTEM_USER_ID, + SYSTEM_USER_AUDIT, + TEST_AUDIT_USER, } from '../../../types'; import EnvironmentService from '../../project-environments/environment-service'; import { ForbiddenError, PatternError, PermissionError } from '../../../error'; @@ -81,14 +81,13 @@ test('Should create feature toggle strategy configuration', async () => { { name: 'Demo', }, - 'test', - TEST_USER_ID, + TEST_AUDIT_USER, ); const createdConfig = await service.createStrategy( config, { projectId, featureName: 'Demo', environment: DEFAULT_ENV }, - username, + TEST_AUDIT_USER, ); expect(createdConfig.name).toEqual('default'); @@ -110,21 +109,20 @@ test('Should be able to update existing strategy configuration', async () => { { name: featureName, }, - 'test', - TEST_USER_ID, + TEST_AUDIT_USER, ); const createdConfig = await service.createStrategy( config, { projectId, featureName, environment: DEFAULT_ENV }, - username, + TEST_AUDIT_USER, ); expect(createdConfig.name).toEqual('default'); const updatedConfig = await service.updateStrategy( createdConfig.id, { parameters: { b2b: 'true' } }, { projectId, featureName, environment: DEFAULT_ENV }, - username, + TEST_AUDIT_USER, ); expect(createdConfig.id).toEqual(updatedConfig.id); expect(updatedConfig.parameters).toEqual({ b2b: 'true' }); @@ -147,14 +145,13 @@ test('Should be able to get strategy by id', async () => { { name: featureName, }, - userName, - TEST_USER_ID, + TEST_AUDIT_USER, ); const createdConfig = await service.createStrategy( config, { projectId, featureName, environment: DEFAULT_ENV }, - userName, + TEST_AUDIT_USER, ); const fetchedConfig = await service.getStrategy(createdConfig.id); expect(fetchedConfig).toEqual(createdConfig); @@ -173,8 +170,7 @@ test('should ignore name in the body when updating feature toggle', async () => name: featureName, description: 'First toggle', }, - userName, - TEST_USER_ID, + TEST_AUDIT_USER, ); await service.createFeatureToggle( @@ -183,8 +179,7 @@ test('should ignore name in the body when updating feature toggle', async () => name: secondFeatureName, description: 'Second toggle', }, - userName, - TEST_USER_ID, + TEST_AUDIT_USER, ); const update = { @@ -195,9 +190,8 @@ test('should ignore name in the body when updating feature toggle', async () => await service.updateFeatureToggle( projectId, update, - userName, featureName, - TEST_USER_ID, + TEST_AUDIT_USER, ); const featureOne = await service.getFeature({ featureName }); const featureTwo = await service.getFeature({ @@ -219,8 +213,7 @@ test('should not get empty rows as features', async () => { name: 'linked-with-segment', description: 'First toggle', }, - userName, - TEST_USER_ID, + TEST_AUDIT_USER, ); await service.createFeatureToggle( @@ -229,8 +222,7 @@ test('should not get empty rows as features', async () => { name: 'not-linked-with-segment', description: 'Second toggle', }, - userName, - TEST_USER_ID, + TEST_AUDIT_USER, ); const user = { email: 'test@example.com' } as User; @@ -238,7 +230,7 @@ test('should not get empty rows as features', async () => { name: 'Unlinked segment', constraints: mockConstraints(), }; - await segmentService.create(postData, user); + await segmentService.create(postData, extractAuditInfoFromUser(user)); const features = await service.getClientFeatures(); const namelessFeature = features.find((p) => !p.name); @@ -270,8 +262,7 @@ test('adding and removing an environment preserves variants when variants per en }, ], }, - 'random_user', - TEST_USER_ID, + TEST_AUDIT_USER, ); //force the variantEnvironments flag off so that we can test legacy behavior @@ -291,20 +282,17 @@ test('adding and removing an environment preserves variants when variants per en await environmentService.addEnvironmentToProject( prodEnv, 'default', - SYSTEM_USER.username, - SYSTEM_USER.id, + SYSTEM_USER_AUDIT, ); await environmentService.removeEnvironmentFromProject( prodEnv, 'default', - SYSTEM_USER.username, - SYSTEM_USER.id, + SYSTEM_USER_AUDIT, ); await environmentService.addEnvironmentToProject( prodEnv, 'default', - SYSTEM_USER.username, - SYSTEM_USER.id, + SYSTEM_USER_AUDIT, ); const toggle = await service.getFeature({ @@ -325,8 +313,7 @@ test('cloning a feature toggle copies variant environments correctly', async () { name: newToggleName, }, - 'test', - TEST_USER_ID, + TEST_AUDIT_USER, ); await stores.environmentStore.create({ @@ -356,8 +343,7 @@ test('cloning a feature toggle copies variant environments correctly', async () newToggleName, 'default', clonedToggleName, - 'test-user', - SYSTEM_USER_ID, + SYSTEM_USER_AUDIT, true, ); @@ -385,8 +371,7 @@ test('cloning a feature toggle not allowed for change requests enabled', async ( 'newToggleName', 'default', 'clonedToggleName', - 'test-user', - SYSTEM_USER_ID, + SYSTEM_USER_AUDIT, true, ), ).rejects.toEqual( @@ -402,7 +387,7 @@ test('changing to a project with change requests enabled should not be allowed', environment: 'default', }); await expect( - service.changeProject('newToggleName', 'default', 'user', TEST_USER_ID), + service.changeProject('newToggleName', 'default', TEST_AUDIT_USER), ).rejects.toEqual( new ForbiddenError( `Changing project not allowed. Project default has change requests enabled.`, @@ -419,9 +404,7 @@ test('Cloning a feature toggle also clones segments correctly', async () => { name: 'SomeSegment', constraints: mockConstraints(), }, - { - email: 'test@example.com', - }, + TEST_AUDIT_USER, ); await service.createFeatureToggle( @@ -429,8 +412,7 @@ test('Cloning a feature toggle also clones segments correctly', async () => { { name: featureName, }, - 'test-user', - TEST_USER_ID, + TEST_AUDIT_USER, ); const config: Omit = { @@ -443,15 +425,14 @@ test('Cloning a feature toggle also clones segments correctly', async () => { await service.createStrategy( config, { projectId: 'default', featureName, environment: DEFAULT_ENV }, - 'test-user', + TEST_AUDIT_USER, ); await service.cloneFeatureToggle( featureName, 'default', clonedFeatureName, - 'test-user', - SYSTEM_USER_ID, + TEST_AUDIT_USER, true, ); @@ -469,8 +450,7 @@ test('If change requests are enabled, cannot change variants without going via C await service.createFeatureToggle( 'default', { name: featureName }, - 'test-user', - TEST_USER_ID, + TEST_AUDIT_USER, ); // Force all feature flags on to make sure we have Change requests on @@ -510,6 +490,7 @@ test('If change requests are enabled, cannot change variants without going via C username: '', isAPI: true, }, + TEST_AUDIT_USER, [], ), ).rejects.toThrowError(new PermissionError(SKIP_CHANGE_REQUEST)); @@ -551,8 +532,7 @@ test('If CRs are protected for any environment in the project stops bulk update const toggle = await service.createFeatureToggle( project.id, { name: 'crOnVariantToggle' }, - user.username, - user.id, + TEST_AUDIT_USER, ); const variant: IVariant = { @@ -571,7 +551,7 @@ test('If CRs are protected for any environment in the project stops bulk update toggle.name, [enabledEnv.name, disabledEnv.name], [variant], - user, + TEST_AUDIT_USER, ); const newVariants = [ @@ -601,6 +581,7 @@ test('If CRs are protected for any environment in the project stops bulk update username: '', isAPI: true, }, + TEST_AUDIT_USER, ), ).rejects.toThrowError(new PermissionError(SKIP_CHANGE_REQUEST)); }); @@ -622,14 +603,13 @@ test('getPlaygroundFeatures should return ids and titles (if they exist) on clie { name: featureName, }, - userName, - TEST_USER_ID, + TEST_AUDIT_USER, ); await service.createStrategy( config, { projectId, featureName, environment: DEFAULT_ENV }, - userName, + TEST_AUDIT_USER, ); const playgroundFeatures = await service.getPlaygroundFeatures(); @@ -716,8 +696,7 @@ test('Should return last seen at per environment', async () => { { name: featureName, }, - userName, - TEST_USER_ID, + TEST_AUDIT_USER, ); 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 72254d4f5f1..d247abc44d5 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 @@ -7,7 +7,11 @@ import { } from '../../../../test/e2e/helpers/test-helper'; import getLogger from '../../../../test/fixtures/no-logger'; import { DEFAULT_ENV } from '../../../util'; -import { RoleName, CREATE_FEATURE_STRATEGY } from '../../../types'; +import { + CREATE_FEATURE_STRATEGY, + RoleName, + TEST_AUDIT_USER, +} from '../../../types'; let app: IUnleashTest; let db: ITestDb; @@ -47,10 +51,13 @@ test('Should not be possible to update feature toggle without permission', async createdByUserId: 9999, }); - await app.services.userService.createUser({ - email, - rootRole: RoleName.VIEWER, - }); + await app.services.userService.createUser( + { + email, + rootRole: RoleName.VIEWER, + }, + TEST_AUDIT_USER, + ); await app.request.post('/auth/demo/login').send({ email, @@ -72,10 +79,13 @@ test('Should be possible to update feature toggle with permission', async () => createdByUserId: 9999, }); - await app.services.userService.createUser({ - email, - rootRole: RoleName.EDITOR, - }); + await app.services.userService.createUser( + { + email, + rootRole: RoleName.EDITOR, + }, + TEST_AUDIT_USER, + ); await app.request.post('/auth/demo/login').send({ email, @@ -95,15 +105,17 @@ test('Should not be possible auto-enable feature toggle without CREATE_FEATURE_S await app.services.featureToggleServiceV2.createFeatureToggle( 'default', { name }, - 'me', - -9999, + TEST_AUDIT_USER, true, ); - await app.services.userService.createUser({ - email, - rootRole: RoleName.EDITOR, - }); + await app.services.userService.createUser( + { + email, + rootRole: RoleName.EDITOR, + }, + TEST_AUDIT_USER, + ); await app.request.post('/auth/demo/login').send({ email, diff --git a/src/lib/features/feature-toggle/tests/feature-toggles.e2e.test.ts b/src/lib/features/feature-toggle/tests/feature-toggles.e2e.test.ts index baa2f6eded5..d473c6d908e 100644 --- a/src/lib/features/feature-toggle/tests/feature-toggles.e2e.test.ts +++ b/src/lib/features/feature-toggle/tests/feature-toggles.e2e.test.ts @@ -27,7 +27,7 @@ import { import { v4 as uuidv4 } from 'uuid'; import type supertest from 'supertest'; import { randomId } from '../../../util/random-id'; -import { DEFAULT_PROJECT } from '../../../types'; +import { DEFAULT_PROJECT, TEST_AUDIT_USER } from '../../../types'; import type { FeatureStrategySchema, SetStrategySortOrderSchema, @@ -55,7 +55,7 @@ const createSegment = async (segmentName: string) => { }, ], }, - { username: 'testuser', email: 'test@test.com' }, + TEST_AUDIT_USER, ); return segment; @@ -268,6 +268,7 @@ test('should not allow to change project with dependencies', async () => { // @ts-ignore user, 'default', + TEST_AUDIT_USER, ), ).rejects.toThrow( new ForbiddenError( @@ -727,11 +728,14 @@ describe('Interacting with features using project IDs that belong to other proje const nonExistingProject = 'this-is-not-a-project'; beforeAll(async () => { - const dummyAdmin = await app.services.userService.createUser({ - name: 'Some Name', - email: 'test@getunleash.io', - rootRole: RoleName.ADMIN, - }); + const dummyAdmin = await app.services.userService.createUser( + { + name: 'Some Name', + email: 'test@getunleash.io', + rootRole: RoleName.ADMIN, + }, + TEST_AUDIT_USER, + ); await app.services.projectService.createProject( { name: otherProject, @@ -740,6 +744,7 @@ describe('Interacting with features using project IDs that belong to other proje defaultStickiness: 'clientId', }, dummyAdmin, + TEST_AUDIT_USER, ); // ensure the new project has been created @@ -2307,6 +2312,7 @@ test('Should not allow changing project to target project without the same enabl //@ts-ignore user, 'default', + TEST_AUDIT_USER, ), ).rejects.toThrow(new IncompatibleProjectError(targetProject)); }); @@ -2387,6 +2393,7 @@ test('Should allow changing project to target project with the same enabled envi //@ts-ignore user, 'default', + TEST_AUDIT_USER, ), ).resolves; }); diff --git a/src/lib/features/frontend-api/frontend-api-service.e2e.test.ts b/src/lib/features/frontend-api/frontend-api-service.e2e.test.ts index c99d3066581..01817fd820f 100644 --- a/src/lib/features/frontend-api/frontend-api-service.e2e.test.ts +++ b/src/lib/features/frontend-api/frontend-api-service.e2e.test.ts @@ -1,4 +1,9 @@ -import type { IApiUser, IUnleashConfig, IUnleashStores } from '../../types'; +import { + type IApiUser, + type IUnleashConfig, + type IUnleashStores, + TEST_AUDIT_USER, +} from '../../types'; import dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init'; import type { FrontendApiService } from './frontend-api-service'; import { createFrontendApiService } from './createFrontendApiService'; @@ -74,8 +79,7 @@ const createFeature = async (project: string, featureName: string) => { await featureToggleService.createFeatureToggle( project, { name: featureName, description: '' }, - 'user@example.com', - 1, + TEST_AUDIT_USER, ); }; @@ -89,7 +93,7 @@ const enableFeature = async ( featureName, environment, true, - 'user@example.com', + TEST_AUDIT_USER, ); }; diff --git a/src/lib/features/frontend-api/frontend-api-service.ts b/src/lib/features/frontend-api/frontend-api-service.ts index ceace4dfdcb..e3532d6b0b4 100644 --- a/src/lib/features/frontend-api/frontend-api-service.ts +++ b/src/lib/features/frontend-api/frontend-api-service.ts @@ -1,5 +1,6 @@ import crypto from 'crypto'; import type { + IAuditUser, IUnleashConfig, IUnleashServices, IUnleashStores, @@ -266,8 +267,7 @@ export class FrontendApiService { async setFrontendSettings( value: FrontendSettings, - createdBy: string, - createdByUserId: number, + auditUser: IAuditUser, ): Promise { const error = validateOrigins(value.frontendApiOrigins); if (error) { @@ -276,8 +276,7 @@ export class FrontendApiService { await this.services.settingService.insert( frontendSettingsKey, value, - createdBy, - createdByUserId, + auditUser, false, ); } diff --git a/src/lib/features/frontend-api/frontend-api.e2e.test.ts b/src/lib/features/frontend-api/frontend-api.e2e.test.ts index ecf124262a1..4383e5b85d6 100644 --- a/src/lib/features/frontend-api/frontend-api.e2e.test.ts +++ b/src/lib/features/frontend-api/frontend-api.e2e.test.ts @@ -15,7 +15,8 @@ import { FEATURE_UPDATED, type IConstraint, type IStrategyConfig, - SYSTEM_USER, + SYSTEM_USER_AUDIT, + TEST_AUDIT_USER, } from '../../types'; import { ProxyRepository } from './index'; import type { Logger } from '../../logger'; @@ -81,8 +82,7 @@ const createFeatureToggle = async ({ await app.services.featureToggleService.createFeatureToggle( project, { name }, - 'userName', - TEST_USER_ID, + TEST_AUDIT_USER, true, ); const createdStrategies = await Promise.all( @@ -90,7 +90,7 @@ const createFeatureToggle = async ({ app.services.featureToggleService.createStrategy( s, { projectId: project, featureName: name, environment }, - 'userName', + TEST_AUDIT_USER, ), ), ); @@ -99,7 +99,7 @@ const createFeatureToggle = async ({ name, environment, enabled, - 'userName', + TEST_AUDIT_USER, ); return [createdFeature, createdStrategies] as const; }; @@ -112,6 +112,7 @@ const createProject = async (id: string, name: string): Promise => { await app.services.projectService.createProject( { id, name, mode: 'open', defaultStickiness: 'default' }, user, + TEST_AUDIT_USER, ); }; @@ -728,14 +729,12 @@ test('should filter features by environment', async () => { await app.services.environmentService.addEnvironmentToProject( environmentA, 'default', - SYSTEM_USER.username, - SYSTEM_USER.id, + SYSTEM_USER_AUDIT, ); await app.services.environmentService.addEnvironmentToProject( environmentB, 'default', - SYSTEM_USER.username, - SYSTEM_USER.id, + SYSTEM_USER_AUDIT, ); const frontendTokenEnvironmentDefault = await createApiToken( ApiTokenType.FRONTEND, @@ -860,11 +859,11 @@ test('should filter features by segment', async () => { }; const segmentA = await app.services.segmentService.create( { name: randomId(), constraints: [constraintA] }, - { email: 'test@example.com' }, + TEST_AUDIT_USER, ); const segmentB = await app.services.segmentService.create( { name: randomId(), constraints: [constraintB] }, - { email: 'test@example.com' }, + TEST_AUDIT_USER, ); await app.services.segmentService.addToStrategy(segmentA.id, strategyA.id); await app.services.segmentService.addToStrategy(segmentB.id, strategyB.id); diff --git a/src/lib/features/maintenance/maintenance-controller.ts b/src/lib/features/maintenance/maintenance-controller.ts index 25646e1ee01..bba95b8d592 100644 --- a/src/lib/features/maintenance/maintenance-controller.ts +++ b/src/lib/features/maintenance/maintenance-controller.ts @@ -10,7 +10,6 @@ import { } from '../../openapi'; import type { OpenApiService } from '../../services'; import type { IAuthRequest } from '../../routes/unleash-types'; -import { extractUsername } from '../../util'; import { type MaintenanceSchema, maintenanceSchema, @@ -83,8 +82,7 @@ export default class MaintenanceController extends Controller { ): Promise { await this.maintenanceService.toggleMaintenanceMode( req.body, - extractUsername(req), - req.user.id, + req.audit, ); 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 0f6cf2cda47..093af5ebdee 100644 --- a/src/lib/features/maintenance/maintenance-service.test.ts +++ b/src/lib/features/maintenance/maintenance-service.test.ts @@ -4,6 +4,7 @@ import SettingService from '../../services/setting-service'; import { createTestConfig } from '../../../test/config/test-config'; import FakeSettingStore from '../../../test/fixtures/fake-setting-store'; import type EventService from '../events/event-service'; +import { TEST_AUDIT_USER } from '../../types'; test('Scheduler should run scheduled functions if maintenance mode is off', async () => { const config = createTestConfig(); @@ -41,8 +42,7 @@ test('Scheduler should not run scheduled functions if maintenance mode is on', a await maintenanceService.toggleMaintenanceMode( { enabled: true }, - 'irrelevant user', - -9999, + TEST_AUDIT_USER, ); const job = jest.fn(); diff --git a/src/lib/features/maintenance/maintenance-service.ts b/src/lib/features/maintenance/maintenance-service.ts index a4caeddbccc..7339f86f6e7 100644 --- a/src/lib/features/maintenance/maintenance-service.ts +++ b/src/lib/features/maintenance/maintenance-service.ts @@ -1,5 +1,5 @@ import memoizee from 'memoizee'; -import type { IUnleashConfig } from '../../types'; +import type { IAuditUser, IUnleashConfig } from '../../types'; import type { Logger } from '../../logger'; import type SettingService from '../../services/setting-service'; import { maintenanceSettingsKey } from '../../types/settings/maintenance-settings'; @@ -53,16 +53,14 @@ export default class MaintenanceService implements IMaintenanceStatus { async toggleMaintenanceMode( setting: MaintenanceSchema, - user: string, - toggledByUserId: number, + auditUser: IAuditUser, ): Promise { //@ts-ignore this.resolveMaintenance.clear(); return this.settingService.insert( maintenanceSettingsKey, setting, - user, - toggledByUserId, + auditUser, false, ); } diff --git a/src/lib/features/metrics/last-seen/tests/last-seen-service.e2e.test.ts b/src/lib/features/metrics/last-seen/tests/last-seen-service.e2e.test.ts index 5a786eb09d4..c96da37f54e 100644 --- a/src/lib/features/metrics/last-seen/tests/last-seen-service.e2e.test.ts +++ b/src/lib/features/metrics/last-seen/tests/last-seen-service.e2e.test.ts @@ -6,6 +6,7 @@ import { setupAppWithCustomConfig, } from '../../../../../test/e2e/helpers/test-helper'; import getLogger from '../../../../../test/fixtures/no-logger'; +import { TEST_AUDIT_USER } from '../../../../types'; let app: IUnleashTest; let db: ITestDb; @@ -45,8 +46,7 @@ test('should clean unknown feature toggle names from last seen store', async () featureToggleService.createFeatureToggle( 'default', { name: featureName }, - 'user', - -9999, + TEST_AUDIT_USER, ), ), ); @@ -101,8 +101,7 @@ test('should clean unknown feature toggle environments from last seen store', as featureToggleService.createFeatureToggle( 'default', { name: feature.name }, - 'user', - -9999, + TEST_AUDIT_USER, ), ), ); diff --git a/src/lib/features/project-environments/environment-service.test.ts b/src/lib/features/project-environments/environment-service.test.ts index 173dbf55b13..ffa92b9e745 100644 --- a/src/lib/features/project-environments/environment-service.test.ts +++ b/src/lib/features/project-environments/environment-service.test.ts @@ -2,7 +2,11 @@ import EnvironmentService from './environment-service'; import { createTestConfig } from '../../../test/config/test-config'; import dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init'; import NotFoundError from '../../error/notfound-error'; -import { type IUnleashStores, SYSTEM_USER } from '../../types'; +import { + type IUnleashStores, + SYSTEM_USER, + SYSTEM_USER_AUDIT, +} from '../../types'; import NameExistsError from '../../error/name-exists-error'; import { EventService } from '../../services'; @@ -57,8 +61,7 @@ test('Can connect environment to project', async () => { await service.addEnvironmentToProject( 'test-connection', 'default', - SYSTEM_USER.username, - SYSTEM_USER.id, + SYSTEM_USER_AUDIT, ); const overview = await stores.featureStrategiesStore.getFeatureOverview({ projectId: 'default', @@ -99,14 +102,12 @@ test('Can remove environment from project', async () => { await service.removeEnvironmentFromProject( 'test-connection', 'default', - SYSTEM_USER.username, - SYSTEM_USER.id, + SYSTEM_USER_AUDIT, ); await service.addEnvironmentToProject( 'removal-test', 'default', - SYSTEM_USER.username, - SYSTEM_USER.id, + SYSTEM_USER_AUDIT, ); let overview = await stores.featureStrategiesStore.getFeatureOverview({ projectId: 'default', @@ -129,8 +130,7 @@ test('Can remove environment from project', async () => { await service.removeEnvironmentFromProject( 'removal-test', 'default', - SYSTEM_USER.username, - SYSTEM_USER.id, + SYSTEM_USER_AUDIT, ); overview = await stores.featureStrategiesStore.getFeatureOverview({ projectId: 'default', @@ -157,29 +157,25 @@ test('Adding same environment twice should throw a NameExistsError', async () => await service.addEnvironmentToProject( 'uniqueness-test', 'default', - SYSTEM_USER.username, - SYSTEM_USER.id, + SYSTEM_USER_AUDIT, ); await service.removeEnvironmentFromProject( 'test-connection', 'default', - SYSTEM_USER.username, - SYSTEM_USER.id, + SYSTEM_USER_AUDIT, ); await service.removeEnvironmentFromProject( 'removal-test', 'default', - SYSTEM_USER.username, - SYSTEM_USER.id, + SYSTEM_USER_AUDIT, ); return expect(async () => service.addEnvironmentToProject( 'uniqueness-test', 'default', - SYSTEM_USER.username, - SYSTEM_USER.id, + SYSTEM_USER_AUDIT, ), ).rejects.toThrow( new NameExistsError( @@ -193,8 +189,7 @@ 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, + SYSTEM_USER_AUDIT, ), ).resolves); @@ -293,8 +288,7 @@ test('When given overrides should remap projects to override environments', asyn await service.addEnvironmentToProject( disabledEnvName, 'default', - SYSTEM_USER.username, - SYSTEM_USER.id, + SYSTEM_USER_AUDIT, ); 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 77bd88a7fde..58b47e38c85 100644 --- a/src/lib/features/project-environments/environment-service.ts +++ b/src/lib/features/project-environments/environment-service.ts @@ -1,5 +1,6 @@ import { - DEFAULT_STRATEGY_UPDATED, + DefaultStrategyUpdatedEvent, + type IAuditUser, type IEnvironment, type IEnvironmentStore, type IFeatureEnvironmentStore, @@ -8,9 +9,9 @@ import { type ISortOrder, type IUnleashConfig, type IUnleashStores, - PROJECT_ENVIRONMENT_ADDED, - PROJECT_ENVIRONMENT_REMOVED, - SYSTEM_USER, + ProjectEnvironmentAdded, + ProjectEnvironmentRemoved, + SYSTEM_USER_AUDIT, } from '../../types'; import type { Logger } from '../../logger'; import { BadDataError, UNIQUE_CONSTRAINT_VIOLATION } from '../../error'; @@ -101,8 +102,7 @@ export default class EnvironmentService { async addEnvironmentToProject( environment: string, projectId: string, - username: string, - userId: number, + auditUser: IAuditUser, ): Promise { try { await this.featureEnvironmentStore.connectProject( @@ -113,13 +113,13 @@ export default class EnvironmentService { environment, projectId, ); - await this.eventService.storeEvent({ - type: PROJECT_ENVIRONMENT_ADDED, - project: projectId, - environment, - createdBy: username, - createdByUserId: userId, - }); + await this.eventService.storeEvent( + new ProjectEnvironmentAdded({ + project: projectId, + environment, + auditUser, + }), + ); } catch (e) { if (e.code === UNIQUE_CONSTRAINT_VIOLATION) { throw new NameExistsError( @@ -134,8 +134,7 @@ export default class EnvironmentService { environment: string, projectId: string, strategy: CreateFeatureStrategySchema, - username: string, - userId: number, + auditUser: IAuditUser, ): Promise { if (strategy.name !== 'flexibleRollout') { throw new BadDataError( @@ -149,15 +148,15 @@ export default class EnvironmentService { environment, strategy, ); - await this.eventService.storeEvent({ - type: DEFAULT_STRATEGY_UPDATED, - project: projectId, - environment, - createdBy: username, - preData: previousDefaultStrategy, - data: defaultStrategy, - createdByUserId: userId, - }); + await this.eventService.storeEvent( + new DefaultStrategyUpdatedEvent({ + project: projectId, + environment, + preData: previousDefaultStrategy, + data: defaultStrategy, + auditUser, + }), + ); return defaultStrategy; } @@ -225,8 +224,7 @@ export default class EnvironmentService { return this.addEnvironmentToProject( enabledEnv.name, project, - SYSTEM_USER.username, - SYSTEM_USER.id, + SYSTEM_USER_AUDIT, ); }); }); @@ -251,8 +249,7 @@ export default class EnvironmentService { async removeEnvironmentFromProject( environment: string, projectId: string, - username: string, - userId: number, + auditUser: IAuditUser, ): Promise { const projectEnvs = await this.projectStore.getEnvironmentsForProject(projectId); @@ -262,13 +259,13 @@ export default class EnvironmentService { environment, projectId, ); - await this.eventService.storeEvent({ - type: PROJECT_ENVIRONMENT_REMOVED, - project: projectId, - environment, - createdBy: username, - createdByUserId: userId, - }); + await this.eventService.storeEvent( + new ProjectEnvironmentRemoved({ + project: projectId, + environment, + auditUser, + }), + ); return; } throw new MinimumOneEnvironmentError( diff --git a/src/lib/features/project-environments/environments.ts b/src/lib/features/project-environments/environments.ts index 8f55c9f1925..a80010edce4 100644 --- a/src/lib/features/project-environments/environments.ts +++ b/src/lib/features/project-environments/environments.ts @@ -4,7 +4,6 @@ import { type IUnleashConfig, type IUnleashServices, serializeDates, - SYSTEM_USER_ID, UPDATE_PROJECT, } from '../../types'; import type { Logger } from '../../logger'; @@ -19,7 +18,6 @@ import { type ProjectEnvironmentSchema, } from '../../openapi'; import type { OpenApiService, ProjectService } from '../../services'; -import { extractUsername } from '../../util'; import type { IAuthRequest } from '../../routes/unleash-types'; import type { WithTransactional } from '../../db/transaction'; @@ -142,12 +140,7 @@ export default class EnvironmentsController extends Controller { await this.projectService.getProject(projectId); // Validates that the project exists await this.environmentService.transactional((service) => - service.addEnvironmentToProject( - environment, - projectId, - extractUsername(req), - req.user.id, - ), + service.addEnvironmentToProject(environment, projectId, req.audit), ); res.status(200).end(); @@ -163,8 +156,7 @@ export default class EnvironmentsController extends Controller { service.removeEnvironmentFromProject( environment, projectId, - extractUsername(req), - req.user.id, + req.audit, ), ); @@ -186,8 +178,7 @@ export default class EnvironmentsController extends Controller { environment, projectId, strategy, - extractUsername(req), - req.user.id || SYSTEM_USER_ID, + req.audit, ), ); diff --git a/src/lib/features/project-insights/project-insights-service.e2e.test.ts b/src/lib/features/project-insights/project-insights-service.e2e.test.ts index 86250e98815..b08a3b43903 100644 --- a/src/lib/features/project-insights/project-insights-service.e2e.test.ts +++ b/src/lib/features/project-insights/project-insights-service.e2e.test.ts @@ -13,9 +13,14 @@ import { createFeatureToggleService, createProjectService, } from '../../../lib/features'; -import type { IUnleashStores, IUser } from '../../../lib/types'; +import { + type IUnleashStores, + type IUser, + TEST_AUDIT_USER, +} from '../../../lib/types'; import type { User } from '../../../lib/server-impl'; import { createProjectInsightsService } from './createProjectInsightsService'; +import { extractAuditInfoFromUser } from '../../util'; let stores: IUnleashStores; let db: ITestDb; @@ -77,7 +82,11 @@ test('should return average time to production per toggle', async () => { defaultStickiness: 'clientId', }; - await projectService.createProject(project, user); + await projectService.createProject( + project, + user, + extractAuditInfoFromUser(user), + ); const toggles = [ { name: 'average-prod-time-pt', subdays: 7 }, @@ -92,8 +101,7 @@ test('should return average time to production per toggle', async () => { return featureToggleService.createFeatureToggle( project.id, toggle, - user.email, - opsUser.id, + extractAuditInfoFromUser(opsUser), ); }), ); @@ -106,8 +114,7 @@ test('should return average time to production per toggle', async () => { project: project.id, featureName: toggle.name, environment: 'default', - createdBy: 'Fredrik', - createdByUserId: opsUser.id, + auditUser: TEST_AUDIT_USER, }), ); }), @@ -143,8 +150,16 @@ test('should return average time to production per toggle for a specific project defaultStickiness: 'clientId', }; - await projectService.createProject(project1, user); - await projectService.createProject(project2, user); + await projectService.createProject( + project1, + user, + extractAuditInfoFromUser(user), + ); + await projectService.createProject( + project2, + user, + extractAuditInfoFromUser(user), + ); const togglesProject1 = [ { name: 'average-prod-time-pt-10', subdays: 7 }, @@ -162,8 +177,7 @@ test('should return average time to production per toggle for a specific project return featureToggleService.createFeatureToggle( project1.id, toggle, - user.email, - opsUser.id, + extractAuditInfoFromUser(opsUser), ); }), ); @@ -173,8 +187,7 @@ test('should return average time to production per toggle for a specific project return featureToggleService.createFeatureToggle( project2.id, toggle, - user.email, - opsUser.id, + extractAuditInfoFromUser(opsUser), ); }), ); @@ -187,8 +200,7 @@ test('should return average time to production per toggle for a specific project project: project1.id, featureName: toggle.name, environment: 'default', - createdBy: 'Fredrik', - createdByUserId: opsUser.id, + auditUser: TEST_AUDIT_USER, }), ); }), @@ -202,8 +214,7 @@ test('should return average time to production per toggle for a specific project project: project2.id, featureName: toggle.name, environment: 'default', - createdBy: 'Fredrik', - createdByUserId: opsUser.id, + auditUser: TEST_AUDIT_USER, }), ); }), @@ -244,7 +255,11 @@ test('should return average time to production per toggle and include archived t defaultStickiness: 'clientId', }; - await projectService.createProject(project1, user); + await projectService.createProject( + project1, + user, + extractAuditInfoFromUser(user), + ); const togglesProject1 = [ { name: 'average-prod-time-pta-10', subdays: 7 }, @@ -257,8 +272,7 @@ test('should return average time to production per toggle and include archived t return featureToggleService.createFeatureToggle( project1.id, toggle, - user.email, - opsUser.id, + extractAuditInfoFromUser(opsUser), ); }), ); @@ -271,8 +285,7 @@ test('should return average time to production per toggle and include archived t project: project1.id, featureName: toggle.name, environment: 'default', - createdBy: 'Fredrik', - createdByUserId: opsUser.id, + auditUser: TEST_AUDIT_USER, }), ); }), @@ -286,7 +299,11 @@ test('should return average time to production per toggle and include archived t ), ); - await featureToggleService.archiveToggle('average-prod-time-pta-12', user); + await featureToggleService.archiveToggle( + 'average-prod-time-pta-12', + user, + extractAuditInfoFromUser(user), + ); const resultProject1 = await projectInsightsService.getDoraMetrics( project1.id, diff --git a/src/lib/features/project/project-service.e2e.test.ts b/src/lib/features/project/project-service.e2e.test.ts index 2eda643f1ef..df9a87cbaa4 100644 --- a/src/lib/features/project/project-service.e2e.test.ts +++ b/src/lib/features/project/project-service.e2e.test.ts @@ -18,14 +18,17 @@ import { createProjectService, } from '../index'; import { + type IAuditUser, type IGroup, type IUnleashStores, type IUser, - SYSTEM_USER, + SYSTEM_USER_AUDIT, SYSTEM_USER_ID, + TEST_AUDIT_USER, } from '../../types'; import type { User } from '../../server-impl'; import { BadDataError, InvalidOperationError } from '../../error'; +import { extractAuditInfoFromUser } from '../../util'; let stores: IUnleashStores; let db: ITestDb; @@ -36,6 +39,8 @@ let eventService: EventService; let environmentService: EnvironmentService; let featureToggleService: FeatureToggleService; let user: User; // many methods in this test use User instead of IUser +let auditUser: IAuditUser; + let opsUser: IUser; let group: IGroup; @@ -57,6 +62,11 @@ beforeAll(async () => { name: 'Some Name', email: 'test@getunleash.io', }); + auditUser = { + id: user.id, + username: user.email, + ip: '127.0.0.1', + }; group = await stores.groupStore.create({ name: 'aTestGroup', description: '', @@ -121,7 +131,7 @@ test('should list all projects', async () => { defaultStickiness: 'default', }; - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); const projects = await projectService.getProjects(); expect(projects).toHaveLength(2); expect(projects.find((p) => p.name === project.name)?.memberCount).toBe(1); @@ -135,7 +145,7 @@ test('should create new project', async () => { defaultStickiness: 'default', }; - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); const ret = await projectService.getProject('test'); expect(project.id).toEqual(ret.id); expect(project.name).toEqual(ret.name); @@ -151,7 +161,7 @@ test('should create new private project', async () => { defaultStickiness: 'default', }; - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); const ret = await projectService.getProject('testPrivate'); expect(project.id).toEqual(ret.id); expect(project.name).toEqual(ret.name); @@ -168,8 +178,8 @@ test('should delete project', async () => { defaultStickiness: 'default', }; - await projectService.createProject(project, user); - await projectService.deleteProject(project.id, user); + await projectService.createProject(project, user, auditUser); + await projectService.deleteProject(project.id, user, auditUser); try { await projectService.getProject(project.id); @@ -186,14 +196,14 @@ test('should not be able to delete project with toggles', async () => { mode: 'open' as const, defaultStickiness: 'clientId', }; - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); await stores.featureToggleStore.create(project.id, { name: 'test-project-delete', createdByUserId: 9999, }); try { - await projectService.deleteProject(project.id, user); + await projectService.deleteProject(project.id, user, auditUser); } catch (err) { expect(err.message).toBe( 'You can not delete a project with active feature toggles', @@ -203,7 +213,7 @@ test('should not be able to delete project with toggles', async () => { test('should not delete "default" project', async () => { try { - await projectService.deleteProject('default', user); + await projectService.deleteProject('default', user, auditUser); } catch (err) { expect(err.message).toBe('You can not delete the default project!'); } @@ -223,8 +233,8 @@ test('should not be able to create existing project', async () => { defaultStickiness: 'default', }; try { - await projectService.createProject(project, user); - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); + await projectService.createProject(project, user, auditUser); } catch (err) { expect(err.message).toBe('A project with this id already exists.'); } @@ -263,8 +273,8 @@ test('should update project', async () => { defaultStickiness: 'userId', }; - await projectService.createProject(project, user); - await projectService.updateProject(updatedProject, user); + await projectService.createProject(project, user, TEST_AUDIT_USER); + await projectService.updateProject(updatedProject, TEST_AUDIT_USER); const readProject = await projectService.getProject(project.id); @@ -291,12 +301,16 @@ test('should update project without existing settings', async () => { defaultStickiness: 'clientId', }; - await projectService.createProject(project, user); + await projectService.createProject(project, user, { + id: user.id, + username: user.email, + ip: '127.0.0.1', + }); await db .rawDatabase('project_settings') .del() .where({ project: project.id }); - await projectService.updateProject(updatedProject, user); + await projectService.updateProject(updatedProject, auditUser); const readProject = await projectService.getProject(project.id); @@ -322,7 +336,7 @@ test('should get list of users with access to project', async () => { mode: 'open' as const, defaultStickiness: 'clientId', }; - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); const { users } = await projectService.getAccessToProject(project.id); const member = await stores.roleStore.getRoleByName(RoleName.MEMBER); @@ -345,7 +359,7 @@ test('should add a member user to the project', async () => { mode: 'open' as const, defaultStickiness: 'clientId', }; - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); const projectMember1 = await stores.userStore.insert({ name: 'Some Member', @@ -362,13 +376,13 @@ test('should add a member user to the project', async () => { project.id, memberRole.id, projectMember1.id, - 'test', + auditUser, ); await projectService.addUser( project.id, memberRole.id, projectMember2.id, - 'test', + auditUser, ); const { users } = await projectService.getAccessToProject(project.id); @@ -403,7 +417,7 @@ describe('Managing Project access', () => { mode: 'open' as const, defaultStickiness: 'clientId', }; - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); const customRole = await stores.roleStore.create({ name: 'my_custom_role_admin_user', roleType: 'custom', @@ -416,14 +430,13 @@ describe('Managing Project access', () => { }); const ownerRole = await stores.roleStore.getRoleByName(RoleName.OWNER); - expect( + await expect( projectService.addAccess( project.id, [customRole.id, ownerRole.id], [], [projectUserAdmin.id], - opsUser.username, - opsUser.id, + auditUser, ), ).resolves.not.toThrow(); }); @@ -435,7 +448,7 @@ describe('Managing Project access', () => { mode: 'open' as const, defaultStickiness: 'clientId', }; - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); const projectAdmin = await stores.userStore.insert({ name: 'Some project admin', email: 'admin@example.com', @@ -456,14 +469,13 @@ describe('Managing Project access', () => { description: 'Used to prove that you can assign a role the project owner does not have', }); - expect( + await expect( projectService.addAccess( project.id, [customRole.id], [], [projectCustomer.id], - projectAdmin.username, - projectAdmin.id, + auditUser, ), ).resolves; }); @@ -475,11 +487,12 @@ describe('Managing Project access', () => { mode: 'open' as const, defaultStickiness: 'clientId', }; - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); const projectUser = await stores.userStore.insert({ name: 'Some project user', email: 'user@example.com', }); + const projectAuditUser = extractAuditInfoFromUser(projectUser); const secondUser = await stores.userStore.insert({ name: 'Some other user', email: 'otheruser@example.com', @@ -496,26 +509,28 @@ describe('Managing Project access', () => { project.id, ); const ownerRole = await stores.roleStore.getRoleByName(RoleName.OWNER); - expect( + await expect( projectService.addAccess( project.id, [customRole.id], [], [secondUser.id], - projectUser.username, - projectUser.id, + projectAuditUser, ), ).resolves.not.toThrow(); - expect( + await expect(async () => projectService.addAccess( project.id, [ownerRole.id], [], [secondUser.id], - projectUser.username, - projectUser.id, + projectAuditUser, + ), + ).rejects.toThrow( + new InvalidOperationError( + 'User tried to grant role they did not have access to', ), - ).rejects.toThrow(); + ); }); test('Users that are members of a group with project role should only be allowed to grant same role to others', async () => { const project = { @@ -525,18 +540,19 @@ describe('Managing Project access', () => { mode: 'open' as const, defaultStickiness: 'clientId', }; - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); const projectUser = await stores.userStore.insert({ name: 'Some project user', email: 'user_with_group_membership@example.com', }); + const projectAuditUser = extractAuditInfoFromUser(projectUser); const group = await stores.groupStore.create({ name: 'custom_group_for_role_access', }); await stores.groupStore.addUsersToGroup( group.id, [{ user: { id: projectUser.id } }], - opsUser.username, + opsUser.username!, ); const secondUser = await stores.userStore.insert({ name: 'Some other user', @@ -551,43 +567,44 @@ describe('Managing Project access', () => { await accessService.addGroupToRole( group.id, customRole.id, - opsUser.username, + opsUser.username!, project.id, ); const ownerRole = await stores.roleStore.getRoleByName(RoleName.OWNER); const otherGroup = await stores.groupStore.create({ name: 'custom_group_to_receive_new_access', }); - expect( + await expect( projectService.addAccess( project.id, [customRole.id], [], [secondUser.id], - projectUser.username, - projectUser.id, + projectAuditUser, ), ).resolves.not.toThrow(); - expect( + await expect( projectService.addAccess( project.id, [customRole.id], [otherGroup.id], [], - projectUser.username, - projectUser.id, + projectAuditUser, ), ).resolves.not.toThrow(); - expect( + await expect( projectService.addAccess( project.id, [ownerRole.id], [], [secondUser.id], - projectUser.username, - projectUser.id, + projectAuditUser, ), - ).rejects.toThrow(); + ).rejects.toThrow( + new InvalidOperationError( + 'User tried to grant role they did not have access to', + ), + ); }); test('Users can assign roles they have to a group', async () => { const project = { @@ -597,7 +614,7 @@ describe('Managing Project access', () => { mode: 'open' as const, defaultStickiness: 'clientId', }; - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); const projectUser = await stores.userStore.insert({ name: 'Some project user', email: 'assign_role_to_group@example.com', @@ -616,14 +633,13 @@ describe('Managing Project access', () => { customRole.id, project.id, ); - expect( + await expect( projectService.addAccess( project.id, [customRole.id], [secondGroup.id], [], - projectUser.username, - projectUser.id, + auditUser, ), ).resolves.not.toThrow( new InvalidOperationError( @@ -639,11 +655,12 @@ describe('Managing Project access', () => { mode: 'open' as const, defaultStickiness: 'clientId', }; - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); const projectUser = await stores.userStore.insert({ name: 'Some project user', email: 'fail_assign_role_to_user@example.com', }); + const projectAuditUser = extractAuditInfoFromUser(projectUser); const secondUser = await stores.userStore.insert({ name: 'Some other user', email: 'otheruser_no_roles@example.com', @@ -654,15 +671,18 @@ describe('Managing Project access', () => { description: 'Used to prove that you can not assign a role you do not have via setRolesForUser', }); - expect( + await expect( projectService.setRolesForUser( project.id, secondUser.id, [customRole.id], - projectUser.username, - projectUser.id, + projectAuditUser, ), - ).rejects.toThrow(); + ).rejects.toThrow( + new InvalidOperationError( + 'User tried to assign a role they did not have access to', + ), + ); }); test('Users can not assign roles they do not have to a group through explicit roles endpoint', async () => { const project = { @@ -672,11 +692,12 @@ describe('Managing Project access', () => { mode: 'open' as const, defaultStickiness: 'clientId', }; - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); const projectUser = await stores.userStore.insert({ name: 'Some project user', email: 'fail_assign_role_to_group@example.com', }); + const projectAuditUser = extractAuditInfoFromUser(projectUser); const group = await stores.groupStore.create({ name: 'Some group_awaiting_role', }); @@ -686,13 +707,12 @@ describe('Managing Project access', () => { description: 'Used to prove that you can not assign a role you do not have via setRolesForGroup', }); - expect( + return expect( projectService.setRolesForGroup( project.id, group.id, [customRole.id], - projectUser.username, - projectUser.id, + projectAuditUser, ), ).rejects.toThrow( new InvalidOperationError( @@ -710,7 +730,7 @@ test('should add admin users to the project', async () => { mode: 'open' as const, defaultStickiness: 'clientId', }; - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); const projectAdmin1 = await stores.userStore.insert({ name: 'Some Member', @@ -727,13 +747,13 @@ test('should add admin users to the project', async () => { project.id, ownerRole.id, projectAdmin1.id, - 'test', + auditUser, ); await projectService.addUser( project.id, ownerRole.id, projectAdmin2.id, - 'test', + auditUser, ); const { users } = await projectService.getAccessToProject(project.id); @@ -758,7 +778,7 @@ test('add user should fail if user already have access', async () => { mode: 'open' as const, defaultStickiness: 'clientId', }; - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); const projectMember1 = await stores.userStore.insert({ name: 'Some Member', @@ -771,7 +791,7 @@ test('add user should fail if user already have access', async () => { project.id, memberRole.id, projectMember1.id, - 'test', + auditUser, ); await expect(async () => @@ -779,7 +799,7 @@ test('add user should fail if user already have access', async () => { project.id, memberRole.id, projectMember1.id, - 'test', + auditUser, ), ).rejects.toThrow( new Error('User already has access to project=add-users-twice'), @@ -794,7 +814,7 @@ test('should remove user from the project', async () => { mode: 'open' as const, defaultStickiness: 'clientId', }; - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); const projectMember1 = await stores.userStore.insert({ name: 'Some Member', @@ -807,14 +827,13 @@ test('should remove user from the project', async () => { project.id, memberRole.id, projectMember1.id, - 'test', + auditUser, ); await projectService.removeUser( project.id, memberRole.id, projectMember1.id, - 'test', - opsUser.id, + auditUser, ); const { users } = await projectService.getAccessToProject(project.id); @@ -834,12 +853,11 @@ test('should not change project if feature toggle project does not match current const toggle = { name: 'test-toggle' }; - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); await featureToggleService.createFeatureToggle( project.id, toggle, - user.email, - opsUser.id, + auditUser, ); try { @@ -848,6 +866,7 @@ test('should not change project if feature toggle project does not match current toggle.name, user, 'wrong-project-id', + auditUser, ); } catch (err) { expect(err.message.toLowerCase().includes('permission')).toBeTruthy(); @@ -866,12 +885,11 @@ test('should return 404 if no project is found with the project id', async () => const toggle = { name: 'test-toggle-2' }; - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); await featureToggleService.createFeatureToggle( project.id, toggle, - user.email, - opsUser.id, + auditUser, ); try { @@ -880,6 +898,7 @@ test('should return 404 if no project is found with the project id', async () => toggle.name, user, project.id, + auditUser, ); } catch (err) { expect(err.message).toBe(`No project found`); @@ -909,13 +928,16 @@ test('should fail if user is not authorized', async () => { email: 'admin-change-project@getunleash.io', }); - await projectService.createProject(project, user); - await projectService.createProject(projectDestination, projectAdmin1); + await projectService.createProject(project, user, auditUser); + await projectService.createProject( + projectDestination, + projectAdmin1, + auditUser, + ); await featureToggleService.createFeatureToggle( project.id, toggle, - user.email, - opsUser.id, + auditUser, ); try { @@ -924,6 +946,7 @@ test('should fail if user is not authorized', async () => { toggle.name, user, project.id, + auditUser, ); } catch (err) { expect(err.message.toLowerCase().includes('permission')).toBeTruthy(); @@ -946,19 +969,19 @@ test('should change project when checks pass', async () => { }; const toggle = { name: randomId() }; - await projectService.createProject(projectA, user); - await projectService.createProject(projectB, user); + await projectService.createProject(projectA, user, auditUser); + await projectService.createProject(projectB, user, auditUser); await featureToggleService.createFeatureToggle( projectA.id, toggle, - user.email, - opsUser.id, + auditUser, ); await projectService.changeProject( projectB.id, toggle.name, user, projectA.id, + auditUser, ); const updatedFeature = await featureToggleService.getFeature({ @@ -981,13 +1004,12 @@ test('changing project should emit event even if user does not have a username s defaultStickiness: 'clientId', }; const toggle = { name: randomId() }; - await projectService.createProject(projectA, user); - await projectService.createProject(projectB, user); + await projectService.createProject(projectA, user, auditUser); + await projectService.createProject(projectB, user, auditUser); await featureToggleService.createFeatureToggle( projectA.id, toggle, - user.email, - opsUser.id, + auditUser, ); const eventsBeforeChange = await stores.eventStore.getEvents(); await projectService.changeProject( @@ -995,6 +1017,7 @@ test('changing project should emit event even if user does not have a username s toggle.name, user, projectA.id, + auditUser, ); const eventsAfterChange = await stores.eventStore.getEvents(); expect(eventsAfterChange.length).toBe(eventsBeforeChange.length + 1); @@ -1016,20 +1039,18 @@ test('should require equal project environments to move features', async () => { const environment = { name: randomId(), type: 'production' }; const toggle = { name: randomId() }; - await projectService.createProject(projectA, user); - await projectService.createProject(projectB, user); + await projectService.createProject(projectA, user, auditUser); + await projectService.createProject(projectB, user, auditUser); await featureToggleService.createFeatureToggle( projectA.id, toggle, - user.email, - opsUser.id, + auditUser, ); await stores.environmentStore.create(environment); await environmentService.addEnvironmentToProject( environment.name, projectB.id, - 'test', - opsUser.id, + auditUser, ); await expect(() => @@ -1038,6 +1059,7 @@ test('should require equal project environments to move features', async () => { toggle.name, user, projectA.id, + auditUser, ), ).rejects.toThrowError(IncompatibleProjectError); }); @@ -1062,7 +1084,7 @@ test('A newly created project only gets connected to enabled environments', asyn enabled: false, }); - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); const connectedEnvs = await db.stores.projectStore.getEnvironmentsForProject(project.id); expect(connectedEnvs).toHaveLength(2); // default, connection_test @@ -1107,7 +1129,7 @@ test('should have environments sorted in order', async () => { sortOrder: 2, }); - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); const connectedEnvs = await db.stores.projectStore.getEnvironmentsForProject(project.id); @@ -1128,32 +1150,35 @@ test('should add a user to the project with a custom role', async () => { mode: 'open' as const, defaultStickiness: 'clientId', }; - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); const projectMember1 = await stores.userStore.insert({ name: 'Custom', email: 'custom@getunleash.io', }); - const customRole = await accessService.createRole({ - name: 'Service Engineer2', - description: '', - permissions: [ - { - id: 2, // CREATE_FEATURE - }, - { - id: 8, // DELETE_FEATURE - }, - ], - createdByUserId: SYSTEM_USER_ID, - }); + const customRole = await accessService.createRole( + { + name: 'Service Engineer2', + description: '', + permissions: [ + { + id: 2, // CREATE_FEATURE + }, + { + id: 8, // DELETE_FEATURE + }, + ], + createdByUserId: SYSTEM_USER_ID, + }, + SYSTEM_USER_AUDIT, + ); await projectService.addUser( project.id, customRole.id, projectMember1.id, - 'test', + auditUser, ); const { users } = await projectService.getAccessToProject(project.id); @@ -1174,7 +1199,7 @@ test('should delete role entries when deleting project', async () => { defaultStickiness: 'clientId', }; - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); const user1 = await stores.userStore.insert({ name: 'Projectuser1', @@ -1186,27 +1211,40 @@ test('should delete role entries when deleting project', async () => { email: 'project2@getunleash.io', }); - const customRole = await accessService.createRole({ - name: 'Service Engineer', - description: '', - permissions: [ - { - id: 2, // CREATE_FEATURE - }, - { - id: 8, // DELETE_FEATURE - }, - ], - createdByUserId: SYSTEM_USER_ID, - }); + const customRole = await accessService.createRole( + { + name: 'Service Engineer', + description: '', + permissions: [ + { + id: 2, // CREATE_FEATURE + }, + { + id: 8, // DELETE_FEATURE + }, + ], + createdByUserId: SYSTEM_USER_ID, + }, + SYSTEM_USER_AUDIT, + ); - await projectService.addUser(project.id, customRole.id, user1.id, 'test'); - await projectService.addUser(project.id, customRole.id, user2.id, 'test'); + await projectService.addUser( + project.id, + customRole.id, + user1.id, + auditUser, + ); + await projectService.addUser( + project.id, + customRole.id, + user2.id, + auditUser, + ); let usersForRole = await accessService.getUsersForRole(customRole.id); expect(usersForRole.length).toBe(2); - await projectService.deleteProject(project.id, user); + await projectService.deleteProject(project.id, user, auditUser); usersForRole = await accessService.getUsersForRole(customRole.id); expect(usersForRole.length).toBe(0); }); @@ -1220,29 +1258,37 @@ test('should change a users role in the project', async () => { defaultStickiness: 'clientId', }; - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); const projectUser = await stores.userStore.insert({ name: 'Projectuser3', email: 'project3@getunleash.io', }); - const customRole = await accessService.createRole({ - name: 'Service Engineer3', - description: '', - permissions: [ - { - id: 2, // CREATE_FEATURE - }, - { - id: 8, // DELETE_FEATURE - }, - ], - createdByUserId: SYSTEM_USER_ID, - }); + const customRole = await accessService.createRole( + { + name: 'Service Engineer3', + description: '', + permissions: [ + { + id: 2, // CREATE_FEATURE + }, + { + id: 8, // DELETE_FEATURE + }, + ], + createdByUserId: SYSTEM_USER_ID, + }, + SYSTEM_USER_AUDIT, + ); const member = await stores.roleStore.getRoleByName(RoleName.MEMBER); - await projectService.addUser(project.id, member.id, projectUser.id, 'test'); + await projectService.addUser( + project.id, + member.id, + projectUser.id, + auditUser, + ); const { users } = await projectService.getAccessToProject(project.id); const memberUser = users.filter((u) => u.roleId === member.id); @@ -1253,14 +1299,13 @@ test('should change a users role in the project', async () => { project.id, member.id, projectUser.id, - 'test', - opsUser.id, + auditUser, ); await projectService.addUser( project.id, customRole.id, projectUser.id, - 'test', + auditUser, ); const { users: updatedUsers } = await projectService.getAccessToProject( @@ -1281,7 +1326,7 @@ test('should update role for user on project', async () => { mode: 'open' as const, defaultStickiness: 'clientId', }; - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); const projectMember1 = await stores.userStore.insert({ name: 'Some Member', @@ -1295,14 +1340,13 @@ test('should update role for user on project', async () => { project.id, memberRole.id, projectMember1.id, - 'test', + auditUser, ); await projectService.changeRole( project.id, ownerRole.id, projectMember1.id, - 'test', - opsUser.id, + auditUser, ); const { users } = await projectService.getAccessToProject(project.id); @@ -1321,7 +1365,7 @@ test('should able to assign role without existing members', async () => { mode: 'open' as const, defaultStickiness: 'clientId', }; - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); const projectMember1 = await stores.userStore.insert({ name: 'Some Member', @@ -1340,14 +1384,13 @@ test('should able to assign role without existing members', async () => { project.id, memberRole.id, projectMember1.id, - 'test', + auditUser, ); await projectService.changeRole( project.id, testRole.id, projectMember1.id, - 'test', - opsUser.id, + auditUser, ); const { users } = await projectService.getAccessToProject(project.id); @@ -1367,7 +1410,7 @@ describe('ensure project has at least one owner', () => { mode: 'open' as const, defaultStickiness: 'clientId', }; - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); const roles = await stores.roleStore.getRolesForProject(project.id); const ownerRole = roles.find((r) => r.name === RoleName.OWNER)!; @@ -1377,8 +1420,7 @@ describe('ensure project has at least one owner', () => { project.id, ownerRole.id, user.id, - 'test', - opsUser.id, + auditUser, ); }).rejects.toThrowError( new Error('A project must have at least one owner'), @@ -1388,8 +1430,7 @@ describe('ensure project has at least one owner', () => { await projectService.removeUserAccess( project.id, user.id, - 'test', - opsUser.id, + auditUser, ); }).rejects.toThrowError( new Error('A project must have at least one owner'), @@ -1404,7 +1445,7 @@ describe('ensure project has at least one owner', () => { mode: 'open' as const, defaultStickiness: 'clientId', }; - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); const memberRole = await stores.roleStore.getRoleByName( RoleName.MEMBER, @@ -1420,16 +1461,14 @@ describe('ensure project has at least one owner', () => { [memberRole.id], [], [memberUser.id], - 'test', - opsUser.id, + auditUser, ); const usersBefore = await projectService.getProjectUsers(project.id); await projectService.removeUserAccess( project.id, memberUser.id, - 'test', - opsUser.id, + auditUser, ); const usersAfter = await projectService.getProjectUsers(project.id); expect(usersBefore).toHaveLength(2); @@ -1444,7 +1483,7 @@ describe('ensure project has at least one owner', () => { mode: 'open' as const, defaultStickiness: 'clientId', }; - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); const projectMember1 = await stores.userStore.insert({ name: 'Some Member', @@ -1459,7 +1498,7 @@ describe('ensure project has at least one owner', () => { project.id, memberRole.id, projectMember1.id, - 'test', + auditUser, ); await expect(async () => { @@ -1467,8 +1506,7 @@ describe('ensure project has at least one owner', () => { project.id, memberRole.id, user.id, - 'test', - opsUser.id, + auditUser, ); }).rejects.toThrowError( new Error('A project must have at least one owner'), @@ -1479,8 +1517,7 @@ describe('ensure project has at least one owner', () => { project.id, user.id, [memberRole.id], - 'test', - opsUser.id, + auditUser, ); }).rejects.toThrowError( new Error('A project must have at least one owner'), @@ -1495,7 +1532,7 @@ describe('ensure project has at least one owner', () => { mode: 'open' as const, defaultStickiness: 'clientId', }; - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); const roles = await stores.roleStore.getRolesForProject(project.id); const ownerRole = roles.find((r) => r.name === RoleName.OWNER)!; @@ -1504,8 +1541,7 @@ describe('ensure project has at least one owner', () => { project.id, ownerRole.id, group.id, - 'test', - opsUser.id, + auditUser, ); // this should be fine, leaving the group as the only owner @@ -1514,8 +1550,7 @@ describe('ensure project has at least one owner', () => { project.id, ownerRole.id, user.id, - 'test', - opsUser.id, + auditUser, ); return { @@ -1535,8 +1570,7 @@ describe('ensure project has at least one owner', () => { project.id, ownerRole.id, group.id, - 'test', - opsUser.id, + auditUser, ); }).rejects.toThrowError( new Error('A project must have at least one owner'), @@ -1546,8 +1580,7 @@ describe('ensure project has at least one owner', () => { await projectService.removeGroupAccess( project.id, group.id, - 'test', - opsUser.id, + auditUser, ); }).rejects.toThrowError( new Error('A project must have at least one owner'), @@ -1567,8 +1600,7 @@ describe('ensure project has at least one owner', () => { project.id, memberRole.id, group.id, - 'test', - opsUser.id, + auditUser, ); }).rejects.toThrowError( new Error('A project must have at least one owner'), @@ -1579,8 +1611,7 @@ describe('ensure project has at least one owner', () => { project.id, group.id, [memberRole.id], - 'test', - opsUser.id, + auditUser, ); }).rejects.toThrowError( new Error('A project must have at least one owner'), @@ -1595,7 +1626,7 @@ test('Should allow bulk update of group permissions', async () => { mode: 'open' as const, defaultStickiness: 'clientId', }; - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); const groupStore = stores.groupStore; const user1 = await stores.userStore.insert({ @@ -1608,16 +1639,19 @@ test('Should allow bulk update of group permissions', async () => { description: '', }); - const createFeatureRole = await accessService.createRole({ - name: 'CreateRole', - description: '', - permissions: [ - { - id: 2, // CREATE_FEATURE - }, - ], - createdByUserId: SYSTEM_USER_ID, - }); + const createFeatureRole = await accessService.createRole( + { + name: 'CreateRole', + description: '', + permissions: [ + { + id: 2, // CREATE_FEATURE + }, + ], + createdByUserId: SYSTEM_USER_ID, + }, + SYSTEM_USER_AUDIT, + ); await stores.accessStore.addUserToRole( opsUser.id, createFeatureRole.id, @@ -1629,8 +1663,7 @@ test('Should allow bulk update of group permissions', async () => { [createFeatureRole.id], [group1.id], [user1.id], - 'some-admin-user', - opsUser.id, + auditUser, ); }); @@ -1642,24 +1675,26 @@ test('Should bulk update of only users', async () => { email: 'vv@getunleash.io', }); - const createFeatureRole = await accessService.createRole({ - name: 'CreateRoleForUsers', - description: '', - permissions: [ - { - id: 2, // CREATE_FEATURE - }, - ], - createdByUserId: SYSTEM_USER_ID, - }); - + const createFeatureRole = await accessService.createRole( + { + name: 'CreateRoleForUsers', + description: '', + permissions: [ + { + id: 2, // CREATE_FEATURE + }, + ], + createdByUserId: SYSTEM_USER_ID, + }, + SYSTEM_USER_AUDIT, + ); + const auditUserFromOps = extractAuditInfoFromUser(opsUser); await projectService.addAccess( project, [createFeatureRole.id], [], [user1.id], - 'some-admin-user', - opsUser.id, + auditUserFromOps, ); }); @@ -1672,31 +1707,33 @@ test('Should allow bulk update of only groups', async () => { }; const groupStore = stores.groupStore; - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); const group1 = await groupStore.create({ name: 'ViewersOnly', description: '', }); - const createFeatureRole = await accessService.createRole({ - name: 'CreateRoleForGroups', - description: '', - permissions: [ - { - id: 2, // CREATE_FEATURE - }, - ], - createdByUserId: SYSTEM_USER_ID, - }); + const createFeatureRole = await accessService.createRole( + { + name: 'CreateRoleForGroups', + description: '', + permissions: [ + { + id: 2, // CREATE_FEATURE + }, + ], + createdByUserId: SYSTEM_USER_ID, + }, + SYSTEM_USER_AUDIT, + ); await projectService.addAccess( project.id, [createFeatureRole.id], [group1.id], [], - 'some-admin-user', - opsUser.id, + auditUser, ); }); @@ -1708,7 +1745,7 @@ test('Should allow permutations of roles, groups and users when adding a new acc defaultStickiness: 'clientId', }; - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); const group1 = await stores.groupStore.create({ name: 'permutation-group-1', @@ -1730,35 +1767,40 @@ test('Should allow permutations of roles, groups and users when adding a new acc email: 'pu2@getunleash.io', }); - const role1 = await accessService.createRole({ - name: 'permutation-role-1', - description: '', - permissions: [ - { - id: 2, // CREATE_FEATURE - }, - ], - createdByUserId: SYSTEM_USER_ID, - }); + const role1 = await accessService.createRole( + { + name: 'permutation-role-1', + description: '', + permissions: [ + { + id: 2, // CREATE_FEATURE + }, + ], + createdByUserId: SYSTEM_USER_ID, + }, + SYSTEM_USER_AUDIT, + ); - const role2 = await accessService.createRole({ - name: 'permutation-role-2', - description: '', - permissions: [ - { - id: 7, // UPDATE_FEATURE - }, - ], - createdByUserId: SYSTEM_USER_ID, - }); + const role2 = await accessService.createRole( + { + name: 'permutation-role-2', + description: '', + permissions: [ + { + id: 7, // UPDATE_FEATURE + }, + ], + createdByUserId: SYSTEM_USER_ID, + }, + SYSTEM_USER_AUDIT, + ); await projectService.addAccess( project.id, [role1.id, role2.id], [group1.id, group2.id], [user1.id, user2.id], - 'some-admin-user', - opsUser.id, + auditUser, ); const { users, groups } = await projectService.getAccessToProject( @@ -1783,7 +1825,7 @@ test('should only count active feature toggles for project', async () => { defaultStickiness: 'clientId', }; - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); await stores.featureToggleStore.create(project.id, { name: 'only-active-t1', @@ -1794,7 +1836,7 @@ test('should only count active feature toggles for project', async () => { createdByUserId: 9999, }); - await featureToggleService.archiveToggle('only-active-t2', user); + await featureToggleService.archiveToggle('only-active-t2', user, auditUser); const projects = await projectService.getProjects(); const theProject = projects.find((p) => p.id === project.id); @@ -1810,14 +1852,18 @@ test('should list projects with all features archived', async () => { defaultStickiness: 'clientId', }; - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); await stores.featureToggleStore.create(project.id, { name: 'archived-toggle', createdByUserId: 9999, }); - await featureToggleService.archiveToggle('archived-toggle', user); + await featureToggleService.archiveToggle( + 'archived-toggle', + user, + auditUser, + ); const projects = await projectService.getProjects(); const theProject = projects.find((p) => p.id === project.id); @@ -1846,7 +1892,7 @@ test('should calculate average time to production', async () => { defaultStickiness: 'clientId', }; - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); const toggles = [ { name: 'average-prod-time' }, @@ -1861,8 +1907,7 @@ test('should calculate average time to production', async () => { return featureToggleService.createFeatureToggle( project.id, toggle, - user.email, - opsUser.id, + auditUser, ); }), ); @@ -1875,8 +1920,7 @@ test('should calculate average time to production', async () => { project: project.id, featureName: toggle.name, environment: 'default', - createdBy: 'Fredrik', - createdByUserId: opsUser.id, + auditUser, }), ); }), @@ -1910,12 +1954,11 @@ test('should calculate average time to production ignoring some items', async () project: project.id, featureName, environment: 'default', - createdBy: 'Fredrik', - createdByUserId: opsUser.id, + auditUser, tags: [], }); - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); await stores.environmentStore.create({ name: 'customEnv', type: 'development', @@ -1923,8 +1966,7 @@ test('should calculate average time to production ignoring some items', async () await environmentService.addEnvironmentToProject( 'customEnv', project.id, - SYSTEM_USER.username, - SYSTEM_USER.id, + SYSTEM_USER_AUDIT, ); // actual toggle we take for calculations @@ -1932,8 +1974,7 @@ test('should calculate average time to production ignoring some items', async () await featureToggleService.createFeatureToggle( project.id, toggle, - user.email, - opsUser.id, + auditUser, ); await updateFeature(toggle.name, { created_at: subDays(new Date(), 20), @@ -1952,8 +1993,7 @@ test('should calculate average time to production ignoring some items', async () await featureToggleService.createFeatureToggle( project.id, devToggle, - user.email, - opsUser.id, + auditUser, ); await eventService.storeEvent( new FeatureEnvironmentEvent({ @@ -1967,8 +2007,7 @@ test('should calculate average time to production ignoring some items', async () await featureToggleService.createFeatureToggle( 'default', otherProjectToggle, - user.email, - opsUser.id, + auditUser, ); await eventService.storeEvent( new FeatureEnvironmentEvent(makeEvent(otherProjectToggle.name)), @@ -1979,8 +2018,7 @@ test('should calculate average time to production ignoring some items', async () await featureToggleService.createFeatureToggle( project.id, nonReleaseToggle, - user.email, - opsUser.id, + auditUser, ); await eventService.storeEvent( new FeatureEnvironmentEvent(makeEvent(nonReleaseToggle.name)), @@ -1991,8 +2029,7 @@ test('should calculate average time to production ignoring some items', async () await featureToggleService.createFeatureToggle( project.id, previouslyDeleteToggle, - user.email, - opsUser.id, + auditUser, ); await eventService.storeEvent( new FeatureEnvironmentEvent(makeEvent(previouslyDeleteToggle.name)), @@ -2014,7 +2051,7 @@ test('should get correct amount of features created in current and past window', defaultStickiness: 'clientId', }; - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); const toggles = [ { name: 'features-created' }, @@ -2028,8 +2065,7 @@ test('should get correct amount of features created in current and past window', return featureToggleService.createFeatureToggle( project.id, toggle, - user.email, - opsUser.id, + auditUser, ); }), ); @@ -2052,7 +2088,7 @@ test('should get correct amount of features archived in current and past window' defaultStickiness: 'clientId', }; - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); const toggles = [ { name: 'features-archived' }, @@ -2066,8 +2102,7 @@ test('should get correct amount of features archived in current and past window' return featureToggleService.createFeatureToggle( project.id, toggle, - user.email, - opsUser.id, + auditUser, ); }), ); @@ -2100,7 +2135,7 @@ test('should get correct amount of project members for current and past window', defaultStickiness: 'default', }; - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); const users = [ { name: 'memberOne', email: 'memberOne@getunleash.io' }, @@ -2121,7 +2156,7 @@ test('should get correct amount of project members for current and past window', project.id, memberRole.id, createdUser.id, - 'test', + auditUser, ), ), ); @@ -2140,7 +2175,7 @@ test('should return average time to production per toggle', async () => { defaultStickiness: 'clientId', }; - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); const toggles = [ { name: 'average-prod-time-pt', subdays: 7 }, @@ -2155,8 +2190,7 @@ test('should return average time to production per toggle', async () => { return featureToggleService.createFeatureToggle( project.id, toggle, - user.email, - opsUser.id, + auditUser, ); }), ); @@ -2169,8 +2203,7 @@ test('should return average time to production per toggle', async () => { project: project.id, featureName: toggle.name, environment: 'default', - createdBy: 'Fredrik', - createdByUserId: opsUser.id, + auditUser, }), ); }), @@ -2206,8 +2239,8 @@ test('should return average time to production per toggle for a specific project defaultStickiness: 'clientId', }; - await projectService.createProject(project1, user); - await projectService.createProject(project2, user); + await projectService.createProject(project1, user, auditUser); + await projectService.createProject(project2, user, auditUser); const togglesProject1 = [ { name: 'average-prod-time-pt-10', subdays: 7 }, @@ -2225,8 +2258,7 @@ test('should return average time to production per toggle for a specific project return featureToggleService.createFeatureToggle( project1.id, toggle, - user.email, - opsUser.id, + auditUser, ); }), ); @@ -2236,8 +2268,7 @@ test('should return average time to production per toggle for a specific project return featureToggleService.createFeatureToggle( project2.id, toggle, - user.email, - opsUser.id, + auditUser, ); }), ); @@ -2250,8 +2281,7 @@ test('should return average time to production per toggle for a specific project project: project1.id, featureName: toggle.name, environment: 'default', - createdBy: 'Fredrik', - createdByUserId: opsUser.id, + auditUser, }), ); }), @@ -2265,8 +2295,7 @@ test('should return average time to production per toggle for a specific project project: project2.id, featureName: toggle.name, environment: 'default', - createdBy: 'Fredrik', - createdByUserId: opsUser.id, + auditUser, }), ); }), @@ -2303,7 +2332,7 @@ test('should return average time to production per toggle and include archived t defaultStickiness: 'clientId', }; - await projectService.createProject(project1, user); + await projectService.createProject(project1, user, auditUser); const togglesProject1 = [ { name: 'average-prod-time-pta-10', subdays: 7 }, @@ -2316,8 +2345,7 @@ test('should return average time to production per toggle and include archived t return featureToggleService.createFeatureToggle( project1.id, toggle, - user.email, - opsUser.id, + auditUser, ); }), ); @@ -2330,8 +2358,7 @@ test('should return average time to production per toggle and include archived t project: project1.id, featureName: toggle.name, environment: 'default', - createdBy: 'Fredrik', - createdByUserId: opsUser.id, + auditUser, }), ); }), @@ -2345,7 +2372,11 @@ test('should return average time to production per toggle and include archived t ), ); - await featureToggleService.archiveToggle('average-prod-time-pta-12', user); + await featureToggleService.archiveToggle( + 'average-prod-time-pta-12', + user, + auditUser, + ); const resultProject1 = await projectService.getDoraMetrics(project1.id); @@ -2369,9 +2400,12 @@ describe('feature flag naming patterns', () => { featureNaming, }; - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); - await projectService.updateProjectEnterpriseSettings(project, user); + await projectService.updateProjectEnterpriseSettings( + project, + extractAuditInfoFromUser(user), + ); expect( (await projectService.getProject(project.id)).featureNaming, @@ -2383,7 +2417,7 @@ describe('feature flag naming patterns', () => { ...project, featureNaming: { pattern: newPattern }, }, - user, + extractAuditInfoFromUser(user), ); const { events } = await eventService.getEvents(); expect(events[0]).toMatchObject({ @@ -2409,7 +2443,7 @@ test('deleting a project with archived toggles should result in any remaining ar }; const toggleName = 'archived-and-deleted'; - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); await stores.featureToggleStore.create(project.id, { name: toggleName, @@ -2417,11 +2451,11 @@ test('deleting a project with archived toggles should result in any remaining ar }); await stores.featureToggleStore.archive(toggleName); - await projectService.deleteProject(project.id, user); + await projectService.deleteProject(project.id, user, auditUser); // bring the project back again, previously this would allow those archived toggles to be resurrected // we now expect them to be deleted correctly - await projectService.createProject(project, user); + await projectService.createProject(project, user, auditUser); const toggles = await stores.featureToggleStore.getAll({ project: project.id, @@ -2437,8 +2471,8 @@ test('deleting a project with no archived toggles should not result in an error' name: 'project-with-nothing', }; - await projectService.createProject(project, user); - await projectService.deleteProject(project.id, user); + await projectService.createProject(project, user, auditUser); + await projectService.deleteProject(project.id, user, auditUser); }); test('should get project settings with mode', async () => { @@ -2468,10 +2502,13 @@ test('should get project settings with mode', async () => { const { mode, id, ...rest } = updatedProject; - await projectService.createProject(projectOne, user); - await projectService.createProject(projectTwo, user); - await projectService.updateProject({ id, ...rest }, user); - await projectService.updateProjectEnterpriseSettings({ mode, id }, user); + await projectService.createProject(projectOne, user, auditUser); + await projectService.createProject(projectTwo, user, auditUser); + await projectService.updateProject({ id, ...rest }, auditUser); + await projectService.updateProjectEnterpriseSettings( + { mode, id }, + extractAuditInfoFromUser(user), + ); const projects = await projectService.getProjects(); const foundProjectOne = projects.find( @@ -2532,6 +2569,7 @@ describe('create project with environments', () => { ...(environments ? { environments } : {}), }, user, + auditUser, ); const projectEnvs = ( diff --git a/src/lib/features/project/project-service.test.ts b/src/lib/features/project/project-service.test.ts index 4dc601dadba..6b9fbf5c7f0 100644 --- a/src/lib/features/project/project-service.test.ts +++ b/src/lib/features/project/project-service.test.ts @@ -1,5 +1,5 @@ import { createTestConfig } from '../../../test/config/test-config'; -import { RoleName } from '../../types'; +import { RoleName, TEST_AUDIT_USER } from '../../types'; import { createFakeProjectService } from './createProjectService'; describe('enterprise extension: enable change requests', () => { @@ -10,11 +10,14 @@ describe('enterprise extension: enable change requests', () => { const service = createFakeProjectService(config); // @ts-expect-error: if we don't set this up, the test will fail due to a missing role. - service.accessService.createRole({ - name: RoleName.OWNER, - description: 'Project owner', - createdByUserId: -1, - }); + service.accessService.createRole( + { + name: RoleName.OWNER, + description: 'Project owner', + createdByUserId: -1, + }, + TEST_AUDIT_USER, + ); const projectId = 'fake-project-id'; await service.createProject( @@ -27,6 +30,7 @@ describe('enterprise extension: enable change requests', () => { permissions: [], isAPI: false, }, + TEST_AUDIT_USER, async () => { // @ts-expect-error: we want to verify that the project /has/ // been created when calling the function. diff --git a/src/lib/features/project/project-service.ts b/src/lib/features/project/project-service.ts index 7fa036cf2d7..c6da2ad6cb9 100644 --- a/src/lib/features/project/project-service.ts +++ b/src/lib/features/project/project-service.ts @@ -1,6 +1,6 @@ import { subDays } from 'date-fns'; import { ValidationError } from 'joi'; -import type { IUser } from '../../types/user'; +import type { IAuditUser, IUser } from '../../types/user'; import type { AccessService, AccessWithRoles, @@ -34,21 +34,20 @@ import { type IUnleashConfig, type IUnleashStores, MOVE_FEATURE_TOGGLE, - PROJECT_CREATED, - PROJECT_DELETED, - PROJECT_UPDATED, ProjectAccessAddedEvent, ProjectAccessGroupRolesUpdated, ProjectAccessUserRolesDeleted, ProjectAccessUserRolesUpdated, + ProjectCreatedEvent, + ProjectDeletedEvent, ProjectGroupAddedEvent, ProjectGroupRemovedEvent, ProjectGroupUpdateRoleEvent, + ProjectUpdatedEvent, ProjectUserAddedEvent, ProjectUserRemovedEvent, ProjectUserUpdateRoleEvent, RoleName, - SYSTEM_USER, SYSTEM_USER_ID, } from '../../types'; import type { @@ -283,6 +282,7 @@ export default class ProjectService { async createProject( newProject: CreateProject, user: IUser, + auditUser: IAuditUser, enableChangeRequestsForSpecifiedEnvironments: () => Promise = async () => {}, ): Promise { await this.validateProjectEnvironments(newProject.environments); @@ -313,20 +313,20 @@ export default class ProjectService { await this.accessService.createDefaultProjectRoles(user, data.id); - await this.eventService.storeEvent({ - type: PROJECT_CREATED, - createdBy: getCreatedBy(user), - createdByUserId: user.id, - data, - project: newProject.id, - }); + await this.eventService.storeEvent( + new ProjectCreatedEvent({ + data, + project: newProject.id, + auditUser, + }), + ); return data; } async updateProject( updatedProject: IProjectUpdate, - user: IUser, + auditUser: IAuditUser, ): Promise { const preData = await this.projectStore.get(updatedProject.id); @@ -335,19 +335,19 @@ 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.eventService.storeEvent({ - type: PROJECT_UPDATED, - project: updatedProject.id, - createdBy: getCreatedBy(user), - createdByUserId: user.id, - data: afterData, - preData, - }); + await this.eventService.storeEvent( + new ProjectUpdatedEvent({ + project: updatedProject.id, + data: afterData, + preData, + auditUser, + }), + ); } async updateProjectEnterpriseSettings( updatedProject: IProjectEnterpriseSettingsUpdate, - user: IUser, + auditUser: IAuditUser, ): Promise { const preData = await this.projectStore.get(updatedProject.id); @@ -359,14 +359,14 @@ export default class ProjectService { await this.projectStore.updateProjectEnterpriseSettings(updatedProject); - await this.eventService.storeEvent({ - type: PROJECT_UPDATED, - project: updatedProject.id, - createdBy: getCreatedBy(user), - createdByUserId: user.id, - data: { ...preData, ...updatedProject }, - preData, - }); + await this.eventService.storeEvent( + new ProjectUpdatedEvent({ + project: updatedProject.id, + data: { ...preData, ...updatedProject }, + preData, + auditUser, + }), + ); } async checkProjectsCompatibility( @@ -396,6 +396,7 @@ export default class ProjectService { featureName: string, user: IUser, currentProjectId: string, + auditUser: IAuditUser, ): Promise { const feature = await this.featureToggleStore.get(featureName); @@ -426,8 +427,7 @@ export default class ProjectService { const updatedFeature = await this.featureToggleService.changeProject( featureName, newProjectId, - getCreatedBy(user), - user.id, + auditUser, ); await this.featureToggleService.updateFeatureStrategyProject( featureName, @@ -437,7 +437,11 @@ export default class ProjectService { return updatedFeature; } - async deleteProject(id: string, user: IUser): Promise { + async deleteProject( + id: string, + user: IUser, + auditUser: IAuditUser, + ): Promise { if (id === DEFAULT_PROJECT) { throw new InvalidOperationError( 'You can not delete the default project!', @@ -463,18 +467,17 @@ export default class ProjectService { this.featureToggleService.deleteFeatures( archivedToggles.map((toggle) => toggle.name), id, - user.name, - user.id, + auditUser, ); await this.projectStore.delete(id); - await this.eventService.storeEvent({ - type: PROJECT_DELETED, - createdBy: getCreatedBy(user), - project: id, - createdByUserId: user.id, - }); + await this.eventService.storeEvent( + new ProjectDeletedEvent({ + project: id, + auditUser, + }), + ); await this.accessService.removeDefaultProjectRoles(user, id); } @@ -504,7 +507,7 @@ export default class ProjectService { projectId: string, roleId: number, userId: number, - createdBy: string, + auditUser: IAuditUser, ): Promise { const { roles, users } = await this.accessService.getProjectRoleAccess(projectId); @@ -527,8 +530,7 @@ export default class ProjectService { await this.eventService.storeEvent( new ProjectUserAddedEvent({ project: projectId, - createdBy: createdBy || SYSTEM_USER.username, - createdByUserId: user.id || SYSTEM_USER.id, + auditUser, data: { roleId, userId, @@ -546,8 +548,7 @@ export default class ProjectService { projectId: string, roleId: number, userId: number, - createdBy: string, - createdByUserId: number, + auditUser: IAuditUser, ): Promise { const role = await this.findProjectRole(projectId, roleId); @@ -560,8 +561,7 @@ export default class ProjectService { await this.eventService.storeEvent( new ProjectUserRemovedEvent({ project: projectId, - createdBy, - createdByUserId, + auditUser, preData: { roleId, userId, @@ -575,8 +575,7 @@ export default class ProjectService { async removeUserAccess( projectId: string, userId: number, - createdBy: string, - createdByUserId: number, + auditUser: IAuditUser, ): Promise { const existingRoles = await this.accessService.getProjectRolesForUser( projectId, @@ -596,8 +595,7 @@ export default class ProjectService { await this.eventService.storeEvent( new ProjectAccessUserRolesDeleted({ project: projectId, - createdBy, - createdByUserId, + auditUser, preData: { roles: existingRoles, userId, @@ -609,8 +607,7 @@ export default class ProjectService { async removeGroupAccess( projectId: string, groupId: number, - createdBy: string, - createdByUserId: number, + auditUser: IAuditUser, ): Promise { const existingRoles = await this.accessService.getProjectRolesForGroup( projectId, @@ -630,8 +627,7 @@ export default class ProjectService { await this.eventService.storeEvent( new ProjectAccessUserRolesDeleted({ project: projectId, - createdBy, - createdByUserId, + auditUser, preData: { roles: existingRoles, groupId, @@ -644,8 +640,7 @@ export default class ProjectService { projectId: string, roleId: number, groupId: number, - modifiedBy: string, - modifiedById: number, + auditUser: IAuditUser, ): Promise { const role = await this.accessService.getRole(roleId); const group = await this.groupService.getGroup(groupId); @@ -660,15 +655,14 @@ export default class ProjectService { await this.accessService.addGroupToRole( group.id, role.id, - modifiedBy, + auditUser.username, project.id, ); await this.eventService.storeEvent( new ProjectGroupAddedEvent({ project: project.id, - createdBy: modifiedBy, - createdByUserId: modifiedById, + auditUser, data: { groupId: group.id, projectId: project.id, @@ -685,8 +679,7 @@ export default class ProjectService { projectId: string, roleId: number, groupId: number, - modifiedBy: string, - modifiedById: number, + auditUser: IAuditUser, ): Promise { const group = await this.groupService.getGroup(groupId); const role = await this.accessService.getRole(roleId); @@ -709,8 +702,7 @@ export default class ProjectService { await this.eventService.storeEvent( new ProjectGroupRemovedEvent({ project: projectId, - createdBy: modifiedBy, - createdByUserId: modifiedById, + auditUser, preData: { groupId: group.id, projectId: project.id, @@ -724,22 +716,20 @@ export default class ProjectService { projectId: string, roleId: number, usersAndGroups: IProjectAccessModel, - createdBy: string, - createdByUserId: number, + auditUser: IAuditUser, ): Promise { await this.accessService.addRoleAccessToProject( usersAndGroups.users, usersAndGroups.groups, projectId, roleId, - createdBy, + auditUser, ); await this.eventService.storeEvent( new ProjectAccessAddedEvent({ project: projectId, - createdBy, - createdByUserId, + auditUser, data: { roles: { roleId, @@ -793,25 +783,21 @@ export default class ProjectService { roles: number[], groups: number[], users: number[], - createdBy: string, - createdByUserId: number, + auditUser: IAuditUser, ): Promise { - if ( - await this.isAllowedToAddAccess(createdByUserId, projectId, roles) - ) { + if (await this.isAllowedToAddAccess(auditUser.id, projectId, roles)) { await this.accessService.addAccessToProject( roles, groups, users, projectId, - createdBy, + auditUser.username, ); await this.eventService.storeEvent( new ProjectAccessAddedEvent({ project: projectId, - createdBy, - createdByUserId, + auditUser, data: { roles: roles.map((roleId) => { return { @@ -834,8 +820,7 @@ export default class ProjectService { projectId: string, userId: number, newRoles: number[], - createdByUserName: string, - createdByUserId: number, + auditUser: IAuditUser, ): Promise { const currentRoles = await this.accessService.getProjectRolesForUser( projectId, @@ -851,7 +836,7 @@ export default class ProjectService { await this.validateAtLeastOneOwner(projectId, ownerRole); } const isAllowedToAssignRoles = await this.isAllowedToAddAccess( - createdByUserId, + auditUser.id, projectId, newRoles, ); @@ -864,8 +849,7 @@ export default class ProjectService { await this.eventService.storeEvent( new ProjectAccessUserRolesUpdated({ project: projectId, - createdBy: createdByUserName, - createdByUserId, + auditUser, data: { roles: newRoles, userId, @@ -887,8 +871,7 @@ export default class ProjectService { projectId: string, groupId: number, newRoles: number[], - createdBy: string, - createdByUserId: number, + auditUser: IAuditUser, ): Promise { const currentRoles = await this.accessService.getProjectRolesForGroup( projectId, @@ -904,7 +887,7 @@ export default class ProjectService { await this.validateAtLeastOneOwner(projectId, ownerRole); } const isAllowedToAssignRoles = await this.isAllowedToAddAccess( - createdByUserId, + auditUser.id, projectId, newRoles, ); @@ -913,13 +896,12 @@ export default class ProjectService { projectId, groupId, newRoles, - createdBy, + auditUser.username, ); await this.eventService.storeEvent( new ProjectAccessGroupRolesUpdated({ project: projectId, - createdBy, - createdByUserId, + auditUser, data: { roles: newRoles, groupId, @@ -1030,8 +1012,7 @@ export default class ProjectService { projectId: string, roleId: number, userId: number, - createdBy: string, - createdByUserId: number, + auditUser: IAuditUser, ): Promise { const usersWithRoles = await this.getAccessToProject(projectId); const user = usersWithRoles.users.find((u) => u.id === userId); @@ -1064,8 +1045,7 @@ export default class ProjectService { await this.eventService.storeEvent( new ProjectUserUpdateRoleEvent({ project: projectId, - createdBy, - createdByUserId, + auditUser, preData: { userId, roleId: currentRole.id, @@ -1086,8 +1066,7 @@ export default class ProjectService { projectId: string, roleId: number, userId: number, - createdBy: string, - createdByUserId: number, + auditUser: IAuditUser, ): Promise { const usersWithRoles = await this.getAccessToProject(projectId); const user = usersWithRoles.groups.find((u) => u.id === userId); @@ -1119,8 +1098,7 @@ export default class ProjectService { await this.eventService.storeEvent( new ProjectGroupUpdateRoleEvent({ project: projectId, - createdBy, - createdByUserId, + auditUser, preData: { userId, roleId: currentRole.id, diff --git a/src/lib/features/scheduler/scheduler-service.test.ts b/src/lib/features/scheduler/scheduler-service.test.ts index 5363133b6d8..ab255cdbc09 100644 --- a/src/lib/features/scheduler/scheduler-service.test.ts +++ b/src/lib/features/scheduler/scheduler-service.test.ts @@ -7,6 +7,7 @@ import FakeSettingStore from '../../../test/fixtures/fake-setting-store'; import type EventService from '../events/event-service'; import { SCHEDULER_JOB_TIME } from '../../metric-events'; import EventEmitter from 'events'; +import { TEST_AUDIT_USER } from '../../types'; function ms(timeMs) { return new Promise((resolve) => setTimeout(resolve, timeMs)); @@ -34,8 +35,7 @@ const toggleMaintenanceMode = async ( ) => { await maintenanceService.toggleMaintenanceMode( { enabled }, - 'irrelevant user', - -9999, + TEST_AUDIT_USER, ); }; diff --git a/src/lib/features/segment/client-segment.e2e.test.ts b/src/lib/features/segment/client-segment.e2e.test.ts index 770c4185f8b..091165eeca7 100644 --- a/src/lib/features/segment/client-segment.e2e.test.ts +++ b/src/lib/features/segment/client-segment.e2e.test.ts @@ -23,8 +23,8 @@ import type { FeatureStrategySchema, UpsertSegmentSchema, } from '../../openapi'; -import { DEFAULT_ENV } from '../../util'; -import { DEFAULT_PROJECT } from '../../types'; +import { DEFAULT_ENV, extractAuditInfoFromUser } from '../../util'; +import { DEFAULT_PROJECT, TEST_AUDIT_USER } from '../../types'; let db: ITestDb; let app: IUnleashTest; @@ -50,19 +50,22 @@ const fetchClientFeatures = (): Promise => { }; const createSegment = (postData: UpsertSegmentSchema): Promise => { - return app.services.segmentService.create(postData, { - email: 'test@example.com', - }); + return app.services.segmentService.create(postData, TEST_AUDIT_USER); }; const updateSegment = ( id: number, postData: UpsertSegmentSchema, ): Promise => { - return app.services.segmentService.update(id, postData, { - email: 'test@example.com', - id: 1, - }); + return app.services.segmentService.update( + id, + postData, + { + email: 'test@example.com', + id: 1, + }, + TEST_AUDIT_USER, + ); }; const mockStrategy = (segments: number[] = []) => { @@ -298,7 +301,11 @@ test('should store segment-created and segment-deleted events', async () => { await createSegment({ name: 'S1', constraints }); const [segment1] = await fetchSegments(); - await app.services.segmentService.delete(segment1.id, user); + await app.services.segmentService.delete( + segment1.id, + user, + extractAuditInfoFromUser(user), + ); const events = await db.stores.eventStore.getEvents(); expect(events[0].type).toEqual('segment-deleted'); diff --git a/src/lib/features/segment/segment-controller.ts b/src/lib/features/segment/segment-controller.ts index ec72e8f5f26..e5c1b957351 100644 --- a/src/lib/features/segment/segment-controller.ts +++ b/src/lib/features/segment/segment-controller.ts @@ -9,13 +9,13 @@ import type { } from '../../server-impl'; import { type AdminSegmentSchema, - type UpdateFeatureStrategySegmentsSchema, - type UpsertSegmentSchema, adminSegmentSchema, createRequestSchema, createResponseSchema, resourceCreatedResponseSchema, updateFeatureStrategySchema, + type UpdateFeatureStrategySegmentsSchema, + type UpsertSegmentSchema, } from '../../openapi'; import { emptyResponse, @@ -29,10 +29,10 @@ import { DELETE_SEGMENT, type IFlagResolver, NONE, + serializeDates, UPDATE_FEATURE_STRATEGY, UPDATE_PROJECT_SEGMENT, UPDATE_SEGMENT, - serializeDates, } from '../../types'; import { segmentsSchema, @@ -396,7 +396,7 @@ export class SegmentsController extends Controller { if (segmentIsInUse) { res.status(409).send(); } else { - await this.segmentService.delete(id, req.user); + await this.segmentService.delete(id, req.user, req.audit); res.status(204).send(); } } @@ -412,7 +412,12 @@ export class SegmentsController extends Controller { project: req.body.project, constraints: req.body.constraints, }; - await this.segmentService.update(id, updateRequest, req.user); + await this.segmentService.update( + id, + updateRequest, + req.user, + req.audit, + ); res.status(204).send(); } @@ -436,7 +441,7 @@ export class SegmentsController extends Controller { const createRequest = req.body; const segment = await this.segmentService.create( createRequest, - req.user, + req.audit, ); this.openApiService.respondWithValidation( 201, diff --git a/src/lib/features/segment/segment-service-interface.ts b/src/lib/features/segment/segment-service-interface.ts index 382a5dfd661..8a74b3bdf76 100644 --- a/src/lib/features/segment/segment-service-interface.ts +++ b/src/lib/features/segment/segment-service-interface.ts @@ -1,6 +1,11 @@ import type { ChangeRequestStrategy } from '../change-request-segment-usage-service/change-request-segment-usage-read-model'; import type { UpsertSegmentSchema } from '../../openapi'; -import type { IFeatureStrategy, ISegment, IUser } from '../../types'; +import type { + IAuditUser, + IFeatureStrategy, + ISegment, + IUser, +} from '../../types'; export type StrategiesUsingSegment = { strategies: IFeatureStrategy[]; @@ -35,26 +40,24 @@ export interface ISegmentService { getAll(): Promise; - create( - data: UpsertSegmentSchema, - user: Partial>, - ): Promise; + create(data: UpsertSegmentSchema, auditUser: IAuditUser): Promise; update( id: number, data: UpsertSegmentSchema, user: Partial>, + auditUser: IAuditUser, ): Promise; unprotectedUpdate( id: number, data: UpsertSegmentSchema, - user: Partial>, + auditUser: IAuditUser, ): Promise; - delete(id: number, user: IUser): Promise; + delete(id: number, user: IUser, auditUser: IAuditUser): Promise; - unprotectedDelete(id: number, user: IUser): Promise; + unprotectedDelete(id: number, auditUser: IAuditUser): Promise; removeFromStrategy(id: number, strategyId: string): Promise; diff --git a/src/lib/features/segment/segment-service.ts b/src/lib/features/segment/segment-service.ts index 2e32d9ca996..144c7772967 100644 --- a/src/lib/features/segment/segment-service.ts +++ b/src/lib/features/segment/segment-service.ts @@ -1,20 +1,18 @@ import type { IUnleashConfig } from '../../types/option'; import { + type IAuditUser, type IFlagResolver, type IUnleashStores, + SegmentCreatedEvent, + SegmentDeletedEvent, + SegmentUpdatedEvent, SKIP_CHANGE_REQUEST, - SYSTEM_USER, } from '../../types'; import type { Logger } from '../../logger'; import NameExistsError from '../../error/name-exists-error'; import type { ISegmentStore } from './segment-store-type'; import type { ISegment } from '../../types/model'; import { segmentSchema } from '../../services/segment-schema'; -import { - SEGMENT_CREATED, - SEGMENT_DELETED, - SEGMENT_UPDATED, -} from '../../types/events'; import type User from '../../types/user'; import type { IFeatureStrategiesStore } from '../feature-toggle/types/feature-toggle-strategies-store-type'; import BadDataError from '../../error/bad-data-error'; @@ -125,36 +123,38 @@ export class SegmentService implements ISegmentService { return strategies.length > 0 || changeRequestStrategies.length > 0; } - async create( - data: unknown, - user: Partial>, - ): Promise { + async create(data: unknown, auditUser: IAuditUser): Promise { const input = await segmentSchema.validateAsync(data); this.validateSegmentValuesLimit(input); await this.validateName(input.name); - const segment = await this.segmentStore.create(input, user); + const segment = await this.segmentStore.create(input, auditUser); - await this.eventService.storeEvent({ - type: SEGMENT_CREATED, - createdBy: user.email || user.username || SYSTEM_USER.username, - createdByUserId: user.id || SYSTEM_USER.id, - data: segment, - project: segment.project, - }); + await this.eventService.storeEvent( + new SegmentCreatedEvent({ + data: segment, + project: segment.project || 'no-project', + auditUser, + }), + ); return segment; } - async update(id: number, data: unknown, user: User): Promise { + async update( + id: number, + data: unknown, + user: User, + auditUser: IAuditUser, + ): Promise { const input = await segmentSchema.validateAsync(data); await this.stopWhenChangeRequestsEnabled(input.project, user); - return this.unprotectedUpdate(id, data, user); + return this.unprotectedUpdate(id, data, auditUser); } async unprotectedUpdate( id: number, data: unknown, - user: User, + auditUser: IAuditUser, ): Promise { const input = await segmentSchema.validateAsync(data); this.validateSegmentValuesLimit(input); @@ -168,38 +168,38 @@ export class SegmentService implements ISegmentService { const segment = await this.segmentStore.update(id, input); - await this.eventService.storeEvent({ - type: SEGMENT_UPDATED, - createdBy: user.email || user.username || 'unknown', - createdByUserId: user.id, - data: segment, - preData, - project: segment.project, - }); + await this.eventService.storeEvent( + new SegmentUpdatedEvent({ + data: segment, + preData, + project: segment.project!, + auditUser, + }), + ); } - async delete(id: number, user: User): Promise { + async delete(id: number, user: User, auditUser: IAuditUser): Promise { const segment = await this.segmentStore.get(id); await this.stopWhenChangeRequestsEnabled(segment.project, user); await this.segmentStore.delete(id); - await this.eventService.storeEvent({ - type: SEGMENT_DELETED, - createdBy: user.email || user.username, - createdByUserId: user.id, - preData: segment, - project: segment.project, - }); + await this.eventService.storeEvent( + new SegmentDeletedEvent({ + preData: segment, + project: segment.project, + auditUser, + }), + ); } - async unprotectedDelete(id: number, user: User): Promise { + async unprotectedDelete(id: number, auditUser: IAuditUser): Promise { const segment = await this.segmentStore.get(id); await this.segmentStore.delete(id); - await this.eventService.storeEvent({ - type: SEGMENT_DELETED, - createdBy: user.email || user.username, - createdByUserId: user.id, - preData: segment, - }); + await this.eventService.storeEvent( + new SegmentDeletedEvent({ + preData: segment, + auditUser, + }), + ); } async cloneStrategySegments( diff --git a/src/lib/features/segment/segment-store-type.ts b/src/lib/features/segment/segment-store-type.ts index 88927eac558..eb6f417a595 100644 --- a/src/lib/features/segment/segment-store-type.ts +++ b/src/lib/features/segment/segment-store-type.ts @@ -1,6 +1,6 @@ import type { IFeatureStrategySegment, ISegment } from '../../types/model'; import type { Store } from '../../types/stores/store'; -import type User from '../../types/user'; +import type { IAuditUser } from '../../types/user'; export interface ISegmentStore extends Store { getAll(includeChangeRequestUsageData?: boolean): Promise; @@ -9,7 +9,7 @@ export interface ISegmentStore extends Store { create( segment: Omit, - user: Partial>, + createdBy: Pick, ): Promise; update(id: number, segment: Omit): Promise; diff --git a/src/lib/features/segment/segment-store.test.ts b/src/lib/features/segment/segment-store.test.ts index f96cc5440e7..b5c4e282285 100644 --- a/src/lib/features/segment/segment-store.test.ts +++ b/src/lib/features/segment/segment-store.test.ts @@ -2,7 +2,7 @@ import type { ISegmentStore } from './segment-store-type'; import dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init'; import getLogger from '../../../test/fixtures/no-logger'; import NotFoundError from '../../error/notfound-error'; -import type { IUnleashStores, IUser } from '../../types'; +import { type IUnleashStores, type IUser, TEST_AUDIT_USER } from '../../types'; let stores: IUnleashStores; let db: ITestDb; @@ -67,7 +67,7 @@ describe('usage counting', () => { constraints: [], createdAt: new Date(), }, - user, + TEST_AUDIT_USER, ); await db.rawDatabase.table('change_requests').insert({ @@ -152,7 +152,7 @@ describe('usage counting', () => { constraints: [], createdAt: new Date(), }, - user, + TEST_AUDIT_USER, ); const segment2 = await segmentStore.create( @@ -161,7 +161,7 @@ describe('usage counting', () => { constraints: [], createdAt: new Date(), }, - user, + TEST_AUDIT_USER, ); const strategy = diff --git a/src/lib/features/segment/segment-store.ts b/src/lib/features/segment/segment-store.ts index 98425a0263b..91ff8234f53 100644 --- a/src/lib/features/segment/segment-store.ts +++ b/src/lib/features/segment/segment-store.ts @@ -8,7 +8,7 @@ import type { Logger, LogProvider } from '../../logger'; import type EventEmitter from 'events'; import NotFoundError from '../../error/notfound-error'; import type { PartialSome } from '../../types/partial'; -import type User from '../../types/user'; +import type { IAuditUser } from '../../types/user'; import type { Db } from '../../db/db'; import type { IFlagResolver } from '../../types'; import { isDefined } from '../../util'; @@ -77,7 +77,7 @@ export default class SegmentStore implements ISegmentStore { async create( segment: PartialSome, - user: Partial>, + user: Pick, ): Promise { const rows = await this.db(T.segments) .insert({ @@ -86,7 +86,7 @@ export default class SegmentStore implements ISegmentStore { description: segment.description, segment_project_id: segment.project || null, constraints: JSON.stringify(segment.constraints), - created_by: user.username || user.email, + created_by: user.username, }) .returning(COLUMNS); diff --git a/src/lib/features/tag-type/tag-type-service.ts b/src/lib/features/tag-type/tag-type-service.ts index 727226f2171..6365135d530 100644 --- a/src/lib/features/tag-type/tag-type-service.ts +++ b/src/lib/features/tag-type/tag-type-service.ts @@ -4,16 +4,16 @@ import { tagTypeSchema } from '../../services/tag-type-schema'; import type { IUnleashStores } from '../../types/stores'; import { - TAG_TYPE_CREATED, - TAG_TYPE_DELETED, - TAG_TYPE_UPDATED, + TagTypeCreatedEvent, + TagTypeDeletedEvent, + TagTypeUpdatedEvent, } from '../../types/events'; import type { Logger } from '../../logger'; import type { ITagType, ITagTypeStore } from './tag-type-store-type'; import type { IUnleashConfig } from '../../types/option'; import type EventService from '../events/event-service'; -import { SYSTEM_USER } from '../../types'; +import type { IAuditUser } from '../../types'; export default class TagTypeService { private tagTypeStore: ITagTypeStore; @@ -42,20 +42,19 @@ export default class TagTypeService { async createTagType( newTagType: ITagType, - userName: string, - userId: number, + auditUser: IAuditUser, ): Promise { const data = (await tagTypeSchema.validateAsync( newTagType, )) as ITagType; await this.validateUnique(data.name); await this.tagTypeStore.createTagType(data); - await this.eventService.storeEvent({ - type: TAG_TYPE_CREATED, - createdBy: userName || SYSTEM_USER.username, - createdByUserId: userId, - data, - }); + await this.eventService.storeEvent( + new TagTypeCreatedEvent({ + auditUser, + data, + }), + ); return data; } @@ -76,34 +75,29 @@ export default class TagTypeService { } } - async deleteTagType( - name: string, - userName: string, - userId: number, - ): Promise { + async deleteTagType(name: string, auditUser: IAuditUser): Promise { const tagType = await this.tagTypeStore.get(name); await this.tagTypeStore.delete(name); - await this.eventService.storeEvent({ - type: TAG_TYPE_DELETED, - createdBy: userName || SYSTEM_USER.username, - createdByUserId: userId, - preData: tagType, - }); + await this.eventService.storeEvent( + new TagTypeDeletedEvent({ + preData: tagType, + auditUser, + }), + ); } async updateTagType( updatedTagType: ITagType, - userName: string, - userId: number, + auditUser: IAuditUser, ): Promise { const data = await tagTypeSchema.validateAsync(updatedTagType); await this.tagTypeStore.updateTagType(data); - await this.eventService.storeEvent({ - type: TAG_TYPE_UPDATED, - createdBy: userName || SYSTEM_USER.username, - createdByUserId: userId, - data, - }); + await this.eventService.storeEvent( + new TagTypeUpdatedEvent({ + data, + auditUser, + }), + ); return data; } } diff --git a/src/lib/features/tag-type/tag-type.ts b/src/lib/features/tag-type/tag-type.ts index 38fd1745f1c..13db0296add 100644 --- a/src/lib/features/tag-type/tag-type.ts +++ b/src/lib/features/tag-type/tag-type.ts @@ -7,7 +7,6 @@ import { NONE, UPDATE_TAG_TYPE, } from '../../types/permissions'; -import { extractUsername } from '../../util/extract-user'; import type { IUnleashConfig } from '../../types/option'; import type { IUnleashServices } from '../../types/services'; import type TagTypeService from './tag-type-service'; @@ -201,9 +200,8 @@ class TagTypeController extends Controller { req: IAuthRequest, res: Response, ): Promise { - const userName = extractUsername(req); const tagType = await this.tagTypeService.transactional((service) => - service.createTagType(req.body, userName, req.user.id), + service.createTagType(req.body, req.audit), ); res.status(201) .header('location', `tag-types/${tagType.name}`) @@ -216,14 +214,9 @@ class TagTypeController extends Controller { ): Promise { const { description, icon } = req.body; const { name } = req.params; - const userName = extractUsername(req); await this.tagTypeService.transactional((service) => - service.updateTagType( - { name, description, icon }, - userName, - req.user.id, - ), + service.updateTagType({ name, description, icon }, req.audit), ); res.status(200).end(); } @@ -237,9 +230,8 @@ class TagTypeController extends Controller { async deleteTagType(req: IAuthRequest, res: Response): Promise { const { name } = req.params; - const userName = extractUsername(req); await this.tagTypeService.transactional((service) => - service.deleteTagType(name, userName, req.user.id), + service.deleteTagType(name, req.audit), ); res.status(200).end(); } diff --git a/src/lib/middleware/audit-middleware.ts b/src/lib/middleware/audit-middleware.ts index e4adaecd9fb..e1c8110c1b2 100644 --- a/src/lib/middleware/audit-middleware.ts +++ b/src/lib/middleware/audit-middleware.ts @@ -10,7 +10,11 @@ export const auditAccessMiddleware = ({ if (!req.user) { logger.info('Could not find user'); } else { - req.audit = extractAuditInfo(req); + try { + req.audit = extractAuditInfo(req); + } catch (e) { + logger.warn('Could not find audit info in request'); + } } next(); }; diff --git a/src/lib/middleware/cors-origin-middleware.test.ts b/src/lib/middleware/cors-origin-middleware.test.ts index 49263f8de20..dda47db1e5b 100644 --- a/src/lib/middleware/cors-origin-middleware.test.ts +++ b/src/lib/middleware/cors-origin-middleware.test.ts @@ -9,7 +9,7 @@ import { FrontendApiService, SettingService, } from '../../lib/services'; -import type { ISettingStore } from '../../lib/types'; +import { type ISettingStore, TEST_AUDIT_USER } from '../../lib/types'; import { frontendSettingsKey } from '../../lib/types/settings/frontend-settings'; import FakeFeatureTagStore from '../../test/fixtures/fake-feature-tag-store'; @@ -56,8 +56,7 @@ test('corsOriginMiddleware origin validation', async () => { await expect(() => frontendApiService.setFrontendSettings( { frontendApiOrigins: ['a'] }, - userName, - TEST_USER_ID, + TEST_AUDIT_USER, ), ).rejects.toThrow('Invalid origin: a'); }); @@ -70,16 +69,14 @@ test('corsOriginMiddleware without config', async () => { }); await frontendApiService.setFrontendSettings( { frontendApiOrigins: [] }, - userName, - TEST_USER_ID, + TEST_AUDIT_USER, ); expect(await frontendApiService.getFrontendSettings(false)).toEqual({ frontendApiOrigins: [], }); await frontendApiService.setFrontendSettings( { frontendApiOrigins: ['*'] }, - userName, - TEST_USER_ID, + TEST_AUDIT_USER, ); expect(await frontendApiService.getFrontendSettings(false)).toEqual({ frontendApiOrigins: ['*'], @@ -98,16 +95,14 @@ test('corsOriginMiddleware with config', async () => { }); await frontendApiService.setFrontendSettings( { frontendApiOrigins: [] }, - userName, - TEST_USER_ID, + TEST_AUDIT_USER, ); expect(await frontendApiService.getFrontendSettings(false)).toEqual({ frontendApiOrigins: [], }); await frontendApiService.setFrontendSettings( { frontendApiOrigins: ['https://example.com', 'https://example.org'] }, - userName, - TEST_USER_ID, + TEST_AUDIT_USER, ); expect(await frontendApiService.getFrontendSettings(false)).toEqual({ frontendApiOrigins: ['https://example.com', 'https://example.org'], @@ -129,8 +124,7 @@ test('corsOriginMiddleware with caching enabled', async () => { //setting await frontendApiService.setFrontendSettings( { frontendApiOrigins: ['*'] }, - userName, - TEST_USER_ID, + TEST_AUDIT_USER, ); //still get cached value diff --git a/src/lib/routes/admin-api/addon.ts b/src/lib/routes/admin-api/addon.ts index 7a49a2290c0..8e26ec416e1 100644 --- a/src/lib/routes/admin-api/addon.ts +++ b/src/lib/routes/admin-api/addon.ts @@ -4,7 +4,6 @@ import type { IUnleashConfig, IUnleashServices } from '../../types'; import type { Logger } from '../../logger'; import type AddonService from '../../services/addon-service'; -import { extractUsername } from '../../util/extract-user'; import { CREATE_ADDON, DELETE_ADDON, @@ -181,15 +180,9 @@ Note: passing \`null\` as a value for the description property will set it to an res: Response, ): Promise { const { id } = req.params; - const createdBy = extractUsername(req); const data = req.body; - const addon = await this.addonService.updateAddon( - id, - data, - createdBy, - req.user.id, - ); + const addon = await this.addonService.updateAddon(id, data, req.audit); this.openApiService.respondWithValidation( 200, @@ -203,13 +196,8 @@ Note: passing \`null\` as a value for the description property will set it to an req: IAuthRequest, res: Response, ): Promise { - const createdBy = extractUsername(req); const data = req.body; - const addon = await this.addonService.createAddon( - data, - createdBy, - req.user.id, - ); + const addon = await this.addonService.createAddon(data, req.audit); this.openApiService.respondWithValidation( 201, @@ -224,8 +212,7 @@ Note: passing \`null\` as a value for the description property will set it to an res: Response, ): Promise { const { id } = req.params; - const username = extractUsername(req); - await this.addonService.removeAddon(id, username, req.user.id); + await this.addonService.removeAddon(id, req.audit); 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 c36ca080a9c..18799f753ff 100644 --- a/src/lib/routes/admin-api/api-token.ts +++ b/src/lib/routes/admin-api/api-token.ts @@ -42,7 +42,6 @@ import { getStandardResponses, } from '../../openapi/util/standard-responses'; import type { FrontendApiService } from '../../features/frontend-api/frontend-api-service'; -import { extractUserId, extractUsername } from '../../util'; import { OperationDeniedError } from '../../error'; interface TokenParam { @@ -322,8 +321,7 @@ export class ApiTokenController extends Controller { if (hasPermission) { const token = await this.apiTokenService.createApiToken( createToken, - extractUsername(req), - extractUserId(req), + req.audit, ); this.openApiService.respondWithValidation( 201, @@ -374,8 +372,7 @@ export class ApiTokenController extends Controller { await this.apiTokenService.updateExpiry( token, new Date(expiresAt), - extractUsername(req), - req.user.id, + req.audit, ); return res.status(200).end(); @@ -406,11 +403,7 @@ export class ApiTokenController extends Controller { `You do not have the required access [${permissionRequired}] to perform this operation`, ); } - await this.apiTokenService.delete( - token, - extractUsername(req), - req.user.id, - ); + await this.apiTokenService.delete(token, req.audit); await this.frontendApiService.deleteClientForFrontendApiToken(token); res.status(200).end(); } diff --git a/src/lib/routes/admin-api/config.ts b/src/lib/routes/admin-api/config.ts index 0e40d49f2fb..c7eef8d322a 100644 --- a/src/lib/routes/admin-api/config.ts +++ b/src/lib/routes/admin-api/config.ts @@ -20,7 +20,6 @@ import type { OpenApiService } from '../../services/openapi-service'; import type { EmailService } from '../../services/email-service'; import { emptyResponse } from '../../openapi/util/standard-responses'; import type { IAuthRequest } from '../unleash-types'; -import { extractUsername } from '../../util/extract-user'; import NotFoundError from '../../error/notfound-error'; import type { SetUiConfigSchema } from '../../openapi/spec/set-ui-config-schema'; import { createRequestSchema } from '../../openapi/util/create-request-schema'; @@ -192,8 +191,7 @@ class ConfigController extends Controller { if (req.body.frontendSettings) { await this.frontendApiService.setFrontendSettings( req.body.frontendSettings, - extractUsername(req), - req.user.id, + req.audit, ); res.sendStatus(204); return; diff --git a/src/lib/routes/admin-api/context.ts b/src/lib/routes/admin-api/context.ts index d61f96fec4a..7c87787c66e 100644 --- a/src/lib/routes/admin-api/context.ts +++ b/src/lib/routes/admin-api/context.ts @@ -2,11 +2,6 @@ import type { Request, Response } from 'express'; import Controller from '../controller'; -import { - extractUserIdFromUser, - extractUsername, -} from '../../util/extract-user'; - import { CREATE_CONTEXT_FIELD, UPDATE_CONTEXT_FIELD, @@ -43,6 +38,7 @@ import { } from '../../openapi/spec/context-field-strategies-schema'; import type { UpdateContextFieldSchema } from '../../openapi/spec/update-context-field-schema'; import type { CreateContextFieldSchema } from '../../openapi/spec/create-context-field-schema'; +import { extractUserIdFromUser } from '../../util'; interface ContextParam { contextField: string; @@ -246,12 +242,10 @@ export class ContextController extends Controller { res: Response, ): Promise { const value = req.body; - const userName = extractUsername(req); const result = await this.contextService.createContextField( value, - userName, - req.user.id, + req.audit, ); this.openApiService.respondWithValidation( @@ -268,13 +262,11 @@ export class ContextController extends Controller { res: Response, ): Promise { const name = req.params.contextField; - const userName = extractUsername(req); const contextField = req.body; await this.contextService.updateContextField( { ...contextField, name }, - userName, - req.user.id, + req.audit, ); res.status(200).end(); } @@ -284,13 +276,8 @@ export class ContextController extends Controller { res: Response, ): Promise { const name = req.params.contextField; - const userName = extractUsername(req); - await this.contextService.deleteContextField( - name, - userName, - req.user.id, - ); + await this.contextService.deleteContextField(name, req.audit); 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 2c86514c52f..122cbd05a61 100644 --- a/src/lib/routes/admin-api/events.test.ts +++ b/src/lib/routes/admin-api/events.test.ts @@ -11,6 +11,8 @@ import { ProjectUserAddedEvent, ProjectUserRemovedEvent, } from '../../types/events'; +import { TEST_AUDIT_USER } from '../../types'; + const TEST_USER_ID = -9999; async function getSetup(anonymise: boolean = false) { const base = `/random${Math.round(Math.random() * 1000)}`; @@ -45,11 +47,10 @@ test('should get events list via admin', async () => { const { request, base, eventService } = await getSetup(); eventService.storeEvent( new FeatureCreatedEvent({ - createdBy: 'some@email.com', data: { name: 'test', project: 'default' }, featureName: 'test', project: 'default', - createdByUserId: TEST_USER_ID, + auditUser: TEST_AUDIT_USER, }), ); const { body } = await request @@ -58,18 +59,17 @@ test('should get events list via admin', async () => { .expect(200); expect(body.events.length).toBe(1); - expect(body.events[0].createdBy).toBe('some@email.com'); + expect(body.events[0].createdBy).toBe('test@example.com'); }); test('should anonymise events list via admin', async () => { const { request, base, eventService } = await getSetup(true); eventService.storeEvent( new FeatureCreatedEvent({ - createdBy: 'some@email.com', data: { name: 'test', project: 'default' }, featureName: 'test', project: 'default', - createdByUserId: TEST_USER_ID, + auditUser: TEST_AUDIT_USER, }), ); const { body } = await request @@ -78,7 +78,7 @@ test('should anonymise events list via admin', async () => { .expect(200); expect(body.events.length).toBe(1); - expect(body.events[0].createdBy).toBe('676212ff7@unleash.run'); + expect(body.events[0].createdBy).toBe('973dfe463@unleash.run'); }); test('should also anonymise email fields in data and preData properties', async () => { @@ -88,16 +88,14 @@ test('should also anonymise email fields in data and preData properties', async const { request, base, eventService } = await getSetup(true); eventService.storeEvent( new ProjectUserAddedEvent({ - createdBy: 'some@email.com', - createdByUserId: TEST_USER_ID, + auditUser: TEST_AUDIT_USER, data: { name: 'test', project: 'default', email: email1 }, project: 'default', }), ); eventService.storeEvent( new ProjectUserRemovedEvent({ - createdBy: 'some@email.com', - createdByUserId: TEST_USER_ID, + auditUser: TEST_AUDIT_USER, preData: { name: 'test', project: 'default', email: email2 }, project: 'default', }), @@ -118,8 +116,7 @@ test('should anonymise any PII fields, no matter the depth', async () => { const { request, base, eventService } = await getSetup(true); eventService.storeEvent( new ProjectAccessAddedEvent({ - createdBy: 'some@email.com', - createdByUserId: TEST_USER_ID, + auditUser: TEST_AUDIT_USER, data: { roles: [ { diff --git a/src/lib/routes/admin-api/favorites.ts b/src/lib/routes/admin-api/favorites.ts index cfb0351e206..db25d68dce4 100644 --- a/src/lib/routes/admin-api/favorites.ts +++ b/src/lib/routes/admin-api/favorites.ts @@ -112,10 +112,13 @@ export default class FavoritesController extends Controller { ): Promise { const { featureName } = req.params; const { user } = req; - await this.favoritesService.favoriteFeature({ - feature: featureName, - user, - }); + await this.favoritesService.favoriteFeature( + { + feature: featureName, + user, + }, + req.audit, + ); res.status(200).end(); } @@ -125,10 +128,13 @@ export default class FavoritesController extends Controller { ): Promise { const { featureName } = req.params; const { user } = req; - await this.favoritesService.unfavoriteFeature({ - feature: featureName, - user, - }); + await this.favoritesService.unfavoriteFeature( + { + feature: featureName, + user, + }, + req.audit, + ); res.status(200).end(); } @@ -138,10 +144,13 @@ export default class FavoritesController extends Controller { ): Promise { const { projectId } = req.params; const { user } = req; - await this.favoritesService.favoriteProject({ - project: projectId, - user, - }); + await this.favoritesService.favoriteProject( + { + project: projectId, + user, + }, + req.audit, + ); res.status(200).end(); } @@ -151,10 +160,13 @@ export default class FavoritesController extends Controller { ): Promise { const { projectId } = req.params; const { user } = req; - await this.favoritesService.unfavoriteProject({ - project: projectId, - user: user, - }); + await this.favoritesService.unfavoriteProject( + { + project: projectId, + user: user, + }, + req.audit, + ); res.status(200).end(); } } diff --git a/src/lib/routes/admin-api/feature-type.ts b/src/lib/routes/admin-api/feature-type.ts index 0c020f3c637..424d913fe6d 100644 --- a/src/lib/routes/admin-api/feature-type.ts +++ b/src/lib/routes/admin-api/feature-type.ts @@ -116,7 +116,7 @@ When a feature toggle type's expected lifetime is changed, this will also cause const result = await this.featureTypeService.updateLifetime( req.params.id.toLowerCase(), req.body.lifetimeDays, - req.user, + req.audit, ); this.openApiService.respondWithValidation( diff --git a/src/lib/routes/admin-api/project/api-token.ts b/src/lib/routes/admin-api/project/api-token.ts index 06016ad8a47..454d943efae 100644 --- a/src/lib/routes/admin-api/project/api-token.ts +++ b/src/lib/routes/admin-api/project/api-token.ts @@ -27,7 +27,6 @@ import type { ProjectService, FrontendApiService, } from '../../../services'; -import { extractUserId, extractUsername } from '../../../util'; import type { IAuthRequest } from '../../unleash-types'; import Controller from '../../controller'; import type { Logger } from '../../../logger'; @@ -189,8 +188,7 @@ export class ProjectApiTokenController extends Controller { ) { const token = await this.apiTokenService.createApiToken( createToken, - extractUsername(req), - extractUserId(req), + req.audit, ); this.openApiService.respondWithValidation( 201, @@ -221,11 +219,7 @@ export class ProjectApiTokenController extends Controller { (storedToken.projects.length === 1 && storedToken.project[0] === projectId)) ) { - await this.apiTokenService.delete( - token, - extractUsername(req), - user.id, - ); + await this.apiTokenService.delete(token, req.audit); await this.frontendApiService.deleteClientForFrontendApiToken( token, ); diff --git a/src/lib/routes/admin-api/project/project-archive.ts b/src/lib/routes/admin-api/project/project-archive.ts index cc607821668..61aaa06fc69 100644 --- a/src/lib/routes/admin-api/project/project-archive.ts +++ b/src/lib/routes/admin-api/project/project-archive.ts @@ -7,7 +7,6 @@ import { UPDATE_FEATURE, } from '../../../types'; import type { Logger } from '../../../logger'; -import { extractUsername } from '../../../util/extract-user'; import { DELETE_FEATURE } from '../../../types/permissions'; import type FeatureToggleService from '../../../features/feature-toggle/feature-toggle-service'; import type { IAuthRequest } from '../../unleash-types'; @@ -165,12 +164,10 @@ export default class ProjectArchiveController extends Controller { ): Promise { const { projectId } = req.params; const { features } = req.body; - const user = extractUsername(req); await this.featureService.deleteFeatures( features, projectId, - user, - req.user.id, + req.audit, ); res.status(200).end(); } @@ -181,13 +178,11 @@ export default class ProjectArchiveController extends Controller { ): Promise { const { projectId } = req.params; const { features } = req.body; - const user = extractUsername(req); await this.startTransaction(async (tx) => this.transactionalFeatureToggleService(tx).reviveFeatures( features, projectId, - user, - req.user.id, + req.audit, ), ); res.status(200).end(); @@ -204,6 +199,7 @@ export default class ProjectArchiveController extends Controller { this.transactionalFeatureToggleService(tx).archiveToggles( features, req.user, + req.audit, projectId, ), ); diff --git a/src/lib/routes/admin-api/project/variants.ts b/src/lib/routes/admin-api/project/variants.ts index 980cdeda181..79152539751 100644 --- a/src/lib/routes/admin-api/project/variants.ts +++ b/src/lib/routes/admin-api/project/variants.ts @@ -239,6 +239,7 @@ The backend will also distribute remaining weight up to 1000 after adding the va projectId, req.body, req.user, + req.audit, ); res.status(200).json({ version: 1, @@ -256,8 +257,7 @@ The backend will also distribute remaining weight up to 1000 after adding the va featureName, projectId, req.body, - userName, - req.user.id, + req.audit, ); res.status(200).json({ version: 1, @@ -300,6 +300,7 @@ The backend will also distribute remaining weight up to 1000 after adding the va environments, variantsWithDefaults, req.user, + req.audit, ); res.status(200).json({ version: 1, @@ -354,6 +355,7 @@ The backend will also distribute remaining weight up to 1000 after adding the va environment, req.body, req.user, + req.audit, ); res.status(200).json({ version: 1, @@ -372,6 +374,7 @@ The backend will also distribute remaining weight up to 1000 after adding the va environment, req.body, req.user, + req.audit, ); 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 eccc47c8929..adc7f45afe5 100644 --- a/src/lib/routes/admin-api/public-signup.ts +++ b/src/lib/routes/admin-api/public-signup.ts @@ -27,7 +27,6 @@ import { resourceCreatedResponseSchema, } from '../../openapi'; import type UserService from '../../services/user-service'; -import { extractUsername } from '../../util'; interface TokenParam { token: string; @@ -185,12 +184,10 @@ export class PublicSignupController extends Controller { req: IAuthRequest, res: Response, ): Promise { - const username = extractUsername(req); const token = await this.publicSignupTokenService.createNewPublicSignupToken( req.body, - username, - req.user.id, + req.audit, ); this.openApiService.respondWithValidation( 201, @@ -219,8 +216,7 @@ export class PublicSignupController extends Controller { ...(enabled === undefined ? {} : { enabled }), ...(expiresAt ? { expiresAt: new Date(expiresAt) } : {}), }, - extractUsername(req), - req.user.id, + req.audit, ); this.openApiService.respondWithValidation( diff --git a/src/lib/routes/admin-api/strategy.ts b/src/lib/routes/admin-api/strategy.ts index d91a1f39c4d..a68d3e10787 100644 --- a/src/lib/routes/admin-api/strategy.ts +++ b/src/lib/routes/admin-api/strategy.ts @@ -5,10 +5,10 @@ import type { Logger } from '../../logger'; import Controller from '../controller'; import { extractUsername } from '../../util/extract-user'; import { - DELETE_STRATEGY, CREATE_STRATEGY, - UPDATE_STRATEGY, + DELETE_STRATEGY, NONE, + UPDATE_STRATEGY, } from '../../types/permissions'; import type { Request, Response } from 'express'; import type { IAuthRequest } from '../unleash-types'; @@ -233,11 +233,7 @@ class StrategyController extends Controller { const strategyName = req.params.name; const userName = extractUsername(req); - await this.strategyService.removeStrategy( - strategyName, - userName, - req.user.id, - ); + await this.strategyService.removeStrategy(strategyName, req.audit); res.status(200).end(); } @@ -249,8 +245,7 @@ class StrategyController extends Controller { const strategy = await this.strategyService.createStrategy( req.body, - userName, - req.user.id, + req.audit, ); this.openApiService.respondWithValidation( 201, @@ -269,8 +264,7 @@ class StrategyController extends Controller { await this.strategyService.updateStrategy( { ...req.body, name: req.params.name }, - userName, - req.user.id, + req.audit, ); res.status(200).end(); } @@ -282,11 +276,7 @@ class StrategyController extends Controller { const userName = extractUsername(req); const { strategyName } = req.params; - await this.strategyService.deprecateStrategy( - strategyName, - userName, - req.user.id, - ); + await this.strategyService.deprecateStrategy(strategyName, req.audit); res.status(200).end(); } @@ -297,11 +287,7 @@ class StrategyController extends Controller { const userName = extractUsername(req); const { strategyName } = req.params; - await this.strategyService.reactivateStrategy( - strategyName, - userName, - req.user.id, - ); + await this.strategyService.reactivateStrategy(strategyName, req.audit); res.status(200).end(); } } diff --git a/src/lib/routes/admin-api/tag.ts b/src/lib/routes/admin-api/tag.ts index bd9a3a52e52..a7363f55631 100644 --- a/src/lib/routes/admin-api/tag.ts +++ b/src/lib/routes/admin-api/tag.ts @@ -200,11 +200,7 @@ class TagController extends Controller { res: Response, ): Promise { const userName = extractUsername(req); - const tag = await this.tagService.createTag( - req.body, - userName, - req.user.id, - ); + const tag = await this.tagService.createTag(req.body, req.audit); res.status(201) .header('location', `tags/${tag.type}/${tag.value}`) .json({ version, tag }) @@ -217,7 +213,7 @@ class TagController extends Controller { ): Promise { const { type, value } = req.params; const userName = extractUsername(req); - await this.tagService.deleteTag({ type, value }, userName, req.user.id); + await this.tagService.deleteTag({ type, value }, req.audit); res.status(200).end(); } } diff --git a/src/lib/routes/admin-api/user-admin.ts b/src/lib/routes/admin-api/user-admin.ts index 0764ad45cd1..42ca4887669 100644 --- a/src/lib/routes/admin-api/user-admin.ts +++ b/src/lib/routes/admin-api/user-admin.ts @@ -538,7 +538,7 @@ export default class UserAdminController extends Controller { password, rootRole: normalizedRootRole, }, - user, + req.audit, ); const passwordAuthSettings = @@ -621,7 +621,7 @@ export default class UserAdminController extends Controller { email, rootRole: normalizedRootRole, }, - user, + req.audit, ); this.openApiService.respondWithValidation( @@ -641,7 +641,7 @@ export default class UserAdminController extends Controller { await this.throwIfScimUser({ id: Number(id) }); - await this.userService.deleteUser(+id, user); + await this.userService.deleteUser(+id, req.audit); res.status(200).send(); } diff --git a/src/lib/routes/admin-api/user/pat.ts b/src/lib/routes/admin-api/user/pat.ts index 246b7b075ff..1f9bdddb706 100644 --- a/src/lib/routes/admin-api/user/pat.ts +++ b/src/lib/routes/admin-api/user/pat.ts @@ -11,9 +11,11 @@ import { createResponseSchema, resourceCreatedResponseSchema, } from '../../../openapi/util/create-response-schema'; -import { getStandardResponses } from '../../../openapi/util/standard-responses'; +import { + emptyResponse, + getStandardResponses, +} from '../../../openapi/util/standard-responses'; import type { OpenApiService } from '../../../services/openapi-service'; -import { emptyResponse } from '../../../openapi/util/standard-responses'; import type PatService from '../../../services/pat-service'; import { NONE } from '../../../types/permissions'; @@ -131,7 +133,7 @@ export default class PatController extends Controller { const createdPat = await this.patService.createPat( pat, req.user.id, - req.user, + req.audit, ); this.openApiService.respondWithValidation( 201, @@ -161,7 +163,7 @@ export default class PatController extends Controller { res: Response, ): Promise { const { id } = req.params; - await this.patService.deletePat(id, req.user.id, req.user); + await this.patService.deletePat(id, req.user.id, req.audit); res.status(200).end(); } } diff --git a/src/lib/routes/public-invite.ts b/src/lib/routes/public-invite.ts index 37c7a11ae49..f5e2e041fd3 100644 --- a/src/lib/routes/public-invite.ts +++ b/src/lib/routes/public-invite.ts @@ -110,6 +110,7 @@ export class PublicInviteController extends Controller { const user = await this.publicSignupTokenService.addTokenUser( token, req.body, + req.audit, ); this.openApiService.respondWithValidation( 201, diff --git a/src/lib/routes/unleash-types.ts b/src/lib/routes/unleash-types.ts index 4917981b617..2e5bc59c8c1 100644 --- a/src/lib/routes/unleash-types.ts +++ b/src/lib/routes/unleash-types.ts @@ -11,7 +11,7 @@ export interface IAuthRequest< user: IUser; logout: (() => void) | ((callback: (err?: any) => void) => void); session: any; - audit?: IAuditUser; + audit: IAuditUser; } export interface IApiRequest< @@ -23,7 +23,7 @@ export interface IApiRequest< user: IApiUser; logout: (() => void) | ((callback: (err?: any) => void) => void); session: any; - audit?: IAuditUser; + audit: IAuditUser; } export interface RequestBody extends Express.Request { diff --git a/src/lib/services/access-service.test.ts b/src/lib/services/access-service.test.ts index a26128f1619..5a2c5bb82b0 100644 --- a/src/lib/services/access-service.test.ts +++ b/src/lib/services/access-service.test.ts @@ -15,7 +15,12 @@ import FakeEnvironmentStore from '../features/project-environments/fake-environm import AccessStoreMock from '../../test/fixtures/fake-access-store'; import { GroupService } from '../services/group-service'; import type { IRole } from '../../lib/types/stores/access-store'; -import { type IGroup, ROLE_CREATED, SYSTEM_USER } from '../../lib/types'; +import { + type IGroup, + ROLE_CREATED, + SYSTEM_USER, + SYSTEM_USER_AUDIT, +} from '../../lib/types'; import BadDataError from '../../lib/error/bad-data-error'; import { createFakeEventsService } from '../../lib/features/events/createEventsService'; @@ -29,12 +34,15 @@ function getSetup() { test('should fail when name exists', async () => { const { accessService } = getSetup(); - const existingRole = await accessService.createRole({ - name: 'existing role', - description: 'description', - permissions: [], - createdByUserId: -9999, - }); + const existingRole = await accessService.createRole( + { + name: 'existing role', + description: 'description', + permissions: [], + createdByUserId: -9999, + }, + SYSTEM_USER_AUDIT, + ); expect(accessService.validateRole(existingRole)).rejects.toThrow( new NameExistsError( @@ -179,7 +187,10 @@ test('user with custom root role should get a user root role', async () => { ], }; - const customRootRole = await accessService.createRole(createRoleInput); + const customRootRole = await accessService.createRole( + createRoleInput, + SYSTEM_USER_AUDIT, + ); const user = { id: 1, rootRole: customRootRole.id, @@ -192,8 +203,9 @@ test('user with custom root role should get a user root role', async () => { expect(events).toHaveLength(1); expect(events[0]).toEqual({ type: ROLE_CREATED, - createdBy: 'unknown', - createdByUserId: -9999, + createdBy: SYSTEM_USER_AUDIT.username, + createdByUserId: SYSTEM_USER.id, + ip: SYSTEM_USER_AUDIT.ip, data: { id: 0, name: 'custom-root-role', @@ -202,7 +214,7 @@ test('user with custom root role should get a user root role', async () => { // make sure we have a cleaned up version of permissions in the event permissions: [ { environment: 'development', name: 'fake' }, - { name: 'root-fake-permission' }, + { name: 'root-fake-permission', environment: undefined }, ], }, }); @@ -251,7 +263,7 @@ test('throws error when trying to delete a project role in use by group', async ); try { - await accessService.deleteRole(1, SYSTEM_USER.username, SYSTEM_USER.id); + await accessService.deleteRole(1, SYSTEM_USER_AUDIT); } 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 31229add015..9842085b780 100644 --- a/src/lib/services/access-service.ts +++ b/src/lib/services/access-service.ts @@ -1,5 +1,5 @@ import * as permissions from '../types/permissions'; -import type { IUser } from '../types/user'; +import type { IAuditUser, IUser } from '../types/user'; import type { IAccessInfo, IAccessStore, @@ -43,10 +43,9 @@ import type { GroupService } from './group-service'; import { type IUnleashConfig, type IUserAccessOverview, - ROLE_CREATED, - ROLE_DELETED, - ROLE_UPDATED, - SYSTEM_USER, + RoleCreatedEvent, + RoleDeletedEvent, + RoleUpdatedEvent, } from '../types'; import type EventService from '../features/events/event-service'; @@ -336,14 +335,14 @@ export class AccessService { groups: IAccessInfo[], projectId: string, roleId: number, - createdBy: string, + auditUser: IAuditUser, ): Promise { return this.store.addRoleAccessToProject( users, groups, projectId, roleId, - createdBy, + auditUser.username, ); } @@ -707,7 +706,10 @@ export class AccessService { return this.roleStore.getAll(); } - async createRole(role: IRoleCreation): Promise { + async createRole( + role: IRoleCreation, + auditUser: IAuditUser, + ): Promise { // CUSTOM_PROJECT_ROLE_TYPE is assumed by default for backward compatibility const roleType = role.type === CUSTOM_ROOT_ROLE_TYPE @@ -739,19 +741,22 @@ export class AccessService { const addedPermissions = await this.store.getPermissionsForRole( newRole.id, ); - this.eventService.storeEvent({ - type: ROLE_CREATED, - createdBy: role.createdBy || 'unknown', - createdByUserId: role.createdByUserId, - data: { - ...newRole, - permissions: this.sanitizePermissions(addedPermissions), - }, - }); + this.eventService.storeEvent( + new RoleCreatedEvent({ + data: { + ...newRole, + permissions: this.sanitizePermissions(addedPermissions), + }, + auditUser, + }), + ); return newRole; } - async updateRole(role: IRoleUpdate): Promise { + async updateRole( + role: IRoleUpdate, + auditUser: IAuditUser, + ): Promise { const roleType = role.type === CUSTOM_ROOT_ROLE_TYPE ? CUSTOM_ROOT_ROLE_TYPE @@ -787,19 +792,19 @@ export class AccessService { const updatedPermissions = await this.store.getPermissionsForRole( role.id, ); - this.eventService.storeEvent({ - type: ROLE_UPDATED, - createdBy: role.createdBy || SYSTEM_USER.username, - createdByUserId: role.createdByUserId, - data: { - ...updatedRole, - permissions: this.sanitizePermissions(updatedPermissions), - }, - preData: { - ...existingRole, - permissions: this.sanitizePermissions(existingPermissions), - }, - }); + this.eventService.storeEvent( + new RoleUpdatedEvent({ + data: { + ...updatedRole, + permissions: this.sanitizePermissions(updatedPermissions), + }, + preData: { + ...existingRole, + permissions: this.sanitizePermissions(existingPermissions), + }, + auditUser, + }), + ); return updatedRole; } @@ -815,11 +820,7 @@ export class AccessService { }); } - async deleteRole( - id: number, - deletedBy: string, - deletedByUserId: number, - ): Promise { + async deleteRole(id: number, deletedBy: IAuditUser): Promise { await this.validateRoleIsNotBuiltIn(id); const roleUsers = await this.getUsersForRole(id); @@ -834,15 +835,15 @@ export class AccessService { const existingRole = await this.roleStore.get(id); const existingPermissions = await this.store.getPermissionsForRole(id); await this.roleStore.delete(id); - this.eventService.storeEvent({ - type: ROLE_DELETED, - createdBy: deletedBy, - createdByUserId: deletedByUserId, - preData: { - ...existingRole, - permissions: this.sanitizePermissions(existingPermissions), - }, - }); + this.eventService.storeEvent( + new RoleDeletedEvent({ + preData: { + ...existingRole, + permissions: this.sanitizePermissions(existingPermissions), + }, + auditUser: deletedBy, + }), + ); return; } diff --git a/src/lib/services/addon-service.test.ts b/src/lib/services/addon-service.test.ts index 9d1d82a9097..8dca7545e01 100644 --- a/src/lib/services/addon-service.test.ts +++ b/src/lib/services/addon-service.test.ts @@ -15,8 +15,8 @@ import type { IAddonDto } from '../types/stores/addon-store'; import SimpleAddon from './addon-service-test-simple-addon'; import type { IAddonProviders } from '../addons'; import EventService from '../features/events/event-service'; -import { SYSTEM_USER } from '../types'; -import { EventEmitter } from 'stream'; +import { SYSTEM_USER, TEST_AUDIT_USER } from '../types'; +import EventEmitter from 'node:events'; const MASKED_VALUE = '*****'; @@ -86,8 +86,7 @@ test('should not allow addon-config for unknown provider', async () => { events: [], description: '', }, - 'test', - TEST_USER_ID, + TEST_AUDIT_USER, ); }).rejects.toThrow(ValidationError); }); @@ -106,12 +105,12 @@ test('should trigger simple-addon eventHandler', async () => { description: '', }; - await addonService.createAddon(config, 'me@mail.com', TEST_USER_ID); + await addonService.createAddon(config, TEST_AUDIT_USER); // Feature toggle was created await eventService.storeEvent({ type: FEATURE_CREATED, - createdBy: SYSTEM_USER.username, + createdBy: SYSTEM_USER.username!, createdByUserId: SYSTEM_USER.id, data: { name: 'some-toggle', @@ -142,7 +141,7 @@ test('should not trigger event handler if project of event is different from add }, }; - await addonService.createAddon(config, 'me@mail.com', TEST_USER_ID); + await addonService.createAddon(config, TEST_AUDIT_USER); await eventService.storeEvent({ type: FEATURE_CREATED, createdBy: SYSTEM_USER.username, @@ -176,7 +175,7 @@ test('should trigger event handler if project for event is one of the desired pr }, }; - await addonService.createAddon(config, 'me@mail.com', TEST_USER_ID); + await addonService.createAddon(config, TEST_AUDIT_USER); await eventService.storeEvent({ type: FEATURE_CREATED, createdBy: SYSTEM_USER.username, @@ -223,7 +222,7 @@ test('should trigger events for multiple projects if addon is setup to filter mu }, }; - await addonService.createAddon(config, 'me@mail.com', TEST_USER_ID); + await addonService.createAddon(config, TEST_AUDIT_USER); await eventService.storeEvent({ type: FEATURE_CREATED, createdBy: SYSTEM_USER.username, @@ -284,7 +283,7 @@ test('should filter events on environment if addon is setup to filter for it', a }, }; - await addonService.createAddon(config, 'me@mail.com', TEST_USER_ID); + await addonService.createAddon(config, TEST_AUDIT_USER); await eventService.storeEvent({ type: FEATURE_CREATED, createdBy: SYSTEM_USER.username, @@ -344,7 +343,7 @@ test('should not filter out global events (no specific environment) even if addo }, }; - await addonService.createAddon(config, 'me@mail.com', TEST_USER_ID); + await addonService.createAddon(config, TEST_AUDIT_USER); await eventService.storeEvent(globalEventWithNoEnvironment); const simpleProvider = addonService.addonProviders.simple; // @ts-expect-error @@ -381,7 +380,7 @@ test('should not filter out global events (no specific project) even if addon is }, }; - await addonService.createAddon(config, 'me@mail.com', TEST_USER_ID); + await addonService.createAddon(config, TEST_AUDIT_USER); await eventService.storeEvent(globalEventWithNoProject); const simpleProvider = addonService.addonProviders.simple; // @ts-expect-error @@ -407,7 +406,7 @@ test('should support wildcard option for filtering addons', async () => { }, }; - await addonService.createAddon(config, 'me@mail.com', TEST_USER_ID); + await addonService.createAddon(config, TEST_AUDIT_USER); await eventService.storeEvent({ type: FEATURE_CREATED, createdBy: SYSTEM_USER.username, @@ -474,7 +473,7 @@ test('Should support filtering by both project and environment', async () => { 'desired-toggle2', 'desired-toggle3', ]; - await addonService.createAddon(config, 'me@mail.com', TEST_USER_ID); + await addonService.createAddon(config, TEST_AUDIT_USER); await eventService.storeEvent({ type: FEATURE_CREATED, createdBy: SYSTEM_USER.username, @@ -563,7 +562,7 @@ test('should create simple-addon config', async () => { description: '', }; - await addonService.createAddon(config, 'me@mail.com', TEST_USER_ID); + await addonService.createAddon(config, TEST_AUDIT_USER); const addons = await addonService.getAddons(); expect(addons.length).toBe(1); @@ -584,7 +583,7 @@ test('should create tag type for simple-addon', async () => { description: '', }; - await addonService.createAddon(config, 'me@mail.com', TEST_USER_ID); + await addonService.createAddon(config, TEST_AUDIT_USER); const tagType = await tagTypeService.getTagType('me'); expect(tagType.name).toBe('me'); @@ -604,7 +603,7 @@ test('should store ADDON_CONFIG_CREATE event', async () => { description: '', }; - await addonService.createAddon(config, 'me@mail.com', TEST_USER_ID); + await addonService.createAddon(config, TEST_AUDIT_USER); const { events } = await eventService.getEvents(); @@ -627,19 +626,10 @@ test('should store ADDON_CONFIG_UPDATE event', async () => { events: [FEATURE_CREATED], }; - const addonConfig = await addonService.createAddon( - config, - 'me@mail.com', - TEST_USER_ID, - ); + const addonConfig = await addonService.createAddon(config, TEST_AUDIT_USER); const updated = { ...addonConfig, description: 'test' }; - await addonService.updateAddon( - addonConfig.id, - updated, - 'me@mail.com', - TEST_USER_ID, - ); + await addonService.updateAddon(addonConfig.id, updated, TEST_AUDIT_USER); const { events } = await eventService.getEvents(); @@ -662,13 +652,9 @@ test('should store ADDON_CONFIG_REMOVE event', async () => { events: [FEATURE_CREATED], }; - const addonConfig = await addonService.createAddon( - config, - 'me@mail.com', - TEST_USER_ID, - ); + const addonConfig = await addonService.createAddon(config, TEST_AUDIT_USER); - await addonService.removeAddon(addonConfig.id, 'me@mail.com', TEST_USER_ID); + await addonService.removeAddon(addonConfig.id, TEST_AUDIT_USER); const { events } = await eventService.getEvents(); @@ -694,8 +680,7 @@ test('should hide sensitive fields when fetching', async () => { const createdConfig = await addonService.createAddon( config, - 'me@mail.com', - TEST_USER_ID, + TEST_AUDIT_USER, ); const addons = await addonService.getAddons(); const addonRetrieved = await addonService.getAddon(createdConfig.id); @@ -721,23 +706,14 @@ test('should not overwrite masked values when updating', async () => { description: '', }; - const addonConfig = await addonService.createAddon( - config, - 'me@mail.com', - TEST_USER_ID, - ); + const addonConfig = await addonService.createAddon(config, TEST_AUDIT_USER); const updated = { ...addonConfig, parameters: { url: MASKED_VALUE, var: 'some-new-value' }, description: 'test', }; - await addonService.updateAddon( - addonConfig.id, - updated, - 'me@mail.com', - TEST_USER_ID, - ); + await addonService.updateAddon(addonConfig.id, updated, TEST_AUDIT_USER); const updatedConfig = await stores.addonStore.get(addonConfig.id); // @ts-ignore @@ -760,7 +736,7 @@ test('should reject addon config with missing required parameter when creating', }; await expect(async () => - addonService.createAddon(config, 'me@mail.com', TEST_USER_ID), + addonService.createAddon(config, TEST_AUDIT_USER), ).rejects.toThrow(ValidationError); }); @@ -778,23 +754,14 @@ test('should reject updating addon config with missing required parameter', asyn description: '', }; - const config = await addonService.createAddon( - addonConfig, - 'me@mail.com', - TEST_USER_ID, - ); + const config = await addonService.createAddon(addonConfig, TEST_AUDIT_USER); const updated = { ...config, parameters: { var: 'some-new-value' }, description: 'test', }; await expect(async () => - addonService.updateAddon( - config.id, - updated, - 'me@mail.com', - TEST_USER_ID, - ), + addonService.updateAddon(config.id, updated, TEST_AUDIT_USER), ).rejects.toThrow(ValidationError); }); @@ -813,6 +780,6 @@ test('Should reject addon config if a required parameter is just the empty strin }; await expect(async () => - addonService.createAddon(config, 'me@mail.com', TEST_USER_ID), + addonService.createAddon(config, TEST_AUDIT_USER), ).rejects.toThrow(ValidationError); }); diff --git a/src/lib/services/addon-service.ts b/src/lib/services/addon-service.ts index a790aea33b7..b680c2a6663 100644 --- a/src/lib/services/addon-service.ts +++ b/src/lib/services/addon-service.ts @@ -2,6 +2,11 @@ import memoizee from 'memoizee'; import { ValidationError } from 'joi'; import { getAddons, type IAddonProviders } from '../addons'; import * as events from '../types/events'; +import { + AddonConfigCreatedEvent, + AddonConfigDeletedEvent, + AddonConfigUpdatedEvent, +} from '../types/events'; import { addonSchema } from './addon-schema'; import NameExistsError from '../error/name-exists-error'; import type { IFeatureToggleStore } from '../features/feature-toggle/types/feature-toggle-store-type'; @@ -13,9 +18,10 @@ import type { IAddonStore, } from '../types/stores/addon-store'; import { - type IUnleashStores, + type IAuditUser, type IUnleashConfig, - SYSTEM_USER, + type IUnleashStores, + SYSTEM_USER_AUDIT, } from '../types'; import type { IAddonDefinition } from '../types/model'; import { minutesToMilliseconds } from 'date-fns'; @@ -186,8 +192,7 @@ export default class AddonService { await this.tagTypeService.validateUnique(tagType.name); await this.tagTypeService.createTagType( tagType, - providerName, - SYSTEM_USER.id, + SYSTEM_USER_AUDIT, ); } catch (err) { if (!(err instanceof NameExistsError)) { @@ -200,11 +205,7 @@ export default class AddonService { return Promise.resolve(); } - async createAddon( - data: IAddonDto, - userName: string, - userId: number, - ): Promise { + async createAddon(data: IAddonDto, auditUser: IAuditUser): Promise { const addonConfig = await addonSchema.validateAsync(data); await this.validateKnownProvider(addonConfig); await this.validateRequiredParameters(addonConfig); @@ -213,15 +214,15 @@ export default class AddonService { await this.addTagTypes(createdAddon.provider); this.logger.info( - `User ${userName} created addon ${addonConfig.provider}`, + `User ${auditUser.username} created addon ${addonConfig.provider}`, ); - await this.eventService.storeEvent({ - type: events.ADDON_CONFIG_CREATED, - createdBy: userName, - createdByUserId: userId, - data: omitKeys(createdAddon, 'parameters'), - }); + await this.eventService.storeEvent( + new AddonConfigCreatedEvent({ + data: omitKeys(createdAddon, 'parameters'), + auditUser, + }), + ); return createdAddon; } @@ -229,8 +230,7 @@ export default class AddonService { async updateAddon( id: number, data: IAddonDto, - userName: string, - userId: number, + auditUser: IAuditUser, ): Promise { const existingConfig = await this.addonStore.get(id); // because getting an early 404 here makes more sense const addonConfig = await addonSchema.validateAsync(data); @@ -251,31 +251,27 @@ export default class AddonService { ); } const result = await this.addonStore.update(id, addonConfig); - await this.eventService.storeEvent({ - type: events.ADDON_CONFIG_UPDATED, - createdBy: userName, - createdByUserId: userId, - preData: omitKeys(existingConfig, 'parameters'), - data: omitKeys(result, 'parameters'), - }); - this.logger.info(`User ${userName} updated addon ${id}`); + await this.eventService.storeEvent( + new AddonConfigUpdatedEvent({ + preData: omitKeys(existingConfig, 'parameters'), + data: omitKeys(result, 'parameters'), + auditUser, + }), + ); + this.logger.info(`User ${auditUser} updated addon ${id}`); return result; } - async removeAddon( - id: number, - userName: string, - removedByuserId: number, - ): Promise { + async removeAddon(id: number, auditUser: IAuditUser): 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}`); + await this.eventService.storeEvent( + new AddonConfigDeletedEvent({ + preData: omitKeys(existingConfig, 'parameters'), + auditUser, + }), + ); + this.logger.info(`User ${auditUser} removed addon ${id}`); } async validateKnownProvider(config: Partial): Promise { diff --git a/src/lib/services/api-token-service.test.ts b/src/lib/services/api-token-service.test.ts index c9fb871ba45..95d18314ed3 100644 --- a/src/lib/services/api-token-service.test.ts +++ b/src/lib/services/api-token-service.test.ts @@ -10,11 +10,13 @@ import { API_TOKEN_CREATED, API_TOKEN_DELETED, API_TOKEN_UPDATED, + TEST_AUDIT_USER, } from '../types'; import { addDays, minutesToMilliseconds } from 'date-fns'; import EventService from '../features/events/event-service'; import FakeFeatureTagStore from '../../test/fixtures/fake-feature-tag-store'; import { createFakeEventsService } from '../../lib/features'; +import { extractAuditInfoFromUser } from '../util'; test('Should init api token', async () => { const token = { @@ -135,8 +137,12 @@ 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', -9999); - await apiTokenService.delete(saved.secret, 'test', -9999); + await apiTokenService.updateExpiry( + saved.secret, + newExpiry, + TEST_AUDIT_USER, + ); + await apiTokenService.delete(saved.secret, TEST_AUDIT_USER); const { events } = await eventService.getEvents(); const createdApiTokenEvents = events.filter( (e) => e.type === API_TOKEN_CREATED, @@ -182,7 +188,7 @@ test('getUserForToken should get a user with admin token user id and token name' type: ApiTokenType.ADMIN, tokenName: 'admin.token', }, - ADMIN_TOKEN_USER as IUser, + extractAuditInfoFromUser(ADMIN_TOKEN_USER as IUser), ); const user = await tokenService.getUserForToken(token.secret); diff --git a/src/lib/services/api-token-service.ts b/src/lib/services/api-token-service.ts index 46f03ade4cc..21137169844 100644 --- a/src/lib/services/api-token-service.ts +++ b/src/lib/services/api-token-service.ts @@ -24,17 +24,12 @@ import { ApiTokenCreatedEvent, ApiTokenDeletedEvent, ApiTokenUpdatedEvent, + type IAuditUser, type IFlagContext, type IFlagResolver, - type IUser, - SYSTEM_USER, - SYSTEM_USER_ID, + SYSTEM_USER_AUDIT, } from '../types'; -import { - extractUserIdFromUser, - extractUsernameFromUser, - omitKeys, -} from '../util'; +import { omitKeys } from '../util'; import type EventService from '../features/events/event-service'; import { addMinutes, isPast } from 'date-fns'; @@ -151,13 +146,7 @@ export class ApiTokenService { try { const createAll = tokens .map(mapLegacyTokenWithSecret) - .map((t) => - this.insertNewApiToken( - t, - 'init-api-tokens', - SYSTEM_USER_ID, - ), - ); + .map((t) => this.insertNewApiToken(t, SYSTEM_USER_AUDIT)); await Promise.all(createAll); } catch (e) { this.logger.error('Unable to create initial Admin API tokens'); @@ -231,15 +220,13 @@ export class ApiTokenService { public async updateExpiry( secret: string, expiresAt: Date, - updatedBy: string, - updatedById: number, + auditUser: IAuditUser, ): 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, + auditUser, previousToken: omitKeys(previous, 'secret'), apiToken: omitKeys(token, 'secret'), }), @@ -247,18 +234,13 @@ export class ApiTokenService { return token; } - public async delete( - secret: string, - deletedBy: string, - deletedByUserId: number, - ): Promise { + public async delete(secret: string, auditUser: IAuditUser): 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, + auditUser, apiToken: omitKeys(token, 'secret'), }), ); @@ -270,15 +252,10 @@ export class ApiTokenService { */ public async createApiToken( newToken: Omit, - createdBy: string = SYSTEM_USER.username, - createdByUserId: number = SYSTEM_USER.id, + auditUser: IAuditUser = SYSTEM_USER_AUDIT, ): Promise { const token = mapLegacyToken(newToken); - return this.internalCreateApiTokenWithProjects( - token, - createdBy, - createdByUserId, - ); + return this.internalCreateApiTokenWithProjects(token, auditUser); } /** @@ -288,32 +265,14 @@ export class ApiTokenService { */ public async createApiTokenWithProjects( newToken: Omit, - createdBy?: string | IApiUser | IUser, - createdByUserId?: number, + auditUser: IAuditUser = SYSTEM_USER_AUDIT, ): Promise { - // if statement to support old method signature - if ( - createdBy === undefined || - typeof createdBy === 'string' || - createdByUserId - ) { - return this.internalCreateApiTokenWithProjects( - newToken, - (createdBy as string) || SYSTEM_USER.username, - createdByUserId || SYSTEM_USER.id, - ); - } - return this.internalCreateApiTokenWithProjects( - newToken, - extractUsernameFromUser(createdBy), - extractUserIdFromUser(createdBy), - ); + return this.internalCreateApiTokenWithProjects(newToken, auditUser); } private async internalCreateApiTokenWithProjects( newToken: Omit, - createdBy: string, - createdByUserId: number, + auditUser: IAuditUser, ): Promise { validateApiToken(newToken); const environments = await this.environmentStore.getAll(); @@ -321,11 +280,7 @@ export class ApiTokenService { const secret = this.generateSecretKey(newToken); const createNewToken = { ...newToken, secret }; - return this.insertNewApiToken( - createNewToken, - createdBy, - createdByUserId, - ); + return this.insertNewApiToken(createNewToken, auditUser); } // TODO: Remove this service method after embedded proxy has been released in @@ -337,25 +292,19 @@ export class ApiTokenService { const secret = this.generateSecretKey(newToken); const createNewToken = { ...newToken, secret }; - return this.insertNewApiToken( - createNewToken, - 'system-migration', - SYSTEM_USER_ID, - ); + return this.insertNewApiToken(createNewToken, SYSTEM_USER_AUDIT); } private async insertNewApiToken( newApiToken: IApiTokenCreate, - createdBy: string, - createdByUserId: number, + auditUser: IAuditUser, ): Promise { try { const token = await this.store.insert(newApiToken); this.activeTokens.push(token); await this.eventService.storeEvent( new ApiTokenCreatedEvent({ - createdBy, - createdByUserId, + auditUser, apiToken: omitKeys(token, 'secret'), }), ); diff --git a/src/lib/services/context-service.ts b/src/lib/services/context-service.ts index 26c6b3d38fd..8a938d363ec 100644 --- a/src/lib/services/context-service.ts +++ b/src/lib/services/context-service.ts @@ -8,18 +8,19 @@ import type { IProjectStore } from '../features/project/project-store-type'; import type { IFeatureStrategiesStore, IUnleashStores } from '../types/stores'; import type { IUnleashConfig } from '../types/option'; import type { ContextFieldStrategiesSchema } from '../openapi/spec/context-field-strategies-schema'; -import type { IFeatureStrategy, IFlagResolver } from '../types'; -import type { IPrivateProjectChecker } from '../features/private-project/privateProjectCheckerType'; -import type EventService from '../features/events/event-service'; - -const { contextSchema, nameSchema } = require('./context-schema'); -const NameExistsError = require('../error/name-exists-error'); - -const { +import { CONTEXT_FIELD_CREATED, - CONTEXT_FIELD_UPDATED, CONTEXT_FIELD_DELETED, -} = require('../types/events'); + CONTEXT_FIELD_UPDATED, + type IAuditUser, + type IFeatureStrategy, + type IFlagResolver, +} from '../types'; +import type { IPrivateProjectChecker } from '../features/private-project/privateProjectCheckerType'; +import type EventService from '../features/events/event-service'; +import { contextSchema } from './context-schema'; +import { NameExistsError } from '../error'; +import { nameSchema } from '../schema/feature-schema'; class ContextService { private projectStore: IProjectStore; @@ -102,8 +103,7 @@ class ContextService { async createContextField( value: IContextFieldDto, - userName: string, - createdByUserId: number, + auditUser: IAuditUser, ): Promise { // validations await this.validateUniqueName(value); @@ -113,8 +113,9 @@ class ContextService { const createdField = await this.contextFieldStore.create(value); await this.eventService.storeEvent({ type: CONTEXT_FIELD_CREATED, - createdBy: userName, - createdByUserId, + createdBy: auditUser.username, + createdByUserId: auditUser.id, + ip: auditUser.ip, data: contextField, }); @@ -123,8 +124,7 @@ class ContextService { async updateContextField( updatedContextField: IContextFieldDto, - userName: string, - updatedByUserId: number, + auditUser: IAuditUser, ): Promise { const contextField = await this.contextFieldStore.get( updatedContextField.name, @@ -137,8 +137,9 @@ class ContextService { const { createdAt, sortOrder, ...previousContextField } = contextField; await this.eventService.storeEvent({ type: CONTEXT_FIELD_UPDATED, - createdBy: userName, - createdByUserId: updatedByUserId, + createdBy: auditUser.username, + createdByUserId: auditUser.id, + ip: auditUser.ip, preData: previousContextField, data: value, }); @@ -146,8 +147,7 @@ class ContextService { async deleteContextField( name: string, - userName: string, - deletedByUserId: number, + auditUser: IAuditUser, ): Promise { const contextField = await this.contextFieldStore.get(name); @@ -155,8 +155,9 @@ class ContextService { await this.contextFieldStore.delete(name); await this.eventService.storeEvent({ type: CONTEXT_FIELD_DELETED, - createdBy: userName, - createdByUserId: deletedByUserId, + createdBy: auditUser.username, + createdByUserId: auditUser.id, + ip: auditUser.ip, preData: contextField, }); } diff --git a/src/lib/services/favorites-service.ts b/src/lib/services/favorites-service.ts index 90b0d9e546e..fb5b958f7bc 100644 --- a/src/lib/services/favorites-service.ts +++ b/src/lib/services/favorites-service.ts @@ -4,13 +4,13 @@ import type { Logger } from '../logger'; import type { IFavoriteFeaturesStore } from '../types/stores/favorite-features'; import type { IFavoriteFeature, IFavoriteProject } from '../types/favorites'; import { - FEATURE_FAVORITED, - FEATURE_UNFAVORITED, - PROJECT_FAVORITED, - PROJECT_UNFAVORITED, + FeatureFavoritedEvent, + FeatureUnfavoritedEvent, + type IAuditUser, + ProjectFavoritedEvent, + ProjectUnfavoritedEvent, } from '../types'; import type { IUser } from '../types/user'; -import { extractUsernameFromUser } from '../util'; import type { IFavoriteProjectKey } from '../types/stores/favorite-projects'; import type EventService from '../features/events/event-service'; @@ -53,81 +53,83 @@ export class FavoritesService { this.eventService = eventService; } - async favoriteFeature({ - feature, - user, - }: IFavoriteFeatureProps): Promise { + async favoriteFeature( + { feature, user }: IFavoriteFeatureProps, + auditUser: IAuditUser, + ): Promise { const data = await this.favoriteFeaturesStore.addFavoriteFeature({ feature: feature, userId: user.id, }); - await this.eventService.storeEvent({ - type: FEATURE_FAVORITED, - featureName: feature, - createdBy: extractUsernameFromUser(user), - createdByUserId: user.id, - data: { - feature, - }, - }); + await this.eventService.storeEvent( + new FeatureFavoritedEvent({ + featureName: feature, + data: { + feature, + }, + auditUser, + }), + ); return data; } - async unfavoriteFeature({ - feature, - user, - }: IFavoriteFeatureProps): Promise { + async unfavoriteFeature( + { feature, user }: IFavoriteFeatureProps, + auditUser: IAuditUser, + ): Promise { const data = await this.favoriteFeaturesStore.delete({ feature: feature, userId: user.id, }); - await this.eventService.storeEvent({ - type: FEATURE_UNFAVORITED, - featureName: feature, - createdBy: extractUsernameFromUser(user), - createdByUserId: user.id, - data: { - feature, - }, - }); + await this.eventService.storeEvent( + new FeatureUnfavoritedEvent({ + featureName: feature, + data: { + feature, + }, + auditUser, + }), + ); return data; } - async favoriteProject({ - project, - user, - }: IFavoriteProjectProps): Promise { + async favoriteProject( + { project, user }: IFavoriteProjectProps, + auditUser: IAuditUser, + ): Promise { const data = this.favoriteProjectsStore.addFavoriteProject({ project, userId: user.id, }); - await this.eventService.storeEvent({ - type: PROJECT_FAVORITED, - createdBy: extractUsernameFromUser(user), - createdByUserId: user.id, - data: { + await this.eventService.storeEvent( + new ProjectFavoritedEvent({ + data: { + project, + }, project, - }, - }); + auditUser, + }), + ); return data; } - async unfavoriteProject({ - project, - user, - }: IFavoriteProjectProps): Promise { + async unfavoriteProject( + { project, user }: IFavoriteProjectProps, + auditUser: IAuditUser, + ): Promise { const data = this.favoriteProjectsStore.delete({ project: project, userId: user.id, }); - await this.eventService.storeEvent({ - type: PROJECT_UNFAVORITED, - createdBy: extractUsernameFromUser(user), - createdByUserId: user.id, - data: { + await this.eventService.storeEvent( + new ProjectUnfavoritedEvent({ + data: { + project, + }, project, - }, - }); + auditUser, + }), + ); return data; } diff --git a/src/lib/services/feature-tag-service.ts b/src/lib/services/feature-tag-service.ts index 431a5f95fc5..b8323f50926 100644 --- a/src/lib/services/feature-tag-service.ts +++ b/src/lib/services/feature-tag-service.ts @@ -1,6 +1,11 @@ import NotFoundError from '../error/notfound-error'; import type { Logger } from '../logger'; -import { FEATURE_TAGGED, FEATURE_UNTAGGED, TAG_CREATED } from '../types/events'; +import { + FEATURE_TAGGED, + FEATURE_UNTAGGED, + FeatureTaggedEvent, + TAG_CREATED, +} from '../types/events'; import type { IUnleashConfig } from '../types/option'; import type { IFeatureToggleStore, IUnleashStores } from '../types/stores'; import { tagSchema } from './tag-schema'; @@ -13,6 +18,7 @@ import type { ITagStore } from '../types/stores/tag-store'; import type { ITag } from '../types/model'; import { BadDataError, FOREIGN_KEY_VIOLATION } from '../../lib/error'; import type EventService from '../features/events/event-service'; +import type { IAuditUser } from '../types'; class FeatureTagService { private tagStore: ITagStore; @@ -56,26 +62,25 @@ class FeatureTagService { async addTag( featureName: string, tag: ITag, - userName: string, - addedByUserId: number, + auditUser: IAuditUser, ): Promise { const featureToggle = await this.featureToggleStore.get(featureName); const validatedTag = await tagSchema.validateAsync(tag); - await this.createTagIfNeeded(validatedTag, userName, addedByUserId); + await this.createTagIfNeeded(validatedTag, auditUser); await this.featureTagStore.tagFeature( featureName, validatedTag, - addedByUserId, + auditUser.id, ); - await this.eventService.storeEvent({ - type: FEATURE_TAGGED, - createdBy: userName, - createdByUserId: addedByUserId, - featureName, - project: featureToggle.project, - data: validatedTag, - }); + await this.eventService.storeEvent( + new FeatureTaggedEvent({ + featureName, + project: featureToggle.project, + data: validatedTag, + auditUser, + }), + ); return validatedTag; } @@ -83,15 +88,12 @@ class FeatureTagService { featureNames: string[], addedTags: ITag[], removedTags: ITag[], - userName: string, - updatedByUserId: number, + auditUser: IAuditUser, ): Promise { const featureToggles = await this.featureToggleStore.getAllByNames(featureNames); await Promise.all( - addedTags.map((tag) => - this.createTagIfNeeded(tag, userName, updatedByUserId), - ), + addedTags.map((tag) => this.createTagIfNeeded(tag, auditUser)), ); const createdFeatureTags: IFeatureTagInsert[] = featureNames.flatMap( (featureName) => @@ -99,7 +101,7 @@ class FeatureTagService { featureName, tagType: addedTag.type, tagValue: addedTag.value, - createdByUserId: updatedByUserId, + createdByUserId: auditUser.id, })), ); @@ -119,22 +121,24 @@ class FeatureTagService { const creationEvents = featureToggles.flatMap((featureToggle) => addedTags.map((addedTag) => ({ type: FEATURE_TAGGED, - createdBy: userName, + createdBy: auditUser.username, featureName: featureToggle.name, project: featureToggle.project, data: addedTag, - createdByUserId: updatedByUserId, + createdByUserId: auditUser.id, + ip: auditUser.ip, })), ); const removalEvents = featureToggles.flatMap((featureToggle) => removedTags.map((removedTag) => ({ type: FEATURE_UNTAGGED, - createdBy: userName, featureName: featureToggle.name, project: featureToggle.project, preData: removedTag, - createdByUserId: updatedByUserId, + createdBy: auditUser.username, + createdByUserId: auditUser.id, + ip: auditUser.ip, })), ); @@ -144,11 +148,7 @@ class FeatureTagService { ]); } - async createTagIfNeeded( - tag: ITag, - userName: string, - createdByUserId: number, - ): Promise { + async createTagIfNeeded(tag: ITag, auditUser: IAuditUser): Promise { try { await this.tagStore.getTag(tag.type, tag.value); } catch (error) { @@ -157,8 +157,9 @@ class FeatureTagService { await this.tagStore.createTag(tag); await this.eventService.storeEvent({ type: TAG_CREATED, - createdBy: userName, - createdByUserId, + createdBy: auditUser.username, + createdByUserId: auditUser.id, + ip: auditUser.ip, data: tag, }); } catch (err) { @@ -176,8 +177,7 @@ class FeatureTagService { async removeTag( featureName: string, tag: ITag, - userName: string, - removedByUserId: number, + auditUser: IAuditUser, ): Promise { const featureToggle = await this.featureToggleStore.get(featureName); const tags = @@ -185,8 +185,9 @@ class FeatureTagService { await this.featureTagStore.untagFeature(featureName, tag); await this.eventService.storeEvent({ type: FEATURE_UNTAGGED, - createdBy: userName, - createdByUserId: removedByUserId, + createdBy: auditUser.username, + createdByUserId: auditUser.id, + ip: auditUser.ip, 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 69b601a1e04..c67dccc8e0f 100644 --- a/src/lib/services/feature-type-service.ts +++ b/src/lib/services/feature-type-service.ts @@ -7,8 +7,7 @@ import type { } from '../types/stores/feature-type-store'; import NotFoundError from '../error/notfound-error'; import type EventService from '../features/events/event-service'; -import { FEATURE_TYPE_UPDATED, type IUser } from '../types'; -import { extractUsernameFromUser } from '../util'; +import { FeatureTypeUpdatedEvent, type IAuditUser } from '../types'; export default class FeatureTypeService { private featureTypeStore: IFeatureTypeStore; @@ -34,7 +33,7 @@ export default class FeatureTypeService { async updateLifetime( id: string, newLifetimeDays: number | null, - user: IUser, + auditUser: IAuditUser, ): Promise { // because our OpenAPI library does type coercion, any `null` values you // pass in get converted to `0`. @@ -54,13 +53,13 @@ export default class FeatureTypeService { ); } - await this.eventService.storeEvent({ - type: FEATURE_TYPE_UPDATED, - createdBy: extractUsernameFromUser(user), - createdByUserId: user.id, - data: { ...featureType, lifetimeDays: translatedLifetime }, - preData: featureType, - }); + await this.eventService.storeEvent( + new FeatureTypeUpdatedEvent({ + auditUser, + data: { ...featureType, lifetimeDays: translatedLifetime }, + preData: featureType, + }), + ); return result; } diff --git a/src/lib/services/group-service.ts b/src/lib/services/group-service.ts index 11f5e0d7281..0fd70cb4a6d 100644 --- a/src/lib/services/group-service.ts +++ b/src/lib/services/group-service.ts @@ -7,14 +7,18 @@ import type { IGroupRole, IGroupUser, } from '../types/group'; -import type { IUnleashConfig, IUnleashStores } from '../types'; +import { + GroupDeletedEvent, + GroupUpdatedEvent, + type IAuditUser, + type IUnleashConfig, + type IUnleashStores, +} from '../types'; import type { IGroupStore } from '../types/stores/group-store'; import type { Logger } from '../logger'; import BadDataError from '../error/bad-data-error'; import { GROUP_CREATED, - GROUP_DELETED, - GROUP_UPDATED, GROUP_USER_ADDED, GROUP_USER_REMOVED, type IBaseEvent, @@ -98,8 +102,7 @@ export class GroupService { async createGroup( group: ICreateGroupModel, - userName: string, - createdByUserId: number, + auditUser: IAuditUser, ): Promise { await this.validateGroup(group); @@ -109,15 +112,16 @@ export class GroupService { await this.groupStore.addUsersToGroup( newGroup.id, group.users, - userName, + auditUser.username, ); } const newUserIds = group.users?.map((g) => g.user.id); await this.eventService.storeEvent({ type: GROUP_CREATED, - createdBy: userName, - createdByUserId, + createdBy: auditUser.username, + createdByUserId: auditUser.id, + ip: auditUser.ip, data: { ...group, users: newUserIds }, }); @@ -126,8 +130,7 @@ export class GroupService { async updateGroup( group: IGroupModel, - userName: string, - createdByUserId: number, + auditUser: IAuditUser, ): Promise { const existingGroup = await this.groupStore.get(group.id); @@ -153,17 +156,17 @@ export class GroupService { (user) => !existingUserIds.includes(user.user.id), ), deletableUsers, - userName, + auditUser.username, ); const newUserIds = group.users.map((g) => g.user.id); - await this.eventService.storeEvent({ - type: GROUP_UPDATED, - createdBy: userName, - createdByUserId, - data: { ...newGroup, users: newUserIds }, - preData: { ...existingGroup, users: existingUserIds }, - }); + await this.eventService.storeEvent( + new GroupUpdatedEvent({ + data: { ...newGroup, users: newUserIds }, + preData: { ...existingGroup, users: existingUserIds }, + auditUser, + }), + ); return newGroup; } @@ -195,11 +198,7 @@ export class GroupService { return []; } - async deleteGroup( - id: number, - userName: string, - createdByUserId: number, - ): Promise { + async deleteGroup(id: number, auditUser: IAuditUser): Promise { const group = await this.groupStore.get(id); const existingUsers = await this.groupStore.getAllUsersByGroups([ @@ -209,12 +208,12 @@ export class GroupService { await this.groupStore.delete(id); - await this.eventService.storeEvent({ - type: GROUP_DELETED, - createdBy: userName, - createdByUserId, - preData: { ...group, users: existingUserIds }, - }); + await this.eventService.storeEvent( + new GroupDeletedEvent({ + preData: { ...group, users: existingUserIds }, + auditUser, + }), + ); } async validateGroup( diff --git a/src/lib/services/pat-service.ts b/src/lib/services/pat-service.ts index 8d91bf774e6..291bf8ef562 100644 --- a/src/lib/services/pat-service.ts +++ b/src/lib/services/pat-service.ts @@ -1,16 +1,19 @@ -import type { IUnleashConfig, IUnleashStores } from '../types'; +import { + type IAuditUser, + type IUnleashConfig, + type IUnleashStores, + PatCreatedEvent, + PatDeletedEvent, +} from '../types'; import type { Logger } from '../logger'; import type { IPatStore } from '../types/stores/pat-store'; -import { PAT_CREATED, PAT_DELETED } from '../types/events'; import crypto from 'crypto'; -import type { IUser } from '../types/user'; import BadDataError from '../error/bad-data-error'; import NameExistsError from '../error/name-exists-error'; import { OperationDeniedError } from '../error/operation-denied-error'; import { PAT_LIMIT } from '../util/constants'; import type EventService from '../features/events/event-service'; import type { CreatePatSchema, PatSchema } from '../openapi'; -import { extractUserIdFromUser, extractUsernameFromUser } from '../util'; export default class PatService { private config: IUnleashConfig; @@ -35,19 +38,19 @@ export default class PatService { async createPat( pat: CreatePatSchema, forUserId: number, - byUser: IUser, + auditUser: IAuditUser, ): Promise { await this.validatePat(pat, forUserId); const secret = this.generateSecretKey(); const newPat = await this.patStore.create(pat, secret, forUserId); - await this.eventService.storeEvent({ - type: PAT_CREATED, - createdBy: extractUsernameFromUser(byUser), - createdByUserId: extractUserIdFromUser(byUser), - data: { ...pat, secret: '***' }, - }); + await this.eventService.storeEvent( + new PatCreatedEvent({ + data: { ...pat, secret: '***' }, + auditUser, + }), + ); return { ...newPat, secret }; } @@ -59,16 +62,16 @@ export default class PatService { async deletePat( id: number, forUserId: number, - byUser: IUser, + auditUser: IAuditUser, ): Promise { const pat = await this.patStore.get(id); - await this.eventService.storeEvent({ - type: PAT_DELETED, - createdBy: extractUsernameFromUser(byUser), - createdByUserId: extractUserIdFromUser(byUser), - data: { ...pat, secret: '***' }, - }); + await this.eventService.storeEvent( + new PatDeletedEvent({ + data: { ...pat, secret: '***' }, + auditUser, + }), + ); return this.patStore.deleteForUser(id, forUserId); } diff --git a/src/lib/services/public-signup-token-service.ts b/src/lib/services/public-signup-token-service.ts index 4d9d1c97325..89b9129c7a7 100644 --- a/src/lib/services/public-signup-token-service.ts +++ b/src/lib/services/public-signup-token-service.ts @@ -1,9 +1,10 @@ import crypto from 'crypto'; import type { Logger } from '../logger'; import { + type IAuditUser, type IUnleashConfig, type IUnleashStores, - SYSTEM_USER, + SYSTEM_USER_AUDIT, } from '../types'; import type { IPublicSignupTokenStore } from '../types/stores/public-signup-token-store'; import type { PublicSignupTokenSchema } from '../openapi/spec/public-signup-token-schema'; @@ -80,14 +81,12 @@ export class PublicSignupTokenService { public async update( secret: string, { expiresAt, enabled }: { expiresAt?: Date; enabled?: boolean }, - createdBy: string, - createdByUserId: number, + auditUser: IAuditUser, ): Promise { const result = await this.store.update(secret, { expiresAt, enabled }); await this.eventService.storeEvent( new PublicSignupTokenUpdatedEvent({ - createdBy, - createdByUserId, + auditUser, data: { secret, enabled, expiresAt }, }), ); @@ -97,17 +96,20 @@ export class PublicSignupTokenService { public async addTokenUser( secret: string, createUser: CreateInvitedUserSchema, + auditUser: IAuditUser, ): Promise { const token = await this.get(secret); - const user = await this.userService.createUser({ - ...createUser, - rootRole: token.role.id, - }); + const user = await this.userService.createUser( + { + ...createUser, + rootRole: token.role.id, + }, + auditUser, + ); await this.store.addTokenUser(secret, user.id); await this.eventService.storeEvent( new PublicSignupTokenUserAddedEvent({ - createdBy: SYSTEM_USER.username, - createdByUserId: SYSTEM_USER.id, + auditUser: SYSTEM_USER_AUDIT, data: { secret, userId: user.id }, }), ); @@ -116,8 +118,7 @@ export class PublicSignupTokenService { public async createNewPublicSignupToken( tokenCreate: PublicSignupTokenCreateSchema, - createdBy: string, - createdByUserId: number, + auditUser: IAuditUser, ): Promise { const viewerRole = await this.roleStore.getRoleByName(RoleName.VIEWER); const secret = this.generateSecretKey(); @@ -131,15 +132,14 @@ export class PublicSignupTokenService { expiresAt: cappedDate, secret: secret, roleId: viewerRole ? viewerRole.id : -1, - createdBy: createdBy, + createdBy: auditUser.username, url: url, }; const token = await this.store.insert(newToken); await this.eventService.storeEvent( new PublicSignupTokenCreatedEvent({ - createdBy: createdBy, - createdByUserId, + auditUser, data: token, }), ); diff --git a/src/lib/services/setting-service.ts b/src/lib/services/setting-service.ts index fe2bbff2364..68f31e47123 100644 --- a/src/lib/services/setting-service.ts +++ b/src/lib/services/setting-service.ts @@ -8,6 +8,7 @@ import { SettingUpdatedEvent, } from '../types/events'; import type EventService from '../features/events/event-service'; +import type { IAuditUser } from '../types'; export default class SettingService { private config: IUnleashConfig; @@ -45,8 +46,7 @@ export default class SettingService { async insert( id: string, value: object, - createdBy: string, - createdByUserId: number, + auditUser: IAuditUser, hideEventDetails: boolean = true, ): Promise { const existingSettings = await this.settingStore.get(id); @@ -64,9 +64,8 @@ export default class SettingService { await this.eventService.storeEvent( new SettingUpdatedEvent( { - createdBy, data, - createdByUserId, + auditUser, }, preData, ), @@ -75,24 +74,18 @@ export default class SettingService { await this.settingStore.insert(id, value); await this.eventService.storeEvent( new SettingCreatedEvent({ - createdByUserId, - createdBy, + auditUser, data, }), ); } } - async delete( - id: string, - createdBy: string, - createdByUserId: number, - ): Promise { + async delete(id: string, auditUser: IAuditUser): Promise { await this.settingStore.delete(id); await this.eventService.storeEvent( new SettingDeletedEvent({ - createdByUserId, - createdBy, + auditUser, data: { id, }, diff --git a/src/lib/services/strategy-service.ts b/src/lib/services/strategy-service.ts index 3d822258a34..10dfa7c1814 100644 --- a/src/lib/services/strategy-service.ts +++ b/src/lib/services/strategy-service.ts @@ -8,6 +8,14 @@ import type { } from '../types/stores/strategy-store'; import NotFoundError from '../error/notfound-error'; import type EventService from '../features/events/event-service'; +import { + type IAuditUser, + StrategyCreatedEvent, + StrategyDeletedEvent, + StrategyDeprecatedEvent, + StrategyReactivatedEvent, + StrategyUpdatedEvent, +} from '../types'; const strategySchema = require('./strategy-schema'); const NameExistsError = require('../error/name-exists-error'); @@ -46,38 +54,36 @@ class StrategyService { async removeStrategy( strategyName: string, - userName: string, - userId: number, + auditUser: IAuditUser, ): Promise { const strategy = await this.strategyStore.get(strategyName); await this._validateEditable(strategy); await this.strategyStore.delete(strategyName); - await this.eventService.storeEvent({ - type: STRATEGY_DELETED, - createdBy: userName, - createdByUserId: userId, - data: { - name: strategyName, - }, - }); + await this.eventService.storeEvent( + new StrategyDeletedEvent({ + data: { + name: strategyName, + }, + auditUser, + }), + ); } async deprecateStrategy( strategyName: string, - userName: string, - userId: number, + auditUser: IAuditUser, ): Promise { if (await this.strategyStore.exists(strategyName)) { // Check existence await this.strategyStore.deprecateStrategy({ name: strategyName }); - await this.eventService.storeEvent({ - type: STRATEGY_DEPRECATED, - createdBy: userName, - createdByUserId: userId, - data: { - name: strategyName, - }, - }); + await this.eventService.storeEvent( + new StrategyDeprecatedEvent({ + data: { + name: strategyName, + }, + auditUser, + }), + ); } else { throw new NotFoundError( `Could not find strategy with name ${strategyName}`, @@ -87,54 +93,51 @@ class StrategyService { async reactivateStrategy( strategyName: string, - userName: string, - userId: number, + auditUser: IAuditUser, ): 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, - }, - }); + await this.eventService.storeEvent( + new StrategyReactivatedEvent({ + data: { + name: strategyName, + }, + auditUser, + }), + ); } async createStrategy( value: IMinimalStrategy, - userName: string, - userId: number, + auditUser: IAuditUser, ): Promise { const strategy = await strategySchema.validateAsync(value); strategy.deprecated = false; await this._validateStrategyName(strategy); await this.strategyStore.createStrategy(strategy); - await this.eventService.storeEvent({ - type: STRATEGY_CREATED, - createdBy: userName, - data: strategy, - createdByUserId: userId, - }); + await this.eventService.storeEvent( + new StrategyCreatedEvent({ + data: strategy, + auditUser, + }), + ); return this.strategyStore.get(strategy.name); } async updateStrategy( input: IMinimalStrategy, - userName: string, - userId: number, + auditUser: IAuditUser, ): Promise { const value = await strategySchema.validateAsync(input); const strategy = await this.strategyStore.get(input.name); await this._validateEditable(strategy); await this.strategyStore.updateStrategy(value); - await this.eventService.storeEvent({ - type: STRATEGY_UPDATED, - createdBy: userName, - data: value, - createdByUserId: userId, - }); + await this.eventService.storeEvent( + new StrategyUpdatedEvent({ + data: value, + auditUser, + }), + ); } private _validateStrategyName( diff --git a/src/lib/services/tag-service.ts b/src/lib/services/tag-service.ts index 8e5a27109f8..41990de4b70 100644 --- a/src/lib/services/tag-service.ts +++ b/src/lib/services/tag-service.ts @@ -1,12 +1,13 @@ import { tagSchema } from './tag-schema'; import NameExistsError from '../error/name-exists-error'; -import { TAG_CREATED, TAG_DELETED } from '../types/events'; +import { TagCreatedEvent, TagDeletedEvent } from '../types/events'; import type { Logger } from '../logger'; import type { IUnleashStores } from '../types/stores'; import type { IUnleashConfig } from '../types/option'; import type { ITagStore } from '../types/stores/tag-store'; import type { ITag } from '../types/model'; import type EventService from '../features/events/event-service'; +import type { IAuditUser } from '../types'; export default class TagService { private tagStore: ITagStore; @@ -50,31 +51,27 @@ export default class TagService { return data; } - async createTag( - tag: ITag, - userName: string, - userId: number, - ): Promise { + async createTag(tag: ITag, auditUser: IAuditUser): Promise { const data = await this.validate(tag); await this.tagStore.createTag(data); - await this.eventService.storeEvent({ - type: TAG_CREATED, - createdBy: userName, - createdByUserId: userId, - data, - }); + await this.eventService.storeEvent( + new TagCreatedEvent({ + data, + auditUser, + }), + ); return data; } - async deleteTag(tag: ITag, userName: string, userId): Promise { + async deleteTag(tag: ITag, auditUser: IAuditUser): Promise { await this.tagStore.delete(tag); - await this.eventService.storeEvent({ - type: TAG_DELETED, - createdBy: userName, - createdByUserId: userId, - data: tag, - }); + await this.eventService.storeEvent( + new TagDeletedEvent({ + data: tag, + auditUser, + }), + ); } } diff --git a/src/lib/services/user-service.test.ts b/src/lib/services/user-service.test.ts index d5ea7e6be4d..2884a11364b 100644 --- a/src/lib/services/user-service.test.ts +++ b/src/lib/services/user-service.test.ts @@ -16,6 +16,7 @@ import SettingService from './setting-service'; import FakeSettingStore from '../../test/fixtures/fake-setting-store'; import EventService from '../features/events/event-service'; import FakeFeatureTagStore from '../../test/fixtures/fake-feature-tag-store'; +import { extractAuditInfoFromUser } from '../util'; const config: IUnleashConfig = createTestConfig(); @@ -58,7 +59,7 @@ test('Should create new user', async () => { username: 'test', rootRole: 1, }, - systemUser, + extractAuditInfoFromUser(systemUser), ); const storedUser = await userStore.get(user.id); const allUsers = await userStore.getAll(); diff --git a/src/lib/services/user-service.ts b/src/lib/services/user-service.ts index 04cffe10650..95a7b2e0054 100644 --- a/src/lib/services/user-service.ts +++ b/src/lib/services/user-service.ts @@ -4,7 +4,11 @@ import Joi from 'joi'; import type { URL } from 'url'; import type { Logger } from '../logger'; -import User, { type IUser, type IUserWithRootRole } from '../types/user'; +import User, { + type IAuditUser, + type IUser, + type IUserWithRootRole, +} from '../types/user'; import isEmail from '../util/is-email'; import type { AccessService } from './access-service'; import type ResetTokenService from './reset-token-service'; @@ -18,8 +22,8 @@ import type { IUnleashStores } from '../types/stores'; import PasswordUndefinedError from '../error/password-undefined'; import { UserCreatedEvent, - UserUpdatedEvent, UserDeletedEvent, + UserUpdatedEvent, } from '../types/events'; import type { IUserStore } from '../types/stores/user-store'; import { RoleName } from '../types/model'; @@ -33,7 +37,7 @@ import type { TokenUserSchema } from '../openapi/spec/token-user-schema'; import PasswordMismatch from '../error/password-mismatch'; import type EventService from '../features/events/event-service'; -import { SYSTEM_USER } from '../types'; +import { SYSTEM_USER, SYSTEM_USER_AUDIT } from '../types'; export interface ICreateUser { name?: string; @@ -203,7 +207,7 @@ class UserService { async createUser( { username, email, name, password, rootRole }: ICreateUser, - updatedBy?: IUser, + auditUser: IAuditUser = SYSTEM_USER_AUDIT, ): Promise { if (!username && !email) { throw new BadDataError('You must specify username or email'); @@ -235,8 +239,7 @@ class UserService { await this.eventService.storeEvent( new UserCreatedEvent({ - createdBy: this.getCreatedBy(updatedBy), - createdByUserId: user.id, + auditUser, userCreated, }), ); @@ -244,13 +247,9 @@ class UserService { return userCreated; } - private getCreatedBy(updatedBy: IUser = new User(SYSTEM_USER)): string { - return updatedBy.username || updatedBy.email; - } - async updateUser( { id, name, email, rootRole }: IUpdateUser, - updatedBy?: IUser, + auditUser: IAuditUser, ): Promise { const preUser = await this.getUser(id); @@ -276,17 +275,16 @@ class UserService { await this.eventService.storeEvent( new UserUpdatedEvent({ - createdBy: this.getCreatedBy(updatedBy), + auditUser, preUser: preUser, postUser: storedUser, - createdByUserId: user.id, }), ); return storedUser; } - async deleteUser(userId: number, updatedBy?: IUser): Promise { + async deleteUser(userId: number, auditUser: IAuditUser): Promise { const user = await this.getUser(userId); await this.accessService.wipeUserPermissions(userId); await this.sessionService.deleteSessionsForUser(userId); @@ -295,9 +293,8 @@ class UserService { await this.eventService.storeEvent( new UserDeletedEvent({ - createdBy: this.getCreatedBy(updatedBy), deletedUser: user, - createdByUserId: updatedBy?.id || -1337, + auditUser, }), ); } @@ -366,11 +363,14 @@ class UserService { } catch (e) { // User does not exists. Create if 'autoCreate' is enabled if (autoCreate) { - user = await this.createUser({ - email, - name, - rootRole: rootRole || RoleName.EDITOR, - }); + user = await this.createUser( + { + email, + name, + rootRole: rootRole || RoleName.EDITOR, + }, + SYSTEM_USER_AUDIT, + ); } else { throw e; } diff --git a/src/lib/types/core.ts b/src/lib/types/core.ts index c6084713609..297eaf822ad 100644 --- a/src/lib/types/core.ts +++ b/src/lib/types/core.ts @@ -3,7 +3,7 @@ import type EventEmitter from 'events'; import type * as https from 'https'; import type * as http from 'http'; import type User from './user'; -import type { IUser } from './user'; +import type { IAuditUser, IUser } from './user'; import type { IUnleashConfig } from './option'; import type { IUnleashStores } from './stores'; import type { IUnleashServices } from './services'; @@ -41,4 +41,16 @@ export const ADMIN_TOKEN_USER: Omit = { username: 'unleash_admin_token', }; +export const SYSTEM_USER_AUDIT: IAuditUser = { + id: SYSTEM_USER.id, + username: SYSTEM_USER.username!, + ip: '', +}; + +export const TEST_AUDIT_USER: IAuditUser = { + id: -9999, + username: 'test@example.com', + ip: '999.999.999.999', +}; + export const SYSTEM_USER_ID: number = SYSTEM_USER.id; diff --git a/src/lib/types/events.ts b/src/lib/types/events.ts index 2cd97060112..eb3ef61e750 100644 --- a/src/lib/types/events.ts +++ b/src/lib/types/events.ts @@ -1,8 +1,7 @@ -import { extractUsernameFromUser } from '../util'; import type { IApiUser } from './api-user'; import type { FeatureToggle, IStrategyConfig, ITag, IVariant } from './model'; import type { IApiToken } from './models/api-token'; -import type { IUser, IUserWithRootRole } from './user'; +import type { IAuditUser, IUser, IUserWithRootRole } from './user'; export const APPLICATION_CREATED = 'application-created' as const; @@ -348,6 +347,7 @@ export interface IBaseEvent { project?: string; environment?: string; featureName?: string; + ip?: string; data?: any; preData?: any; tags?: ITag[]; @@ -381,22 +381,16 @@ class BaseEvent implements IBaseEvent { readonly createdByUserId: number; + readonly ip: string; /** * @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 + * @param auditUser User info used to track which user performed the action. Includes username (email or username), userId and ip */ - constructor( - type: IEventType, - createdBy: string | IUser, - createdByUserId: number, - ) { + constructor(type: IEventType, auditUser: IAuditUser) { this.type = type; - this.createdBy = - typeof createdBy === 'string' - ? createdBy - : extractUsernameFromUser(createdBy); - this.createdByUserId = createdByUserId; + this.createdBy = auditUser.username || 'unknown'; + this.createdByUserId = auditUser.id || -1337; + this.ip = auditUser.ip || ''; } } @@ -405,21 +399,13 @@ export class FeatureStaleEvent extends BaseEvent { readonly featureName: string; - /** - * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization - */ constructor(p: { stale: boolean; project: string; featureName: string; - createdBy: string | IUser; - createdByUserId: number; + auditUser: IAuditUser; }) { - super( - p.stale ? FEATURE_STALE_ON : FEATURE_STALE_OFF, - p.createdBy, - p.createdByUserId, - ); + super(p.stale ? FEATURE_STALE_ON : FEATURE_STALE_OFF, p.auditUser); this.project = p.project; this.featureName = p.featureName; } @@ -432,23 +418,18 @@ export class FeatureEnvironmentEvent extends BaseEvent { readonly environment: string; - /** - * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization - */ constructor(p: { enabled: boolean; project: string; featureName: string; environment: string; - createdBy: string | IUser; - createdByUserId: number; + auditUser: IAuditUser; }) { super( p.enabled ? FEATURE_ENVIRONMENT_ENABLED : FEATURE_ENVIRONMENT_DISABLED, - p.createdBy, - p.createdByUserId, + p.auditUser, ); this.project = p.project; this.featureName = p.featureName; @@ -467,19 +448,15 @@ export class StrategiesOrderChangedEvent extends BaseEvent { readonly preData: StrategyIds; - /** - * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization - */ constructor(p: { project: string; featureName: string; environment: string; - createdBy: string | IUser; data: StrategyIds; preData: StrategyIds; - createdByUserId: number; + auditUser: IAuditUser; }) { - super(STRATEGY_ORDER_CHANGED, p.createdBy, p.createdByUserId); + super(STRATEGY_ORDER_CHANGED, p.auditUser); const { project, featureName, environment, data, preData } = p; this.project = project; this.featureName = featureName; @@ -498,18 +475,14 @@ export class FeatureVariantEvent extends BaseEvent { readonly preData: { variants: IVariant[] }; - /** - * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization - */ constructor(p: { project: string; featureName: string; - createdBy: string | IUser; newVariants: IVariant[]; oldVariants: IVariant[]; - createdByUserId: number; + auditUser: IAuditUser; }) { - super(FEATURE_VARIANTS_UPDATED, p.createdBy, p.createdByUserId); + super(FEATURE_VARIANTS_UPDATED, p.auditUser); this.project = p.project; this.featureName = p.featureName; this.data = { variants: p.newVariants }; @@ -529,22 +502,16 @@ export class EnvironmentVariantEvent extends BaseEvent { readonly preData: { variants: IVariant[] }; /** - * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization */ constructor(p: { featureName: string; environment: string; project: string; - createdBy: string | IUser; newVariants: IVariant[]; oldVariants: IVariant[]; - createdByUserId: number; + auditUser: IAuditUser; }) { - super( - FEATURE_ENVIRONMENT_VARIANTS_UPDATED, - p.createdBy, - p.createdByUserId, - ); + super(FEATURE_ENVIRONMENT_VARIANTS_UPDATED, p.auditUser); this.featureName = p.featureName; this.environment = p.environment; this.project = p.project; @@ -552,7 +519,61 @@ export class EnvironmentVariantEvent extends BaseEvent { this.preData = { variants: p.oldVariants }; } } +export class ProjectCreatedEvent extends BaseEvent { + readonly project: string; + readonly data: any; + constructor(eventData: { + data: any; + project: string; + auditUser: IAuditUser; + }) { + super(PROJECT_CREATED, eventData.auditUser); + this.project = eventData.project; + this.data = eventData.data; + } +} +export class ProjectUpdatedEvent extends BaseEvent { + readonly project: string; + readonly data: any; + readonly preData: any; + constructor(eventData: { + data: any; + preData: any; + project: string; + auditUser: IAuditUser; + }) { + super(PROJECT_UPDATED, eventData.auditUser); + this.project = eventData.project; + this.data = eventData.data; + this.preData = eventData.preData; + } +} + +export class ProjectDeletedEvent extends BaseEvent { + readonly project: string; + constructor(eventData: { + project: string; + auditUser: IAuditUser; + }) { + super(PROJECT_DELETED, eventData.auditUser); + this.project = eventData.project; + } +} + +export class RoleUpdatedEvent extends BaseEvent { + readonly data: any; + readonly preData: any; + constructor(eventData: { + auditUser: IAuditUser; + data: any; + preData: any; + }) { + super(ROLE_UPDATED, eventData.auditUser); + this.data = eventData.data; + this.preData = eventData.preData; + } +} export class FeatureChangeProjectEvent extends BaseEvent { readonly project: string; @@ -563,17 +584,13 @@ export class FeatureChangeProjectEvent extends BaseEvent { newProject: string; }; - /** - * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization - */ constructor(p: { oldProject: string; newProject: string; featureName: string; - createdBy: string | IUser; - createdByUserId: number; + auditUser: IAuditUser; }) { - super(FEATURE_PROJECT_CHANGE, p.createdBy, p.createdByUserId); + super(FEATURE_PROJECT_CHANGE, p.auditUser); const { newProject, oldProject, featureName } = p; this.project = newProject; this.featureName = featureName; @@ -588,17 +605,13 @@ export class FeatureCreatedEvent extends BaseEvent { readonly data: FeatureToggle; - /** - * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization - */ constructor(p: { project: string; featureName: string; - createdBy: string | IUser; data: FeatureToggle; - createdByUserId: number; + auditUser: IAuditUser; }) { - super(FEATURE_CREATED, p.createdBy, p.createdByUserId); + super(FEATURE_CREATED, p.auditUser); const { project, featureName, data } = p; this.project = project; this.featureName = featureName; @@ -606,21 +619,126 @@ export class FeatureCreatedEvent extends BaseEvent { } } +export class FeatureUpdatedEvent extends BaseEvent { + readonly data: any; + readonly featureName: string; + readonly project: string; + constructor(eventData: { + project: string; + featureName: string; + data: any; + auditUser: IAuditUser; + }) { + super(FEATURE_UPDATED, eventData.auditUser); + this.data = eventData.data; + this.project = eventData.project; + this.featureName = eventData.featureName; + } +} + +export class FeatureTaggedEvent extends BaseEvent { + readonly data: any; + readonly featureName: string; + readonly project: string; + constructor(eventData: { + project: string; + featureName: string; + data: any; + auditUser: IAuditUser; + }) { + super(FEATURE_TAGGED, eventData.auditUser); + this.project = eventData.project; + this.featureName = eventData.featureName; + this.data = eventData.data; + } +} + +export class FeatureTypeUpdatedEvent extends BaseEvent { + readonly data: any; + readonly preData: any; + constructor(eventData: { + data: any; + preData: any; + auditUser: IAuditUser; + }) { + super(FEATURE_TYPE_UPDATED, eventData.auditUser); + this.data = eventData.data; + this.preData = eventData.preData; + } +} +export class FeatureDependencyAddedEvent extends BaseEvent { + readonly project: string; + readonly featureName: string; + readonly data: any; + constructor(eventData: { + project: string; + featureName: string; + auditUser: IAuditUser; + data: any; + }) { + super('feature-dependency-added', eventData.auditUser); + this.project = eventData.project; + this.featureName = eventData.featureName; + this.data = eventData.data; + } +} + +export class FeatureDependencyRemovedEvent extends BaseEvent { + readonly project: string; + readonly featureName: string; + readonly data: any; + constructor(eventData: { + project: string; + featureName: string; + auditUser: IAuditUser; + data: any; + }) { + super('feature-dependency-removed', eventData.auditUser); + this.project = eventData.project; + this.featureName = eventData.featureName; + this.data = eventData.data; + } +} +export class FeatureDependenciesRemovedEvent extends BaseEvent { + readonly project: string; + readonly featureName: string; + + constructor(eventData: { + project: string; + featureName: string; + auditUser: IAuditUser; + }) { + super('feature-dependencies-removed', eventData.auditUser); + this.project = eventData.project; + this.featureName = eventData.featureName; + } +} + +export class FeaturesImportedEvent extends BaseEvent { + readonly project: string; + readonly environment: string; + constructor(eventData: { + project: string; + environment: string; + auditUser: IAuditUser; + }) { + super(FEATURES_IMPORTED, eventData.auditUser); + this.project = eventData.project; + this.environment = eventData.environment; + } +} + export class FeatureArchivedEvent extends BaseEvent { readonly project: string; readonly featureName: string; - /** - * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization - */ constructor(p: { project: string; featureName: string; - createdBy: string | IUser; - createdByUserId: number; + auditUser: IAuditUser; }) { - super(FEATURE_ARCHIVED, p.createdBy, p.createdByUserId); + super(FEATURE_ARCHIVED, p.auditUser); const { project, featureName } = p; this.project = project; this.featureName = featureName; @@ -633,15 +751,13 @@ export class FeatureRevivedEvent extends BaseEvent { readonly featureName: string; /** - * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization */ constructor(p: { project: string; featureName: string; - createdBy: string | IUser; - createdByUserId: number; + auditUser: IAuditUser; }) { - super(FEATURE_REVIVED, p.createdBy, p.createdByUserId); + super(FEATURE_REVIVED, p.auditUser); const { project, featureName } = p; this.project = project; this.featureName = featureName; @@ -658,17 +774,15 @@ export class FeatureDeletedEvent extends BaseEvent { readonly tags: ITag[]; /** - * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization */ constructor(p: { project: string; featureName: string; preData: FeatureToggle; - createdBy: string | IUser; tags: ITag[]; - createdByUserId: number; + auditUser: IAuditUser; }) { - super(FEATURE_DELETED, p.createdBy, p.createdByUserId); + super(FEATURE_DELETED, p.auditUser); const { project, featureName, preData } = p; this.project = project; this.featureName = featureName; @@ -687,17 +801,15 @@ export class FeatureMetadataUpdateEvent extends BaseEvent { readonly preData: FeatureToggle; /** - * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization */ constructor(p: { featureName: string; - createdBy: string | IUser; project: string; data: FeatureToggle; preData: FeatureToggle; - createdByUserId: number; + auditUser: IAuditUser; }) { - super(FEATURE_METADATA_UPDATED, p.createdBy, p.createdByUserId); + super(FEATURE_METADATA_UPDATED, p.auditUser); const { project, featureName, data, preData } = p; this.project = project; this.featureName = featureName; @@ -716,17 +828,15 @@ export class FeatureStrategyAddEvent extends BaseEvent { readonly data: IStrategyConfig; /** - * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization */ constructor(p: { project: string; featureName: string; environment: string; - createdBy: string | IUser; data: IStrategyConfig; - createdByUserId: number; + auditUser: IAuditUser; }) { - super(FEATURE_STRATEGY_ADD, p.createdBy, p.createdByUserId); + super(FEATURE_STRATEGY_ADD, p.auditUser); const { project, featureName, environment, data } = p; this.project = project; this.featureName = featureName; @@ -747,18 +857,16 @@ export class FeatureStrategyUpdateEvent extends BaseEvent { readonly preData: IStrategyConfig; /** - * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization */ constructor(p: { project: string; featureName: string; environment: string; - createdBy: string | IUser; data: IStrategyConfig; preData: IStrategyConfig; - createdByUserId: number; + auditUser: IAuditUser; }) { - super(FEATURE_STRATEGY_UPDATE, p.createdBy, p.createdByUserId); + super(FEATURE_STRATEGY_UPDATE, p.auditUser); const { project, featureName, environment, data, preData } = p; this.project = project; this.featureName = featureName; @@ -777,18 +885,14 @@ export class FeatureStrategyRemoveEvent extends BaseEvent { readonly preData: IStrategyConfig; - /** - * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization - */ constructor(p: { project: string; featureName: string; environment: string; - createdBy: string | IUser; preData: IStrategyConfig; - createdByUserId: number; + auditUser: IAuditUser; }) { - super(FEATURE_STRATEGY_REMOVE, p.createdBy, p.createdByUserId); + super(FEATURE_STRATEGY_REMOVE, p.auditUser); const { project, featureName, environment, preData } = p; this.project = project; this.featureName = featureName; @@ -797,6 +901,62 @@ export class FeatureStrategyRemoveEvent extends BaseEvent { } } +export class FeatureFavoritedEvent extends BaseEvent { + readonly featureName: string; + readonly data: any; + constructor(eventData: { + featureName: string; + data: any; + auditUser: IAuditUser; + }) { + super(FEATURE_FAVORITED, eventData.auditUser); + this.data = eventData.data; + this.featureName = eventData.featureName; + } +} + +export class ProjectFavoritedEvent extends BaseEvent { + readonly project: string; + readonly data: any; + constructor(eventData: { + project: string; + data: any; + auditUser: IAuditUser; + }) { + super(PROJECT_FAVORITED, eventData.auditUser); + this.data = eventData.data; + this.project = eventData.project; + } +} + +export class FeatureUnfavoritedEvent extends BaseEvent { + readonly featureName: string; + readonly data: any; + constructor(eventData: { + featureName: string; + data: any; + auditUser: IAuditUser; + }) { + super(FEATURE_UNFAVORITED, eventData.auditUser); + this.data = eventData.data; + this.featureName = eventData.featureName; + } +} + +export class ProjectUnfavoritedEvent extends BaseEvent { + readonly project: string; + readonly data: any; + constructor(eventData: { + project: string; + data: any; + auditUser: IAuditUser; + }) { + super(PROJECT_UNFAVORITED, eventData.auditUser); + this.data = eventData.data; + this.project = eventData.project; + } +} + export class ProjectUserAddedEvent extends BaseEvent { readonly project: string; @@ -805,15 +965,13 @@ export class ProjectUserAddedEvent extends BaseEvent { readonly preData: any; /** - * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization */ constructor(p: { project: string; - createdBy: string | IUser; data: any; - createdByUserId: number; + auditUser: IAuditUser; }) { - super(PROJECT_USER_ADDED, p.createdBy, p.createdByUserId); + super(PROJECT_USER_ADDED, p.auditUser); const { project, data } = p; this.project = project; this.data = data; @@ -828,16 +986,12 @@ export class ProjectUserRemovedEvent extends BaseEvent { readonly preData: any; - /** - * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization - */ constructor(p: { project: string; - createdBy: string | IUser; preData: any; - createdByUserId: number; + auditUser: IAuditUser; }) { - super(PROJECT_USER_REMOVED, p.createdBy, p.createdByUserId); + super(PROJECT_USER_REMOVED, p.auditUser); const { project, preData } = p; this.project = project; this.data = null; @@ -852,21 +1006,13 @@ export class ProjectUserUpdateRoleEvent extends BaseEvent { readonly preData: any; - /** - * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization - */ constructor(eventData: { project: string; - createdBy: string | IUser; data: any; preData: any; - createdByUserId: number; + auditUser: IAuditUser; }) { - super( - PROJECT_USER_ROLE_CHANGED, - eventData.createdBy, - eventData.createdByUserId, - ); + super(PROJECT_USER_ROLE_CHANGED, eventData.auditUser); const { project, data, preData } = eventData; this.project = project; this.data = data; @@ -881,16 +1027,12 @@ export class ProjectGroupAddedEvent extends BaseEvent { readonly preData: any; - /** - * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization - */ constructor(p: { project: string; - createdBy: string | IUser; data: any; - createdByUserId: number; + auditUser: IAuditUser; }) { - super(PROJECT_GROUP_ADDED, p.createdBy, p.createdByUserId); + super(PROJECT_GROUP_ADDED, p.auditUser); const { project, data } = p; this.project = project; this.data = data; @@ -906,15 +1048,13 @@ export class ProjectGroupRemovedEvent extends BaseEvent { readonly preData: any; /** - * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization */ constructor(p: { project: string; - createdBy: string | IUser; preData: any; - createdByUserId: number; + auditUser: IAuditUser; }) { - super(PROJECT_GROUP_REMOVED, p.createdBy, p.createdByUserId); + super(PROJECT_GROUP_REMOVED, p.auditUser); const { project, preData } = p; this.project = project; this.data = null; @@ -930,20 +1070,14 @@ export class ProjectGroupUpdateRoleEvent extends BaseEvent { readonly preData: any; /** - * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization */ constructor(eventData: { project: string; - createdBy: string | IUser; data: any; preData: any; - createdByUserId: number; + auditUser: IAuditUser; }) { - super( - PROJECT_GROUP_ROLE_CHANGED, - eventData.createdBy, - eventData.createdByUserId, - ); + super(PROJECT_GROUP_ROLE_CHANGED, eventData.auditUser); const { project, data, preData } = eventData; this.project = project; this.data = data; @@ -958,16 +1092,12 @@ export class ProjectAccessAddedEvent extends BaseEvent { readonly preData: any; - /** - * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization - */ constructor(p: { project: string; - createdBy: string | IUser; data: any; - createdByUserId: number; + auditUser: IAuditUser; }) { - super(PROJECT_ACCESS_ADDED, p.createdBy, p.createdByUserId); + super(PROJECT_ACCESS_ADDED, p.auditUser); const { project, data } = p; this.project = project; this.data = data; @@ -982,21 +1112,13 @@ export class ProjectAccessUserRolesUpdated extends BaseEvent { readonly preData: any; - /** - * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization - */ constructor(p: { project: string; - createdBy: string | IUser; data: any; preData: any; - createdByUserId: number; + auditUser: IAuditUser; }) { - super( - PROJECT_ACCESS_USER_ROLES_UPDATED, - p.createdBy, - p.createdByUserId, - ); + super(PROJECT_ACCESS_USER_ROLES_UPDATED, p.auditUser); const { project, data, preData } = p; this.project = project; this.data = data; @@ -1011,21 +1133,13 @@ export class ProjectAccessGroupRolesUpdated extends BaseEvent { readonly preData: any; - /** - * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization - */ constructor(p: { project: string; - createdBy: string | IUser; data: any; preData: any; - createdByUserId: number; + auditUser: IAuditUser; }) { - super( - PROJECT_ACCESS_GROUP_ROLES_UPDATED, - p.createdBy, - p.createdByUserId, - ); + super(PROJECT_ACCESS_GROUP_ROLES_UPDATED, p.auditUser); const { project, data, preData } = p; this.project = project; this.data = data; @@ -1040,20 +1154,12 @@ export class ProjectAccessUserRolesDeleted extends BaseEvent { readonly preData: any; - /** - * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization - */ constructor(p: { project: string; - createdBy: string | IUser; preData: any; - createdByUserId: number; + auditUser: IAuditUser; }) { - super( - PROJECT_ACCESS_USER_ROLES_DELETED, - p.createdBy, - p.createdByUserId, - ); + super(PROJECT_ACCESS_USER_ROLES_DELETED, p.auditUser); const { project, preData } = p; this.project = project; this.data = null; @@ -1068,20 +1174,12 @@ export class ProjectAccessGroupRolesDeleted extends BaseEvent { readonly preData: any; - /** - * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization - */ constructor(p: { project: string; - createdBy: string | IUser; preData: any; - createdByUserId: number; + auditUser: IAuditUser; }) { - super( - PROJECT_ACCESS_GROUP_ROLES_DELETED, - p.createdBy, - p.createdByUserId, - ); + super(PROJECT_ACCESS_GROUP_ROLES_DELETED, p.auditUser); const { project, preData } = p; this.project = project; this.data = null; @@ -1092,15 +1190,11 @@ export class ProjectAccessGroupRolesDeleted extends BaseEvent { export class SettingCreatedEvent extends BaseEvent { readonly data: any; - /** - * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization - */ constructor(eventData: { - createdBy: string | IUser; data: any; - createdByUserId: number; + auditUser: IAuditUser; }) { - super(SETTING_CREATED, eventData.createdBy, eventData.createdByUserId); + super(SETTING_CREATED, eventData.auditUser); this.data = eventData.data; } } @@ -1108,15 +1202,11 @@ export class SettingCreatedEvent extends BaseEvent { export class SettingDeletedEvent extends BaseEvent { readonly data: any; - /** - * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization - */ constructor(eventData: { - createdBy: string | IUser; data: any; - createdByUserId: number; + auditUser: IAuditUser; }) { - super(SETTING_DELETED, eventData.createdBy, eventData.createdByUserId); + super(SETTING_DELETED, eventData.auditUser); this.data = eventData.data; } } @@ -1125,18 +1215,14 @@ export class SettingUpdatedEvent extends BaseEvent { readonly data: any; readonly preData: any; - /** - * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization - */ constructor( eventData: { - createdBy: string | IUser; data: any; - createdByUserId: number; + auditUser: IAuditUser; }, preData: any, ) { - super(SETTING_UPDATED, eventData.createdBy, eventData.createdByUserId); + super(SETTING_UPDATED, eventData.auditUser); this.data = eventData.data; this.preData = preData; } @@ -1145,19 +1231,11 @@ export class SettingUpdatedEvent extends BaseEvent { export class PublicSignupTokenCreatedEvent extends BaseEvent { readonly data: any; - /** - * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization - */ constructor(eventData: { - createdBy: string | IUser; data: any; - createdByUserId: number; + auditUser: IAuditUser; }) { - super( - PUBLIC_SIGNUP_TOKEN_CREATED, - eventData.createdBy, - eventData.createdByUserId, - ); + super(PUBLIC_SIGNUP_TOKEN_CREATED, eventData.auditUser); this.data = eventData.data; } } @@ -1165,19 +1243,11 @@ export class PublicSignupTokenCreatedEvent extends BaseEvent { export class PublicSignupTokenUpdatedEvent extends BaseEvent { readonly data: any; - /** - * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization - */ constructor(eventData: { - createdBy: string | IUser; data: any; - createdByUserId: number; + auditUser: IAuditUser; }) { - super( - PUBLIC_SIGNUP_TOKEN_TOKEN_UPDATED, - eventData.createdBy, - eventData.createdByUserId, - ); + super(PUBLIC_SIGNUP_TOKEN_TOKEN_UPDATED, eventData.auditUser); this.data = eventData.data; } } @@ -1185,19 +1255,11 @@ export class PublicSignupTokenUpdatedEvent extends BaseEvent { export class PublicSignupTokenUserAddedEvent extends BaseEvent { readonly data: any; - /** - * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization - */ constructor(eventData: { - createdBy: string | IUser; data: any; - createdByUserId: number; + auditUser: IAuditUser; }) { - super( - PUBLIC_SIGNUP_TOKEN_USER_ADDED, - eventData.createdBy, - eventData.createdByUserId, - ); + super(PUBLIC_SIGNUP_TOKEN_USER_ADDED, eventData.auditUser); this.data = eventData.data; } } @@ -1209,19 +1271,11 @@ export class ApiTokenCreatedEvent extends BaseEvent { readonly project: string; - /** - * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization - */ constructor(eventData: { - createdBy: string | IUser; apiToken: Omit; - createdByUserId: number; + auditUser: IAuditUser; }) { - super( - API_TOKEN_CREATED, - eventData.createdBy, - eventData.createdByUserId, - ); + super(API_TOKEN_CREATED, eventData.auditUser); this.data = eventData.apiToken; this.environment = eventData.apiToken.environment; this.project = eventData.apiToken.project; @@ -1235,19 +1289,11 @@ export class ApiTokenDeletedEvent extends BaseEvent { readonly project: string; - /** - * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization - */ constructor(eventData: { - createdBy: string | IUser; apiToken: Omit; - createdByUserId: number; + auditUser: IAuditUser; }) { - super( - API_TOKEN_DELETED, - eventData.createdBy, - eventData.createdByUserId, - ); + super(API_TOKEN_DELETED, eventData.auditUser); this.preData = eventData.apiToken; this.environment = eventData.apiToken.environment; this.project = eventData.apiToken.project; @@ -1263,20 +1309,12 @@ export class ApiTokenUpdatedEvent extends BaseEvent { readonly project: string; - /** - * @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization - */ constructor(eventData: { - createdBy: string | IUser; previousToken: Omit; apiToken: Omit; - createdByUserId: number; + auditUser: IAuditUser; }) { - super( - API_TOKEN_UPDATED, - eventData.createdBy, - eventData.createdByUserId, - ); + super(API_TOKEN_UPDATED, eventData.auditUser); this.preData = eventData.previousToken; this.data = eventData.apiToken; this.environment = eventData.apiToken.environment; @@ -1292,13 +1330,9 @@ export class PotentiallyStaleOnEvent extends BaseEvent { constructor(eventData: { featureName: string; project: string; - createdByUserId: number; + auditUser: IAuditUser; }) { - super( - FEATURE_POTENTIALLY_STALE_ON, - 'unleash-system', - eventData.createdByUserId, - ); + super(FEATURE_POTENTIALLY_STALE_ON, eventData.auditUser); this.featureName = eventData.featureName; this.project = eventData.project; } @@ -1308,11 +1342,10 @@ export class UserCreatedEvent extends BaseEvent { readonly data: IUserEventData; constructor(eventData: { - createdBy: string | IUser; userCreated: IUserEventData; - createdByUserId: number; + auditUser: IAuditUser; }) { - super(USER_CREATED, eventData.createdBy, eventData.createdByUserId); + super(USER_CREATED, eventData.auditUser); this.data = mapUserToData(eventData.userCreated); } } @@ -1322,12 +1355,11 @@ export class UserUpdatedEvent extends BaseEvent { readonly preData: IUserEventData; constructor(eventData: { - createdBy: string | IUser; preUser: IUserEventData; postUser: IUserEventData; - createdByUserId: number; + auditUser: IAuditUser; }) { - super(USER_UPDATED, eventData.createdBy, eventData.createdByUserId); + super(USER_UPDATED, eventData.auditUser); this.preData = mapUserToData(eventData.preUser); this.data = mapUserToData(eventData.postUser); } @@ -1337,15 +1369,326 @@ export class UserDeletedEvent extends BaseEvent { readonly preData: IUserEventData; constructor(eventData: { - createdBy: string | IUser; deletedUser: IUserEventData; - createdByUserId: number; + auditUser: IAuditUser; }) { - super(USER_DELETED, eventData.createdBy, eventData.createdByUserId); + super(USER_DELETED, eventData.auditUser); this.preData = mapUserToData(eventData.deletedUser); } } +export class TagTypeCreatedEvent extends BaseEvent { + readonly data: any; + constructor(eventData: { + auditUser: IAuditUser; + data: any; + }) { + super('tag-type-created', eventData.auditUser); + this.data = eventData.data; + } +} + +export class TagTypeDeletedEvent extends BaseEvent { + readonly preData: any; + constructor(eventData: { + auditUser: IAuditUser; + preData: any; + }) { + super(TAG_TYPE_DELETED, eventData.auditUser); + this.preData = eventData.preData; + } +} + +export class TagTypeUpdatedEvent extends BaseEvent { + readonly data: any; + constructor(eventData: { + auditUser: IAuditUser; + data: any; + }) { + super(TAG_TYPE_UPDATED, eventData.auditUser); + this.data = eventData.data; + } +} + +export class TagCreatedEvent extends BaseEvent { + readonly data: any; + constructor(eventData: { + auditUser: IAuditUser; + data: any; + }) { + super(TAG_CREATED, eventData.auditUser); + this.data = eventData.data; + } +} + +export class TagDeletedEvent extends BaseEvent { + readonly data: any; + constructor(eventData: { + auditUser: IAuditUser; + data: any; + }) { + super(TAG_DELETED, eventData.auditUser); + this.data = eventData.data; + } +} + +export class PatCreatedEvent extends BaseEvent { + readonly data: any; + constructor(eventData: { + auditUser: IAuditUser; + data: any; + }) { + super(PAT_CREATED, eventData.auditUser); + this.data = eventData.data; + } +} +export class PatDeletedEvent extends BaseEvent { + readonly data: any; + constructor(eventData: { + auditUser: IAuditUser; + data: any; + }) { + super(PAT_DELETED, eventData.auditUser); + this.data = eventData.data; + } +} + +export class ProjectEnvironmentAdded extends BaseEvent { + readonly project: string; + readonly environment: string; + constructor(eventData: { + project: string; + environment: string; + auditUser: IAuditUser; + }) { + super(PROJECT_ENVIRONMENT_ADDED, eventData.auditUser); + this.project = eventData.project; + this.environment = eventData.environment; + } +} + +export class ProjectEnvironmentRemoved extends BaseEvent { + readonly project: string; + readonly environment: string; + constructor(eventData: { + project: string; + environment: string; + auditUser: IAuditUser; + }) { + super(PROJECT_ENVIRONMENT_REMOVED, eventData.auditUser); + this.project = eventData.project; + this.environment = eventData.environment; + } +} + +export class FeaturesExportedEvent extends BaseEvent { + readonly data: any; + constructor(eventData: { + auditUser: IAuditUser; + data: any; + }) { + super(FEATURES_EXPORTED, eventData.auditUser); + this.data = eventData; + } +} + +export class RoleCreatedEvent extends BaseEvent { + readonly data: any; + constructor(eventData: { + data: any; + auditUser: IAuditUser; + }) { + super(ROLE_CREATED, eventData.auditUser); + this.data = eventData.data; + } +} + +export class RoleDeletedEvent extends BaseEvent { + readonly preData: any; + constructor(eventData: { + preData: any; + auditUser: IAuditUser; + }) { + super(ROLE_DELETED, eventData.auditUser); + this.preData = eventData.preData; + } +} +export class StrategyCreatedEvent extends BaseEvent { + readonly data: any; + constructor(eventData: { + data: any; + auditUser: IAuditUser; + }) { + super(STRATEGY_CREATED, eventData.auditUser); + this.data = eventData.data; + } +} +export class StrategyUpdatedEvent extends BaseEvent { + readonly data: any; + constructor(eventData: { + data: any; + auditUser: IAuditUser; + }) { + super(STRATEGY_UPDATED, eventData.auditUser); + this.data = eventData.data; + } +} +export class StrategyDeletedEvent extends BaseEvent { + readonly data: any; + constructor(eventData: { + data: any; + auditUser: IAuditUser; + }) { + super(STRATEGY_DELETED, eventData.auditUser); + this.data = eventData.data; + } +} +export class StrategyDeprecatedEvent extends BaseEvent { + readonly data: any; + constructor(eventData: { + data: any; + auditUser: IAuditUser; + }) { + super(STRATEGY_DEPRECATED, eventData.auditUser); + this.data = eventData.data; + } +} +export class StrategyReactivatedEvent extends BaseEvent { + readonly data: any; + constructor(eventData: { + data: any; + auditUser: IAuditUser; + }) { + super(STRATEGY_REACTIVATED, eventData.auditUser); + this.data = eventData.data; + } +} + +export class DefaultStrategyUpdatedEvent extends BaseEvent { + readonly project: string; + readonly environment: string; + readonly preData: any; + readonly data: any; + constructor(eventData: { + project: string; + environment: string; + auditUser: IAuditUser; + preData: any; + data: any; + }) { + super(DEFAULT_STRATEGY_UPDATED, eventData.auditUser); + this.data = eventData.data; + this.preData = eventData.preData; + this.project = eventData.project; + this.environment = eventData.environment; + } +} + +export class AddonConfigCreatedEvent extends BaseEvent { + readonly data: any; + constructor(eventData: { + auditUser: IAuditUser; + data: any; + }) { + super(ADDON_CONFIG_CREATED, eventData.auditUser); + this.data = eventData.data; + } +} + +export class AddonConfigUpdatedEvent extends BaseEvent { + readonly data: any; + readonly preData: any; + constructor(eventData: { + auditUser: IAuditUser; + data: any; + preData: any; + }) { + super(ADDON_CONFIG_UPDATED, eventData.auditUser); + this.data = eventData.data; + this.preData = eventData.preData; + } +} + +export class AddonConfigDeletedEvent extends BaseEvent { + readonly preData: any; + constructor(eventData: { + auditUser: IAuditUser; + preData: any; + }) { + super(ADDON_CONFIG_DELETED, eventData.auditUser); + this.preData = eventData.preData; + } +} + +export class SegmentCreatedEvent extends BaseEvent { + readonly project: string; + readonly data: any; + constructor(eventData: { + auditUser: IAuditUser; + project: string; + data: any; + }) { + super(SEGMENT_CREATED, eventData.auditUser); + this.project = eventData.project; + this.data = eventData.data; + } +} + +export class SegmentUpdatedEvent extends BaseEvent { + readonly data: any; + readonly preData: any; + readonly project: string; + constructor(eventData: { + auditUser: IAuditUser; + project: string; + data: any; + preData: any; + }) { + super(SEGMENT_UPDATED, eventData.auditUser); + this.project = eventData.project; + this.data = eventData.data; + this.preData = eventData.preData; + } +} +export class SegmentDeletedEvent extends BaseEvent { + readonly preData: any; + readonly project?: string; + constructor(eventData: { + auditUser: IAuditUser; + preData: any; + project?: string; + }) { + super(SEGMENT_DELETED, eventData.auditUser); + this.preData = eventData.preData; + this.project = eventData.project; + } +} + +export class GroupUpdatedEvent extends BaseEvent { + readonly preData: any; + readonly data: any; + constructor(eventData: { + data: any; + preData: any; + auditUser: IAuditUser; + }) { + super(GROUP_UPDATED, eventData.auditUser); + this.preData = eventData.preData; + this.data = eventData.data; + } +} + +export class GroupDeletedEvent extends BaseEvent { + readonly preData: any; + constructor(eventData: { + preData: any; + auditUser: IAuditUser; + }) { + super(GROUP_DELETED, eventData.auditUser); + this.preData = eventData.preData; + } +} + interface IUserEventData extends Pick< IUserWithRootRole, diff --git a/src/lib/types/user.ts b/src/lib/types/user.ts index 2b3590e177b..618d4e815c8 100644 --- a/src/lib/types/user.ts +++ b/src/lib/types/user.ts @@ -40,7 +40,7 @@ export interface IProjectUser extends IUser { export interface IAuditUser { id: number; username: string; - ip?: string; + ip: string; } export default class User implements IUser { diff --git a/src/lib/users/inactive/inactive-users-controller.ts b/src/lib/users/inactive/inactive-users-controller.ts index 7d3f27263c0..6d30f21196a 100644 --- a/src/lib/users/inactive/inactive-users-controller.ts +++ b/src/lib/users/inactive/inactive-users-controller.ts @@ -114,7 +114,7 @@ export class InactiveUsersController extends Controller { res: Response, ): Promise { await this.inactiveUsersService.deleteInactiveUsers( - req.user, + req.audit, req.body.ids.filter((inactiveUser) => inactiveUser !== req.user.id), ); res.status(200).send(); diff --git a/src/lib/users/inactive/inactive-users-service.ts b/src/lib/users/inactive/inactive-users-service.ts index b6962454c09..9cd58523f09 100644 --- a/src/lib/users/inactive/inactive-users-service.ts +++ b/src/lib/users/inactive/inactive-users-service.ts @@ -1,7 +1,7 @@ import { + type IAuditUser, type IUnleashConfig, type IUnleashStores, - type IUser, serializeDates, } from '../../types'; import type { IInactiveUsersStore } from './types/inactive-users-store-type'; @@ -48,7 +48,7 @@ export class InactiveUsersService { } async deleteInactiveUsers( - calledByUser: IUser, + calledByUser: IAuditUser, userIds: number[], ): Promise { this.logger.info('Deleting inactive users'); diff --git a/src/lib/util/extract-user.ts b/src/lib/util/extract-user.ts index 3c06cb622d3..a12ab17d32b 100644 --- a/src/lib/util/extract-user.ts +++ b/src/lib/util/extract-user.ts @@ -2,8 +2,8 @@ import { SYSTEM_USER } from '../../lib/types'; import type { IApiRequest, IApiUser, - IAuthRequest, IAuditUser, + IAuthRequest, IUser, } from '../server-impl'; @@ -28,6 +28,14 @@ export const extractUserInfo = (req: IAuthRequest | IApiRequest) => ({ username: extractUsername(req), }); +export const extractAuditInfoFromUser = ( + user: IUser | IApiUser, + ip: string = '127.0.0.1', +): IAuditUser => ({ + id: extractUserIdFromUser(user), + username: extractUsernameFromUser(user), + ip, +}); export const extractAuditInfo = ( req: IAuthRequest | IApiRequest, ): IAuditUser => ({ diff --git a/src/migrations/20240418140646-add-ip-column-to-events-table.js b/src/migrations/20240418140646-add-ip-column-to-events-table.js new file mode 100644 index 00000000000..f69c08c0882 --- /dev/null +++ b/src/migrations/20240418140646-add-ip-column-to-events-table.js @@ -0,0 +1,7 @@ +exports.up = function (db, cb) { + db.runSql(`ALTER TABLE events ADD COLUMN ip TEXT`, cb); +}; + +exports.down = function (db, cb) { + db.runSql(`ALTER TABLE events DROP COLUMN ip`, cb); +}; 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 144658ecec1..8d640330fa6 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 @@ -11,10 +11,14 @@ import { CREATE_CLIENT_API_TOKEN, CREATE_PROJECT_API_TOKEN, DELETE_CLIENT_API_TOKEN, + type IUnleashServices, type IUnleashStores, READ_CLIENT_API_TOKEN, READ_FRONTEND_API_TOKEN, + SYSTEM_USER, + SYSTEM_USER_AUDIT, SYSTEM_USER_ID, + TEST_AUDIT_USER, UPDATE_CLIENT_API_TOKEN, } from '../../../../lib/types'; import { addDays } from 'date-fns'; @@ -217,8 +221,7 @@ test('An admin token should be allowed to create a token', async () => { projects: ['*'], environment: '*', }, - 'test', - 1, + TEST_AUDIT_USER, ); await request @@ -252,17 +255,23 @@ test('A role with only CREATE_PROJECT_API_TOKEN can create project tokens', asyn const role = (await accessService.getPredefinedRole( RoleName.VIEWER, ))!; - const user = await userService.createUser({ - email: 'powerpuffgirls_viewer@example.com', - rootRole: role.id, - }); - const createClientApiTokenRole = await accessService.createRole({ - name: 'project_client_token_creator', - description: 'Can create client tokens', - permissions: [{ name: CREATE_PROJECT_API_TOKEN }], - type: 'root-custom', - createdByUserId: SYSTEM_USER_ID, - }); + const user = await userService.createUser( + { + email: 'powerpuffgirls_viewer@example.com', + rootRole: role.id, + }, + SYSTEM_USER_AUDIT, + ); + const createClientApiTokenRole = await accessService.createRole( + { + name: 'project_client_token_creator', + description: 'Can create client tokens', + permissions: [{ name: CREATE_PROJECT_API_TOKEN }], + type: 'root-custom', + createdByUserId: SYSTEM_USER_ID, + }, + SYSTEM_USER_AUDIT, + ); await accessService.addUserToRole( user.id, createClientApiTokenRole.id, @@ -295,7 +304,14 @@ test('A role with only CREATE_PROJECT_API_TOKEN can create project tokens', asyn describe('Fine grained API token permissions', () => { describe('A role with access to CREATE_CLIENT_API_TOKEN', () => { test('should be allowed to create client tokens', async () => { - const preHook = (app, config, { userService, accessService }) => { + const preHook = ( + app, + config, + { + userService, + accessService, + }: Pick, + ) => { app.use('/api/admin/', async (req, res, next) => { const builtInRole = await accessService.getPredefinedRole( RoleName.VIEWER, @@ -306,12 +322,16 @@ describe('Fine grained API token permissions', () => { }); req.user = user; const createClientApiTokenRole = - await accessService.createRole({ - name: 'client_token_creator', - description: 'Can create client tokens', - permissions: [], - type: 'root-custom', - }); + await accessService.createRole( + { + name: 'client_token_creator', + description: 'Can create client tokens', + permissions: [], + type: 'root-custom', + createdByUserId: SYSTEM_USER.id, + }, + SYSTEM_USER_AUDIT, + ); // not sure if we should add the permission to the builtin role or to the newly created role await accessService.addPermissionToRole( builtInRole.id, @@ -340,7 +360,14 @@ describe('Fine grained API token permissions', () => { await destroy(); }); test('should NOT be allowed to create frontend tokens', async () => { - const preHook = (app, config, { userService, accessService }) => { + const preHook = ( + app, + config, + { + userService, + accessService, + }: Pick, + ) => { app.use('/api/admin/', async (req, res, next) => { const role = await accessService.getPredefinedRole( RoleName.VIEWER, @@ -351,12 +378,16 @@ describe('Fine grained API token permissions', () => { }); req.user = user; const createClientApiTokenRole = - await accessService.createRole({ - name: 'client_token_creator_cannot_create_frontend', - description: 'Can create client tokens', - permissions: [], - type: 'root-custom', - }); + await accessService.createRole( + { + name: 'client_token_creator_cannot_create_frontend', + description: 'Can create client tokens', + permissions: [], + type: 'root-custom', + createdByUserId: SYSTEM_USER_ID, + }, + SYSTEM_USER_AUDIT, + ); await accessService.addPermissionToRole( role.id, CREATE_CLIENT_API_TOKEN, @@ -384,7 +415,14 @@ describe('Fine grained API token permissions', () => { await destroy(); }); test('should NOT be allowed to create ADMIN tokens', async () => { - const preHook = (app, config, { userService, accessService }) => { + const preHook = ( + app, + config, + { + userService, + accessService, + }: Pick, + ) => { app.use('/api/admin/', async (req, res, next) => { const role = await accessService.getPredefinedRole( RoleName.VIEWER, @@ -395,12 +433,16 @@ describe('Fine grained API token permissions', () => { }); req.user = user; const createClientApiTokenRole = - await accessService.createRole({ - name: 'client_token_creator_cannot_create_admin', - description: 'Can create client tokens', - permissions: [], - type: 'root-custom', - }); + await accessService.createRole( + { + name: 'client_token_creator_cannot_create_admin', + description: 'Can create client tokens', + permissions: [], + type: 'root-custom', + createdByUserId: SYSTEM_USER_ID, + }, + SYSTEM_USER_AUDIT, + ); await accessService.addPermissionToRole( role.id, CREATE_CLIENT_API_TOKEN, @@ -430,7 +472,14 @@ describe('Fine grained API token permissions', () => { }); describe('Read operations', () => { test('READ_FRONTEND_API_TOKEN should be able to see FRONTEND tokens', async () => { - const preHook = (app, config, { userService, accessService }) => { + const preHook = ( + app, + config, + { + userService, + accessService, + }: Pick, + ) => { app.use('/api/admin/', async (req, res, next) => { const role = await accessService.getPredefinedRole( RoleName.VIEWER, @@ -446,7 +495,9 @@ describe('Fine grained API token permissions', () => { description: 'Can read frontend tokens', permissions: [], type: 'root-custom', + createdByUserId: SYSTEM_USER_ID, }, + SYSTEM_USER_AUDIT, ); await accessService.addPermissionToRole( readFrontendApiToken.id, @@ -504,7 +555,14 @@ describe('Fine grained API token permissions', () => { await destroy(); }); test('READ_CLIENT_API_TOKEN should be able to see CLIENT tokens', async () => { - const preHook = (app, config, { userService, accessService }) => { + const preHook = ( + app, + config, + { + userService, + accessService, + }: Pick, + ) => { app.use('/api/admin/', async (req, res, next) => { const role = await accessService.getPredefinedRole( RoleName.VIEWER, @@ -514,12 +572,16 @@ describe('Fine grained API token permissions', () => { rootRole: role.id, }); req.user = user; - const readClientTokenRole = await accessService.createRole({ - name: 'client_token_reader', - description: 'Can read client tokens', - permissions: [], - type: 'root-custom', - }); + const readClientTokenRole = await accessService.createRole( + { + name: 'client_token_reader', + description: 'Can read client tokens', + permissions: [], + type: 'root-custom', + createdByUserId: SYSTEM_USER_ID, + }, + SYSTEM_USER_AUDIT, + ); await accessService.addPermissionToRole( readClientTokenRole.id, READ_CLIENT_API_TOKEN, @@ -572,7 +634,14 @@ describe('Fine grained API token permissions', () => { await destroy(); }); test('Admin users should be able to see all tokens', async () => { - const preHook = (app, config, { userService, accessService }) => { + const preHook = ( + app, + config, + { + userService, + accessService, + }: Pick, + ) => { app.use('/api/admin/', async (req, res, next) => { const role = await accessService.getPredefinedRole( RoleName.ADMIN, @@ -624,7 +693,14 @@ describe('Fine grained API token permissions', () => { await destroy(); }); test('Editor users should be able to see all tokens except ADMIN tokens', async () => { - const preHook = (app, config, { userService, accessService }) => { + const preHook = ( + app, + config, + { + userService, + accessService, + }: Pick, + ) => { app.use('/api/admin/', async (req, res, next) => { const role = await accessService.getPredefinedRole( RoleName.EDITOR, @@ -686,7 +762,10 @@ describe('Fine grained API token permissions', () => { const preHook = ( app, config, - { userService, accessService }, + { + userService, + accessService, + }: Pick, ) => { app.use('/api/admin/', async (req, res, next) => { const role = await accessService.getPredefinedRole( @@ -698,12 +777,16 @@ describe('Fine grained API token permissions', () => { }); req.user = user; const updateClientApiExpiry = - await accessService.createRole({ - name: 'update_client_token', - description: 'Can update client tokens', - permissions: [], - type: 'root-custom', - }); + await accessService.createRole( + { + name: 'update_client_token', + description: 'Can update client tokens', + permissions: [], + type: 'root-custom', + createdByUserId: SYSTEM_USER_ID, + }, + SYSTEM_USER_AUDIT, + ); await accessService.addPermissionToRole( updateClientApiExpiry.id, UPDATE_CLIENT_API_TOKEN, @@ -738,7 +821,10 @@ describe('Fine grained API token permissions', () => { const preHook = ( app, config, - { userService, accessService }, + { + userService, + accessService, + }: Pick, ) => { app.use('/api/admin/', async (req, res, next) => { const role = await accessService.getPredefinedRole( @@ -750,12 +836,17 @@ describe('Fine grained API token permissions', () => { }); req.user = user; const updateClientApiExpiry = - await accessService.createRole({ - name: 'update_client_token_not_frontend', - description: 'Can not update frontend tokens', - permissions: [], - type: 'root-custom', - }); + await accessService.createRole( + { + name: 'update_client_token_not_frontend', + description: + 'Can not update frontend tokens', + permissions: [], + type: 'root-custom', + createdByUserId: SYSTEM_USER_ID, + }, + SYSTEM_USER_AUDIT, + ); await accessService.addPermissionToRole( updateClientApiExpiry.id, UPDATE_CLIENT_API_TOKEN, @@ -791,7 +882,10 @@ describe('Fine grained API token permissions', () => { const preHook = ( app, config, - { userService, accessService }, + { + userService, + accessService, + }: Pick, ) => { app.use('/api/admin/', async (req, res, next) => { const role = await accessService.getPredefinedRole( @@ -803,12 +897,16 @@ describe('Fine grained API token permissions', () => { }); req.user = user; const updateClientApiExpiry = - await accessService.createRole({ - name: 'update_client_token_not_admin', - description: 'Can not update admin tokens', - permissions: [], - type: 'root-custom', - }); + await accessService.createRole( + { + name: 'update_client_token_not_admin', + description: 'Can not update admin tokens', + permissions: [], + type: 'root-custom', + createdByUserId: SYSTEM_USER_ID, + }, + SYSTEM_USER_AUDIT, + ); await accessService.addPermissionToRole( updateClientApiExpiry.id, UPDATE_CLIENT_API_TOKEN, @@ -848,7 +946,10 @@ describe('Fine grained API token permissions', () => { const preHook = ( app, config, - { userService, accessService }, + { + userService, + accessService, + }: Pick, ) => { app.use('/api/admin/', async (req, res, next) => { const role = await accessService.getPredefinedRole( @@ -860,12 +961,16 @@ describe('Fine grained API token permissions', () => { }); req.user = user; const updateClientApiExpiry = - await accessService.createRole({ - name: 'delete_client_token', - description: 'Can delete client tokens', - permissions: [], - type: 'root-custom', - }); + await accessService.createRole( + { + name: 'delete_client_token', + description: 'Can delete client tokens', + permissions: [], + type: 'root-custom', + createdByUserId: SYSTEM_USER_ID, + }, + SYSTEM_USER_AUDIT, + ); await accessService.addPermissionToRole( updateClientApiExpiry.id, DELETE_CLIENT_API_TOKEN, @@ -900,7 +1005,10 @@ describe('Fine grained API token permissions', () => { const preHook = ( app, config, - { userService, accessService }, + { + userService, + accessService, + }: Pick, ) => { app.use('/api/admin/', async (req, res, next) => { const role = await accessService.getPredefinedRole( @@ -912,12 +1020,17 @@ describe('Fine grained API token permissions', () => { }); req.user = user; const updateClientApiExpiry = - await accessService.createRole({ - name: 'delete_client_token_not_frontend', - description: 'Can not delete frontend tokens', - permissions: [], - type: 'root-custom', - }); + await accessService.createRole( + { + name: 'delete_client_token_not_frontend', + description: + 'Can not delete frontend tokens', + permissions: [], + type: 'root-custom', + createdByUserId: SYSTEM_USER_ID, + }, + SYSTEM_USER_AUDIT, + ); await accessService.addPermissionToRole( updateClientApiExpiry.id, DELETE_CLIENT_API_TOKEN, @@ -952,7 +1065,10 @@ describe('Fine grained API token permissions', () => { const preHook = ( app, config, - { userService, accessService }, + { + userService, + accessService, + }: Pick, ) => { app.use('/api/admin/', async (req, res, next) => { const role = await accessService.getPredefinedRole( @@ -964,12 +1080,16 @@ describe('Fine grained API token permissions', () => { }); req.user = user; const updateClientApiExpiry = - await accessService.createRole({ - name: 'delete_client_token_not_admin', - description: 'Can not delete admin tokens', - permissions: [], - type: 'root-custom', - }); + await accessService.createRole( + { + name: 'delete_client_token_not_admin', + description: 'Can not delete admin tokens', + permissions: [], + type: 'root-custom', + createdByUserId: SYSTEM_USER_ID, + }, + SYSTEM_USER_AUDIT, + ); await accessService.addPermissionToRole( updateClientApiExpiry.id, DELETE_CLIENT_API_TOKEN, diff --git a/src/test/e2e/api/admin/config.e2e.test.ts b/src/test/e2e/api/admin/config.e2e.test.ts index 27c8869842b..9d4f6f36806 100644 --- a/src/test/e2e/api/admin/config.e2e.test.ts +++ b/src/test/e2e/api/admin/config.e2e.test.ts @@ -5,7 +5,7 @@ import { } from '../../helpers/test-helper'; import getLogger from '../../../fixtures/no-logger'; import { simpleAuthSettingsKey } from '../../../../lib/types/settings/simple-auth-settings'; -import { randomId } from '../../../../lib/util/random-id'; +import { TEST_AUDIT_USER } from '../../../../lib/types'; let db: ITestDb; let app: IUnleashTest; @@ -59,8 +59,7 @@ test('gets ui config with frontendSettings', async () => { const frontendApiOrigins = ['https://example.net']; await app.services.frontendApiService.setFrontendSettings( { frontendApiOrigins }, - randomId(), - -9999, + TEST_AUDIT_USER, ); 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 8c8389b7f1d..04c64f6a1ad 100644 --- a/src/test/e2e/api/admin/event.e2e.test.ts +++ b/src/test/e2e/api/admin/event.e2e.test.ts @@ -7,7 +7,7 @@ import getLogger from '../../../fixtures/no-logger'; import { FEATURE_CREATED, type IBaseEvent } from '../../../../lib/types/events'; import { randomId } from '../../../../lib/util/random-id'; import { EventService } from '../../../../lib/services'; -import { EventEmitter } from 'stream'; +import EventEmitter from 'events'; let app: IUnleashTest; let db: ITestDb; @@ -60,7 +60,6 @@ test('Can filter by project', async () => { data: { id: 'some-other-feature' }, tags: [], createdBy: 'test-user', - environment: 'test', createdByUserId: TEST_USER_ID, }); await eventService.storeEvent({ diff --git a/src/test/e2e/api/admin/project/project.health.e2e.test.ts b/src/test/e2e/api/admin/project/project.health.e2e.test.ts index 27760868159..c7de77ef018 100644 --- a/src/test/e2e/api/admin/project/project.health.e2e.test.ts +++ b/src/test/e2e/api/admin/project/project.health.e2e.test.ts @@ -5,6 +5,7 @@ import { } from '../../../helpers/test-helper'; import getLogger from '../../../../fixtures/no-logger'; import type { IUser } from '../../../../../lib/types'; +import { extractAuditInfoFromUser } from '../../../../../lib/util'; let app: IUnleashTest; let db: ITestDb; @@ -40,7 +41,11 @@ test('Project with no stale toggles should have 100% health rating', async () => name: 'Health rating', description: 'Fancy', }; - await app.services.projectService.createProject(project, user); + await app.services.projectService.createProject( + project, + user, + extractAuditInfoFromUser(user), + ); await app.request .post('/api/admin/projects/fresh/features') .send({ @@ -76,7 +81,11 @@ test('Health rating endpoint yields stale, potentially stale and active count on name: 'Health rating', description: 'Fancy', }; - await app.services.projectService.createProject(project, user); + await app.services.projectService.createProject( + project, + user, + extractAuditInfoFromUser(user), + ); await app.request .post(`/api/admin/projects/${project.id}/features`) .send({ @@ -119,7 +128,11 @@ test('Health rating endpoint does not include archived toggles when calculating name: 'Health rating', description: 'Fancy', }; - await app.services.projectService.createProject(project, user); + await app.services.projectService.createProject( + project, + user, + extractAuditInfoFromUser(user), + ); await app.request .post(`/api/admin/projects/${project.id}/features`) .send({ @@ -180,7 +193,11 @@ test('Health rating endpoint correctly handles potentially stale toggles', async name: 'Health rating', description: 'Fancy', }; - await app.services.projectService.createProject(project, user); + await app.services.projectService.createProject( + project, + user, + extractAuditInfoFromUser(user), + ); await app.request .post(`/api/admin/projects/${project.id}/features`) .send({ diff --git a/src/test/e2e/api/admin/state.e2e.test.ts b/src/test/e2e/api/admin/state.e2e.test.ts index 5fca93b8741..ab1e5dc92eb 100644 --- a/src/test/e2e/api/admin/state.e2e.test.ts +++ b/src/test/e2e/api/admin/state.e2e.test.ts @@ -7,7 +7,11 @@ 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 { type IUser, SYSTEM_USER } from '../../../../lib/types'; +import { + type IUser, + SYSTEM_USER_AUDIT, + TEST_AUDIT_USER, +} from '../../../../lib/types'; const importData = require('../../../examples/import.json'); @@ -174,8 +178,7 @@ test('Can roundtrip. I.e. export and then import', async () => { await app.services.environmentService.addEnvironmentToProject( environment, projectId, - SYSTEM_USER.username, - SYSTEM_USER.id, + SYSTEM_USER_AUDIT, ); await app.services.featureToggleServiceV2.createFeatureToggle( projectId, @@ -184,8 +187,7 @@ test('Can roundtrip. I.e. export and then import', async () => { name: featureName, description: 'Feature for export', }, - userName, - userId, + TEST_AUDIT_USER, ); await app.services.featureToggleServiceV2.createStrategy( { @@ -196,7 +198,7 @@ test('Can roundtrip. I.e. export and then import', async () => { parameters: {}, }, { projectId, featureName, environment }, - userName, + TEST_AUDIT_USER, { id: userId } as IUser, ); const data = await app.services.stateService.export({}); @@ -227,8 +229,7 @@ test('Roundtrip with tags works', async () => { await app.services.environmentService.addEnvironmentToProject( environment, projectId, - SYSTEM_USER.username, - SYSTEM_USER.id, + SYSTEM_USER_AUDIT, ); await app.services.featureToggleServiceV2.createFeatureToggle( projectId, @@ -237,8 +238,7 @@ test('Roundtrip with tags works', async () => { name: featureName, description: 'Feature for export', }, - userName, - userId, + TEST_AUDIT_USER, ); await app.services.featureToggleServiceV2.createStrategy( { @@ -253,19 +253,17 @@ test('Roundtrip with tags works', async () => { featureName, environment, }, - userName, + TEST_AUDIT_USER, ); await app.services.featureTagService.addTag( featureName, { type: 'simple', value: 'export-test' }, - userName, - -9999, + TEST_AUDIT_USER, ); await app.services.featureTagService.addTag( featureName, { type: 'simple', value: 'export-test-2' }, - userName, - -9999, + TEST_AUDIT_USER, ); const data = await app.services.stateService.export({}); await app.services.stateService.import({ @@ -303,21 +301,18 @@ test('Roundtrip with strategies in multiple environments works', async () => { name: featureName, description: 'Feature for export', }, - userName, - userId, + TEST_AUDIT_USER, ); await app.services.environmentService.addEnvironmentToProject( environment, projectId, - SYSTEM_USER.username, - SYSTEM_USER.id, + SYSTEM_USER_AUDIT, ); await app.services.environmentService.addEnvironmentToProject( DEFAULT_ENV, projectId, - SYSTEM_USER.username, - SYSTEM_USER.id, + SYSTEM_USER_AUDIT, ); await app.services.featureToggleServiceV2.createStrategy( { @@ -328,7 +323,7 @@ test('Roundtrip with strategies in multiple environments works', async () => { parameters: {}, }, { projectId, featureName, environment }, - userName, + TEST_AUDIT_USER, ); await app.services.featureToggleServiceV2.createStrategy( { @@ -339,7 +334,7 @@ test('Roundtrip with strategies in multiple environments works', async () => { parameters: {}, }, { projectId, featureName, environment: DEFAULT_ENV }, - userName, + TEST_AUDIT_USER, ); const data = await app.services.stateService.export({}); await app.services.stateService.import({ @@ -405,8 +400,7 @@ 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, + SYSTEM_USER_AUDIT, ); await app.services.featureToggleServiceV2.createFeatureToggle( projectId, @@ -415,8 +409,7 @@ test(`should not delete api_tokens on import when drop-flag is set`, async () => name: featureName, description: 'Feature for export', }, - userName, - userId, + TEST_AUDIT_USER, ); await app.services.apiTokenService.createApiTokenWithProjects({ tokenName: apiTokenName, diff --git a/src/test/e2e/api/auth/reset-password-controller.e2e.test.ts b/src/test/e2e/api/auth/reset-password-controller.e2e.test.ts index d06e5f63157..a48d5ad36a2 100644 --- a/src/test/e2e/api/auth/reset-password-controller.e2e.test.ts +++ b/src/test/e2e/api/auth/reset-password-controller.e2e.test.ts @@ -21,7 +21,7 @@ import SettingService from '../../../../lib/services/setting-service'; import FakeSettingStore from '../../../fixtures/fake-setting-store'; import { GroupService } from '../../../../lib/services/group-service'; import { EventService } from '../../../../lib/services'; -import type { IUnleashStores } from '../../../../lib/types'; +import { type IUnleashStores, TEST_AUDIT_USER } from '../../../../lib/types'; let app: IUnleashTest; let stores: IUnleashStores; @@ -86,17 +86,23 @@ beforeAll(async () => { }); resetTokenService = new ResetTokenService(stores, config); const adminRole = (await accessService.getPredefinedRole(RoleName.ADMIN))!; - adminUser = await userService.createUser({ - username: 'admin@test.com', - rootRole: adminRole.id, - })!; + adminUser = await userService.createUser( + { + username: 'admin@test.com', + rootRole: adminRole.id, + }, + TEST_AUDIT_USER, + )!; const userRole = (await accessService.getPredefinedRole(RoleName.EDITOR))!; - user = await userService.createUser({ - username: 'test@test.com', - email: 'test@test.com', - rootRole: userRole.id, - }); + user = await userService.createUser( + { + username: 'test@test.com', + email: 'test@test.com', + rootRole: userRole.id, + }, + TEST_AUDIT_USER, + ); }); afterAll(async () => { diff --git a/src/test/e2e/api/auth/simple-password-provider.e2e.test.ts b/src/test/e2e/api/auth/simple-password-provider.e2e.test.ts index 6a6c46e0d6f..1befb56b18a 100644 --- a/src/test/e2e/api/auth/simple-password-provider.e2e.test.ts +++ b/src/test/e2e/api/auth/simple-password-provider.e2e.test.ts @@ -1,5 +1,9 @@ import { createTestConfig } from '../../../config/test-config'; -import type { IUnleashConfig, IUnleashStores } from '../../../../lib/types'; +import { + type IUnleashConfig, + type IUnleashStores, + TEST_AUDIT_USER, +} from '../../../../lib/types'; import UserService from '../../../../lib/services/user-service'; import { AccessService } from '../../../../lib/services/access-service'; import type { IUser } from '../../../../lib/types/user'; @@ -57,12 +61,15 @@ beforeAll(async () => { settingService, }); const adminRole = await accessService.getPredefinedRole(RoleName.ADMIN); - adminUser = await userService.createUser({ - username: 'admin@test.com', - email: 'admin@test.com', - rootRole: adminRole!.id, - password: password, - }); + adminUser = await userService.createUser( + { + username: 'admin@test.com', + email: 'admin@test.com', + rootRole: adminRole!.id, + password: password, + }, + TEST_AUDIT_USER, + ); }); beforeEach(async () => { diff --git a/src/test/e2e/api/client/feature.auth-none.e2e.test.ts b/src/test/e2e/api/client/feature.auth-none.e2e.test.ts index 13704449755..ee26f115736 100644 --- a/src/test/e2e/api/client/feature.auth-none.e2e.test.ts +++ b/src/test/e2e/api/client/feature.auth-none.e2e.test.ts @@ -7,6 +7,7 @@ import getLogger from '../../../fixtures/no-logger'; import { DEFAULT_ENV } from '../../../../lib/util/constants'; import type User from '../../../../lib/types/user'; import { ApiTokenType } from '../../../../lib/types/models/api-token'; +import { TEST_AUDIT_USER } from '../../../../lib/types'; let app: IUnleashTest; let db: ITestDb; @@ -37,8 +38,7 @@ beforeAll(async () => { description: 'the #1 feature', impressionData: true, }, - 'test', - testUser.id, + TEST_AUDIT_USER, ); await app.services.featureToggleService.createFeatureToggle( 'default', @@ -46,8 +46,7 @@ beforeAll(async () => { name: 'feature_2', description: 'soon to be the #1 feature', }, - 'test', - testUser.id, + TEST_AUDIT_USER, ); await app.services.featureToggleService.createFeatureToggle( @@ -56,8 +55,7 @@ beforeAll(async () => { name: 'feature_3', description: 'terrible feature', }, - 'test', - testUser.id, + TEST_AUDIT_USER, ); const token = await app.services.apiTokenService.createApiTokenWithProjects( diff --git a/src/test/e2e/api/client/feature.e2e.test.ts b/src/test/e2e/api/client/feature.e2e.test.ts index 682c7294d40..bb17913d0aa 100644 --- a/src/test/e2e/api/client/feature.e2e.test.ts +++ b/src/test/e2e/api/client/feature.e2e.test.ts @@ -6,7 +6,7 @@ import dbInit, { type ITestDb } from '../../helpers/database-init'; import getLogger from '../../../fixtures/no-logger'; import { DEFAULT_ENV } from '../../../../lib/util/constants'; import type User from '../../../../lib/types/user'; -import { SYSTEM_USER } from '../../../../lib/types'; +import { SYSTEM_USER_AUDIT, TEST_AUDIT_USER } from '../../../../lib/types'; let app: IUnleashTest; let db: ITestDb; @@ -32,8 +32,7 @@ beforeAll(async () => { description: 'the #1 feature', impressionData: true, }, - 'test', - testUser.id, + TEST_AUDIT_USER, ); await app.services.featureToggleServiceV2.createFeatureToggle( 'default', @@ -41,8 +40,7 @@ beforeAll(async () => { name: 'featureY', description: 'soon to be the #1 feature', }, - 'test', - testUser.id, + TEST_AUDIT_USER, ); await app.services.featureToggleServiceV2.createFeatureToggle( @@ -51,8 +49,7 @@ beforeAll(async () => { name: 'featureZ', description: 'terrible feature', }, - 'test', - testUser.id, + TEST_AUDIT_USER, ); await app.services.featureToggleServiceV2.createFeatureToggle( 'default', @@ -60,20 +57,19 @@ beforeAll(async () => { name: 'featureArchivedX', description: 'the #1 feature', }, - 'test', - testUser.id, + TEST_AUDIT_USER, ); // depend on enabled feature with variant await app.services.dependentFeaturesService.unprotectedUpsertFeatureDependency( { child: 'featureY', projectId: 'default' }, { feature: 'featureX', variants: ['featureXVariant'] }, - 'test', - testUser.id, + TEST_AUDIT_USER, ); await app.services.featureToggleServiceV2.archiveToggle( 'featureArchivedX', testUser, + TEST_AUDIT_USER, ); await app.services.featureToggleServiceV2.createFeatureToggle( @@ -82,13 +78,13 @@ beforeAll(async () => { name: 'featureArchivedY', description: 'soon to be the #1 feature', }, - 'test', - testUser.id, + TEST_AUDIT_USER, ); await app.services.featureToggleServiceV2.archiveToggle( 'featureArchivedY', testUser, + TEST_AUDIT_USER, ); await app.services.featureToggleServiceV2.createFeatureToggle( 'default', @@ -96,12 +92,12 @@ beforeAll(async () => { name: 'featureArchivedZ', description: 'terrible feature', }, - 'test', - testUser.id, + TEST_AUDIT_USER, ); await app.services.featureToggleServiceV2.archiveToggle( 'featureArchivedZ', testUser, + TEST_AUDIT_USER, ); await app.services.featureToggleServiceV2.createFeatureToggle( 'default', @@ -109,8 +105,7 @@ beforeAll(async () => { name: 'feature.with.variants', description: 'A feature toggle with variants', }, - 'test', - testUser.id, + TEST_AUDIT_USER, ); await app.services.featureToggleServiceV2.saveVariants( 'feature.with.variants', @@ -129,8 +124,7 @@ beforeAll(async () => { stickiness: 'default', }, ], - 'ivar', - testUser.id, + TEST_AUDIT_USER, ); }); @@ -253,8 +247,7 @@ test('Can get strategies for specific environment', async () => { await app.services.environmentService.addEnvironmentToProject( 'testing', 'default', - SYSTEM_USER.username, - SYSTEM_USER.id, + SYSTEM_USER_AUDIT, ); 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 0f43c70a610..05e1ae60fa4 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 @@ -5,7 +5,7 @@ import { import dbInit, { type ITestDb } from '../../helpers/database-init'; import getLogger from '../../../fixtures/no-logger'; import { DEFAULT_ENV } from '../../../../lib/util/constants'; -import type { IUser } from '../../../../lib/types'; +import { type IUser, TEST_AUDIT_USER } from '../../../../lib/types'; let app: IUnleashTest; let db: ITestDb; @@ -25,14 +25,13 @@ beforeAll(async () => { name: featureName, description: 'the #1 feature', }, - username, - userId, + TEST_AUDIT_USER, ); await app.services.featureToggleServiceV2.createStrategy( { name: 'default', constraints: [], parameters: {} }, { projectId, featureName, environment: DEFAULT_ENV }, - username, + TEST_AUDIT_USER, { id: userId } as IUser, ); }); @@ -48,7 +47,7 @@ test('returns feature toggle for default env', async () => { 'feature.default.1', 'default', true, - 'test', + TEST_AUDIT_USER, ); await app.request 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 1d6a5f020f8..916bbf4058e 100644 --- a/src/test/e2e/api/client/feature.optimal304.e2e.test.ts +++ b/src/test/e2e/api/client/feature.optimal304.e2e.test.ts @@ -5,6 +5,7 @@ import { import dbInit, { type ITestDb } from '../../helpers/database-init'; import getLogger from '../../../fixtures/no-logger'; import type User from '../../../../lib/types/user'; +import { TEST_AUDIT_USER } from '../../../../lib/types'; // import { DEFAULT_ENV } from '../../../../lib/util/constants'; let app: IUnleashTest; @@ -28,8 +29,7 @@ beforeAll(async () => { description: 'the #1 feature', impressionData: true, }, - 'test', - testUser.id, + TEST_AUDIT_USER, ); await app.services.featureToggleService.createFeatureToggle( 'default', @@ -37,8 +37,7 @@ beforeAll(async () => { name: 'featureY', description: 'soon to be the #1 feature', }, - 'test', - testUser.id, + TEST_AUDIT_USER, ); await app.services.featureToggleService.createFeatureToggle( 'default', @@ -46,8 +45,7 @@ beforeAll(async () => { name: 'featureZ', description: 'terrible feature', }, - 'test', - testUser.id, + TEST_AUDIT_USER, ); await app.services.featureToggleService.createFeatureToggle( 'default', @@ -55,13 +53,13 @@ beforeAll(async () => { name: 'featureArchivedX', description: 'the #1 feature', }, - 'test', - testUser.id, + TEST_AUDIT_USER, ); await app.services.featureToggleService.archiveToggle( 'featureArchivedX', testUser, + TEST_AUDIT_USER, ); await app.services.featureToggleService.createFeatureToggle( @@ -70,13 +68,13 @@ beforeAll(async () => { name: 'featureArchivedY', description: 'soon to be the #1 feature', }, - 'test', - testUser.id, + TEST_AUDIT_USER, ); await app.services.featureToggleService.archiveToggle( 'featureArchivedY', testUser, + TEST_AUDIT_USER, ); await app.services.featureToggleService.createFeatureToggle( 'default', @@ -84,12 +82,12 @@ beforeAll(async () => { name: 'featureArchivedZ', description: 'terrible feature', }, - 'test', - testUser.id, + TEST_AUDIT_USER, ); await app.services.featureToggleService.archiveToggle( 'featureArchivedZ', testUser, + TEST_AUDIT_USER, ); await app.services.featureToggleService.createFeatureToggle( 'default', @@ -97,8 +95,7 @@ beforeAll(async () => { name: 'feature.with.variants', description: 'A feature toggle with variants', }, - 'test', - testUser.id, + TEST_AUDIT_USER, ); await app.services.featureToggleService.saveVariants( 'feature.with.variants', @@ -117,8 +114,7 @@ beforeAll(async () => { stickiness: 'default', }, ], - 'ivar', - testUser.id, + TEST_AUDIT_USER, ); }); @@ -150,8 +146,7 @@ test('returns 200 when content updates and hash does not match anymore', async ( name: 'featureNew304', description: 'the #1 feature', }, - 'test', - testUser.id, + TEST_AUDIT_USER, ); 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 8ac751a9fe4..74e88d0e40c 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,7 +4,7 @@ import getLogger from '../../../fixtures/no-logger'; import type { 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'; +import { TEST_AUDIT_USER } from '../../../../lib/types'; let app: IUnleashTest; let db: ITestDb; @@ -43,14 +43,12 @@ beforeAll(async () => { await environmentService.addEnvironmentToProject( environment, project, - SYSTEM_USER.username, - SYSTEM_USER.id, + TEST_AUDIT_USER, ); await environmentService.addEnvironmentToProject( environment, project2, - SYSTEM_USER.username, - SYSTEM_USER.id, + TEST_AUDIT_USER, ); await featureToggleServiceV2.createFeatureToggle( @@ -59,8 +57,7 @@ beforeAll(async () => { name: feature1, description: 'the #1 feature', }, - tokenName, - tokenUserId, + TEST_AUDIT_USER, ); await featureToggleServiceV2.createStrategy( @@ -70,7 +67,7 @@ beforeAll(async () => { parameters: {}, }, { projectId: project, featureName: feature1, environment: DEFAULT_ENV }, - tokenName, + TEST_AUDIT_USER, ); await featureToggleServiceV2.createStrategy( { @@ -79,7 +76,7 @@ beforeAll(async () => { parameters: {}, }, { projectId: project, featureName: feature1, environment }, - tokenName, + TEST_AUDIT_USER, ); // create feature 2 @@ -88,8 +85,7 @@ beforeAll(async () => { { name: feature2, }, - tokenName, - tokenUserId, + TEST_AUDIT_USER, ); await featureToggleServiceV2.createStrategy( { @@ -98,7 +94,7 @@ beforeAll(async () => { parameters: {}, }, { projectId: project, featureName: feature2, environment }, - tokenName, + TEST_AUDIT_USER, ); // create feature 3 @@ -107,8 +103,7 @@ beforeAll(async () => { { name: feature3, }, - tokenName, - tokenUserId, + TEST_AUDIT_USER, ); await featureToggleServiceV2.createStrategy( { @@ -117,7 +112,7 @@ beforeAll(async () => { parameters: {}, }, { projectId: project2, featureName: feature3, environment }, - tokenName, + TEST_AUDIT_USER, ); }); diff --git a/src/test/e2e/api/client/metricsV2.e2e.test.ts b/src/test/e2e/api/client/metricsV2.e2e.test.ts index f094f43de79..f0dc9a16906 100644 --- a/src/test/e2e/api/client/metricsV2.e2e.test.ts +++ b/src/test/e2e/api/client/metricsV2.e2e.test.ts @@ -6,12 +6,12 @@ import { ApiTokenType, type IApiToken, } from '../../../../lib/types/models/api-token'; +import { TEST_AUDIT_USER } from '../../../../lib/types'; let app: IUnleashTest; let db: ITestDb; let defaultToken: IApiToken; -const TEST_USER_ID = -9999; beforeAll(async () => { db = await dbInit('metrics_two_api_client', getLogger); app = await setupAppWithAuth(db.stores, {}, db.rawDatabase); @@ -107,14 +107,12 @@ test('should set lastSeen for toggles with metrics both for toggle and toggle en await app.services.featureToggleServiceV2.createFeatureToggle( 'default', { name: 't1' }, - 'tester', - TEST_USER_ID, + TEST_AUDIT_USER, ); await app.services.featureToggleServiceV2.createFeatureToggle( 'default', { name: 't2' }, - 'tester', - TEST_USER_ID, + TEST_AUDIT_USER, ); const token = await app.services.apiTokenService.createApiToken({ diff --git a/src/test/e2e/custom-auth.test.ts b/src/test/e2e/custom-auth.test.ts index 078c8e5ac3c..30ea5927125 100644 --- a/src/test/e2e/custom-auth.test.ts +++ b/src/test/e2e/custom-auth.test.ts @@ -1,11 +1,22 @@ import dbInit, { type ITestDb } from './helpers/database-init'; import { setupAppWithCustomAuth } from './helpers/test-helper'; -import { type IUnleashStores, RoleName } from '../../lib/types'; +import { + type IUnleashServices, + type IUnleashStores, + RoleName, +} from '../../lib/types'; let db: ITestDb; let stores: IUnleashStores; -const preHook = (app, config, { userService, accessService }) => { +const preHook = ( + app, + config, + { + userService, + accessService, + }: Pick, +) => { app.use('/api/admin/', async (req, res, next) => { const role = await accessService.getPredefinedRole(RoleName.EDITOR); req.user = await userService.createUser({ diff --git a/src/test/e2e/seed/segment.seed.ts b/src/test/e2e/seed/segment.seed.ts index a75cfce2cef..0c1a53c9f7c 100644 --- a/src/test/e2e/seed/segment.seed.ts +++ b/src/test/e2e/seed/segment.seed.ts @@ -1,7 +1,6 @@ import dbInit from '../helpers/database-init'; import getLogger from '../../fixtures/no-logger'; import assert from 'assert'; -import type User from '../../../lib/types/user'; import { randomId } from '../../../lib/util/random-id'; import type { IConstraint, @@ -10,6 +9,7 @@ import type { } from '../../../lib/types/model'; import { type IUnleashTest, setupApp } from '../helpers/test-helper'; import type { UpsertSegmentSchema } from '../../../lib/openapi'; +import { TEST_AUDIT_USER } from '../../../lib/types'; interface ISeedSegmentSpec { featuresCount: number; @@ -37,8 +37,7 @@ const createSegment = ( app: IUnleashTest, postData: UpsertSegmentSchema, ): Promise => { - const user = { email: 'test@example.com' } as User; - return app.services.segmentService.create(postData, user); + return app.services.segmentService.create(postData, TEST_AUDIT_USER); }; const createFeatureToggle = ( diff --git a/src/test/e2e/services/access-service.e2e.test.ts b/src/test/e2e/services/access-service.e2e.test.ts index d3f37559d95..1870fe04e49 100644 --- a/src/test/e2e/services/access-service.e2e.test.ts +++ b/src/test/e2e/services/access-service.e2e.test.ts @@ -9,11 +9,13 @@ import type { import * as permissions from '../../../lib/types/permissions'; import { RoleName } from '../../../lib/types/model'; -import type { - ICreateGroupUserModel, - IUnleashStores, - IUser, - IUserAccessOverview, +import { + type ICreateGroupUserModel, + type IUnleashStores, + type IUser, + type IUserAccessOverview, + SYSTEM_USER_AUDIT, + TEST_AUDIT_USER, } from '../../../lib/types'; import { createTestConfig } from '../../config/test-config'; import { DEFAULT_PROJECT } from '../../../lib/types/project'; @@ -30,6 +32,7 @@ import { BadDataError } from '../../../lib/error'; import type FeatureToggleService from '../../../lib/features/feature-toggle/feature-toggle-service'; import type { ProjectService } from '../../../lib/services'; import type { IRole } from '../../../lib/types/stores/access-store'; +import { extractAuditInfoFromUser } from '../../../lib/util'; let db: ITestDb; let stores: IUnleashStores; @@ -78,12 +81,15 @@ const createGroup = async ({ let roleIndex = 0; const createRole = async (rolePermissions: PermissionRef[]) => { - return accessService.createRole({ - name: `Role ${roleIndex}`, - description: `Role ${roleIndex++} description`, - permissions: rolePermissions, - createdByUserId: TEST_USER_ID, - }); + return accessService.createRole( + { + name: `Role ${roleIndex}`, + description: `Role ${roleIndex++} description`, + permissions: rolePermissions, + createdByUserId: TEST_USER_ID, + }, + SYSTEM_USER_AUDIT, + ); }; const hasCommonProjectAccess = async (user, projectName, condition) => { @@ -261,6 +267,7 @@ beforeAll(async () => { name: 'Some project', }, testAdmin, + TEST_AUDIT_USER, ); await projectService.createProject( @@ -269,6 +276,7 @@ beforeAll(async () => { name: 'Another project not used', }, testAdmin, + TEST_AUDIT_USER, ); }); @@ -724,7 +732,7 @@ test('Should be denied access to delete a role that is in use', async () => { name: 'New project', description: 'Blah', }; - await projectService.createProject(project, user); + await projectService.createProject(project, user, TEST_AUDIT_USER); const projectMember = await stores.userStore.insert({ name: 'CustomProjectMember', @@ -746,11 +754,11 @@ test('Should be denied access to delete a role that is in use', async () => { project.id, customRole.id, projectMember.id, - 'systemuser', + SYSTEM_USER_AUDIT, ); try { - await accessService.deleteRole(customRole.id, 'testuser', TEST_USER_ID); + await accessService.deleteRole(customRole.id, TEST_AUDIT_USER); } 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.', @@ -772,16 +780,19 @@ test('Should be denied move feature toggle to project where the user does not ha name: 'New project', description: 'Blah', }; - await projectService.createProject(projectOrigin, user); - await projectService.createProject(projectDest, editorUser2); + await projectService.createProject(projectOrigin, user, TEST_AUDIT_USER); + await projectService.createProject( + projectDest, + editorUser2, + TEST_AUDIT_USER, + ); const featureToggle = { name: 'moveableToggle' }; await featureToggleService.createFeatureToggle( projectOrigin.id, featureToggle, - user.username, - user.id, + extractAuditInfoFromUser(user), ); try { @@ -790,6 +801,7 @@ test('Should be denied move feature toggle to project where the user does not ha featureToggle.name, user, projectOrigin.id, + TEST_AUDIT_USER, ); } catch (e) { expect(e.name).toContain('Permission'); @@ -813,16 +825,23 @@ test('Should be allowed move feature toggle to project when the user has access' name: 'New project', description: 'Blah', }; - await projectService.createProject(projectOrigin, user); - await projectService.createProject(projectDest, user); + await projectService.createProject( + projectOrigin, + user, + extractAuditInfoFromUser(user), + ); + await projectService.createProject( + projectDest, + user, + extractAuditInfoFromUser(user), + ); const featureToggle = { name: 'moveableToggle2' }; await featureToggleService.createFeatureToggle( projectOrigin.id, featureToggle, - user.username, - user.id, + extractAuditInfoFromUser(user), ); await projectService.changeProject( @@ -830,6 +849,7 @@ test('Should be allowed move feature toggle to project when the user has access' featureToggle.name, user, projectOrigin.id, + extractAuditInfoFromUser(user), ); }); @@ -845,7 +865,7 @@ test('Should not be allowed to edit a root role', async () => { }; try { - await accessService.updateRole(roleUpdate); + await accessService.updateRole(roleUpdate, TEST_AUDIT_USER); } catch (e) { expect(e.toString()).toBe( 'InvalidOperationError: You cannot change built in roles.', @@ -859,7 +879,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, 'testuser', TEST_USER_ID); + await accessService.deleteRole(editRole.id, TEST_AUDIT_USER); } catch (e) { expect(e.toString()).toBe( 'InvalidOperationError: You cannot change built in roles.', @@ -879,7 +899,7 @@ test('Should not be allowed to edit a project role', async () => { }; try { - await accessService.updateRole(roleUpdate); + await accessService.updateRole(roleUpdate, TEST_AUDIT_USER); } catch (e) { expect(e.toString()).toBe( 'InvalidOperationError: You cannot change built in roles.', @@ -893,7 +913,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, 'testuser', TEST_USER_ID); + await accessService.deleteRole(ownerRole.id, TEST_AUDIT_USER); } catch (e) { expect(e.toString()).toBe( 'InvalidOperationError: You cannot change built in roles.', @@ -909,7 +929,7 @@ test('Should be allowed move feature toggle to project when given access through const viewerUser = await createUser(readRole.id); - await projectService.createProject(project, editorUser); + await projectService.createProject(project, editorUser, TEST_AUDIT_USER); const groupWithProjectAccess = await createGroup({ users: [{ user: viewerUser }], @@ -936,7 +956,7 @@ test('Should not lose user role access when given permissions from a group', asy }; const user = editorUser; - await projectService.createProject(project, user); + await projectService.createProject(project, user, TEST_AUDIT_USER); const groupWithNoAccess = await createGroup({ users: [{ user }], @@ -968,8 +988,16 @@ test('Should allow user to take multiple group roles and have expected permissio const viewerUser = await createUser(readRole.id); - await projectService.createProject(projectForCreate, editorUser); - await projectService.createProject(projectForDelete, editorUser); + await projectService.createProject( + projectForCreate, + editorUser, + TEST_AUDIT_USER, + ); + await projectService.createProject( + projectForDelete, + editorUser, + TEST_AUDIT_USER, + ); const groupWithCreateAccess = await createGroup({ users: [{ user: viewerUser }], @@ -1855,14 +1883,17 @@ test('access overview should have group access for groups that they are in', asy test('access overview should include users with custom root roles', async () => { const email = 'ratatoskr@yggdrasil.com'; - const customRole = await accessService.createRole({ - name: 'Mischievous Messenger', - type: CUSTOM_ROOT_ROLE_TYPE, - description: - 'A squirrel that runs up and down the world tree, carrying messages.', - permissions: [{ name: permissions.CREATE_ADDON }], - createdByUserId: 1, - }); + const customRole = await accessService.createRole( + { + name: 'Mischievous Messenger', + type: CUSTOM_ROOT_ROLE_TYPE, + description: + 'A squirrel that runs up and down the world tree, carrying messages.', + permissions: [{ name: permissions.CREATE_ADDON }], + createdByUserId: 1, + }, + SYSTEM_USER_AUDIT, + ); const { userStore } = stores; const user = await userStore.insert({ diff --git a/src/test/e2e/services/addon-service.e2e.test.ts b/src/test/e2e/services/addon-service.e2e.test.ts index 9d5bbe7f54c..ddcfb5d1f39 100644 --- a/src/test/e2e/services/addon-service.e2e.test.ts +++ b/src/test/e2e/services/addon-service.e2e.test.ts @@ -2,7 +2,7 @@ import dbInit, { type ITestDb } from '../helpers/database-init'; import getLogger from '../../fixtures/no-logger'; import { createTestConfig } from '../../config/test-config'; import AddonService from '../../../lib/services/addon-service'; -import type { IUnleashStores } from '../../../lib/types'; +import { type IUnleashStores, TEST_AUDIT_USER } from '../../../lib/types'; import SimpleAddon from '../../../lib/services/addon-service-test-simple-addon'; import TagTypeService from '../../../lib/features/tag-type/tag-type-service'; @@ -78,9 +78,9 @@ test('should only return active addons', async () => { description: '', }; - 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); + await addonService.createAddon(config, TEST_AUDIT_USER); + await addonService.createAddon(config2, TEST_AUDIT_USER); + await addonService.createAddon(config3, TEST_AUDIT_USER); 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 7a6e79757bb..f423e2c6530 100644 --- a/src/test/e2e/services/api-token-service.e2e.test.ts +++ b/src/test/e2e/services/api-token-service.e2e.test.ts @@ -11,7 +11,7 @@ import { addDays, subDays } from 'date-fns'; import type ProjectService from '../../../lib/features/project/project-service'; import { createProjectService } from '../../../lib/features'; import { EventService } from '../../../lib/services'; -import type { IUnleashStores } from '../../../lib/types'; +import { type IUnleashStores, TEST_AUDIT_USER } from '../../../lib/types'; let db: ITestDb; let stores: IUnleashStores; @@ -43,7 +43,7 @@ beforeAll(async () => { }); projectService = createProjectService(db.rawDatabase, config); - await projectService.createProject(project, user); + await projectService.createProject(project, user, TEST_AUDIT_USER); apiTokenService = new ApiTokenService(stores, config, eventService); }); @@ -123,10 +123,10 @@ test('should update expiry of token', async () => { project: '*', environment: DEFAULT_ENV, }, - 'tester', + TEST_AUDIT_USER, ); - await apiTokenService.updateExpiry(token.secret, newTime, 'tester', -9999); + await apiTokenService.updateExpiry(token.secret, newTime, TEST_AUDIT_USER); 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 62704bbf4e2..85ed9ae335c 100644 --- a/src/test/e2e/services/group-service.e2e.test.ts +++ b/src/test/e2e/services/group-service.e2e.test.ts @@ -3,7 +3,12 @@ import getLogger from '../../fixtures/no-logger'; import { createTestConfig } from '../../config/test-config'; import { GroupService } from '../../../lib/services/group-service'; import { EventService } from '../../../lib/services'; -import type { IGroupStore, IUnleashStores, IUser } from '../../../lib/types'; +import { + type IGroupStore, + type IUnleashStores, + type IUser, + TEST_AUDIT_USER, +} from '../../../lib/types'; let stores: IUnleashStores; let db: ITestDb; @@ -185,8 +190,7 @@ test('adding a root role to a group with a project role should not fail', async name: 'root_group', description: 'root_group', }, - 'test', - -9999, + TEST_AUDIT_USER, ); await stores.accessStore.addGroupToRole(group.id, 1, 'test', 'default'); @@ -200,8 +204,7 @@ test('adding a root role to a group with a project role should not fail', async createdAt: new Date(), createdBy: 'test', }, - 'test', - -9999, + TEST_AUDIT_USER, ); expect(updatedGroup).toMatchObject({ @@ -257,8 +260,7 @@ test('adding a nonexistent role to a group should fail', async () => { createdAt: new Date(), createdBy: 'test', }, - 'test', - -9999, + TEST_AUDIT_USER, ); }).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-health-service.e2e.test.ts b/src/test/e2e/services/project-health-service.e2e.test.ts index a27cbdd8e71..77f1c6db3d4 100644 --- a/src/test/e2e/services/project-health-service.e2e.test.ts +++ b/src/test/e2e/services/project-health-service.e2e.test.ts @@ -2,7 +2,7 @@ import dbInit, { type ITestDb } from '../helpers/database-init'; import getLogger from '../../fixtures/no-logger'; import ProjectHealthService from '../../../lib/services/project-health-service'; import { createTestConfig } from '../../config/test-config'; -import type { IUnleashStores } from '../../../lib/types'; +import { type IUnleashStores, TEST_AUDIT_USER } from '../../../lib/types'; import type { IUser } from '../../../lib/server-impl'; import { createProjectService } from '../../../lib/features'; import type { ProjectService } from '../../../lib/services'; @@ -38,7 +38,11 @@ test('Project with no stale toggles should have 100% health rating', async () => name: 'Health rating', description: 'Fancy', }; - const savedProject = await projectService.createProject(project, user); + const savedProject = await projectService.createProject( + project, + user, + TEST_AUDIT_USER, + ); await stores.featureToggleStore.create('health-rating', { name: 'health-rating-not-stale', description: 'new', @@ -62,7 +66,11 @@ test('Project with two stale toggles and two non stale should have 50% health ra name: 'Health rating', description: 'Fancy', }; - const savedProject = await projectService.createProject(project, user); + const savedProject = await projectService.createProject( + project, + user, + TEST_AUDIT_USER, + ); await stores.featureToggleStore.create('health-rating-2', { name: 'health-rating-2-not-stale', description: 'new', @@ -98,7 +106,11 @@ test('Project with one non-stale, one potentially stale and one stale should hav name: 'Health rating', description: 'Fancy', }; - const savedProject = await projectService.createProject(project, user); + const savedProject = await projectService.createProject( + project, + user, + TEST_AUDIT_USER, + ); await stores.featureToggleStore.create('health-rating-3', { name: 'health-rating-3-not-stale', description: 'new', diff --git a/src/test/e2e/services/reset-token-service.e2e.test.ts b/src/test/e2e/services/reset-token-service.e2e.test.ts index 4a3bf8c03e2..a156c6cfaa6 100644 --- a/src/test/e2e/services/reset-token-service.e2e.test.ts +++ b/src/test/e2e/services/reset-token-service.e2e.test.ts @@ -13,7 +13,7 @@ import SettingService from '../../../lib/services/setting-service'; import FakeSettingStore from '../../fixtures/fake-setting-store'; import { GroupService } from '../../../lib/services/group-service'; import { EventService } from '../../../lib/services'; -import type { IUnleashStores } from '../../../lib/types'; +import { type IUnleashStores, TEST_AUDIT_USER } from '../../../lib/types'; const config: IUnleashConfig = createTestConfig(); @@ -57,15 +57,21 @@ beforeAll(async () => { settingService, }); - adminUser = await userService.createUser({ - username: 'admin@test.com', - rootRole: 1, - }); + adminUser = await userService.createUser( + { + username: 'admin@test.com', + rootRole: 1, + }, + TEST_AUDIT_USER, + ); - userToCreateResetFor = await userService.createUser({ - username: 'test@test.com', - rootRole: 2, - }); + userToCreateResetFor = await userService.createUser( + { + username: 'test@test.com', + rootRole: 2, + }, + TEST_AUDIT_USER, + ); userIdToCreateResetFor = userToCreateResetFor.id; }); @@ -78,7 +84,7 @@ afterAll(async () => { test('Should create a reset link', async () => { const url = await resetTokenService.createResetPasswordUrl( userIdToCreateResetFor, - adminUser.username, + adminUser.username!, ); expect(url.toString().substring(0, url.toString().indexOf('='))).toBe( diff --git a/src/test/e2e/services/setting-service.test.ts b/src/test/e2e/services/setting-service.test.ts index b0405a1855e..a051b193392 100644 --- a/src/test/e2e/services/setting-service.test.ts +++ b/src/test/e2e/services/setting-service.test.ts @@ -8,6 +8,7 @@ import { SETTING_UPDATED, } from '../../../lib/types/events'; import { EventService } from '../../../lib/services'; +import { TEST_AUDIT_USER } from '../../../lib/types'; let stores: IUnleashStores; let db: ITestDb; @@ -30,13 +31,7 @@ afterAll(async () => { test('Can create new setting', async () => { const someData = { some: 'blob' }; - await service.insert( - 'some-setting', - someData, - 'test-user', - TEST_USER_ID, - false, - ); + await service.insert('some-setting', someData, TEST_AUDIT_USER, false); const actual = await service.get('some-setting'); expect(actual).toStrictEqual(someData); @@ -50,8 +45,8 @@ test('Can create new setting', async () => { test('Can delete setting', async () => { const someData = { some: 'blob' }; - await service.insert('some-setting', someData, 'test-user', TEST_USER_ID); - await service.delete('some-setting', 'test-user', TEST_USER_ID); + await service.insert('some-setting', someData, TEST_AUDIT_USER); + await service.delete('some-setting', TEST_AUDIT_USER); const actual = await service.get('some-setting'); expect(actual).toBeUndefined(); @@ -65,14 +60,9 @@ 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', TEST_USER_ID); + await service.insert(property, someData, TEST_AUDIT_USER); - await service.insert( - property, - { password: 'changed' }, - 'a-user-in-places', - TEST_USER_ID, - ); + await service.insert(property, { password: 'changed' }, TEST_AUDIT_USER); const actual = await service.get(property); const { eventStore } = stores; @@ -80,24 +70,17 @@ 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', TEST_USER_ID); + await service.delete(property, TEST_AUDIT_USER); }); test('Can update setting', async () => { const { eventStore } = stores; const someData = { some: 'blob' }; - await service.insert( - 'updated-setting', - someData, - 'test-user', - TEST_USER_ID, - false, - ); + await service.insert('updated-setting', someData, TEST_AUDIT_USER, false); await service.insert( 'updated-setting', { ...someData, test: 'fun' }, - 'test-user', - TEST_USER_ID, + TEST_AUDIT_USER, false, ); const updatedEvents = await eventStore.searchEvents({ diff --git a/src/test/e2e/services/user-service.e2e.test.ts b/src/test/e2e/services/user-service.e2e.test.ts index 484e0ef23bf..b69a724d465 100644 --- a/src/test/e2e/services/user-service.e2e.test.ts +++ b/src/test/e2e/services/user-service.e2e.test.ts @@ -13,7 +13,6 @@ import SettingService from '../../../lib/services/setting-service'; import { simpleAuthSettingsKey } from '../../../lib/types/settings/simple-auth-settings'; import { addDays, minutesToMilliseconds } from 'date-fns'; import { GroupService } from '../../../lib/services/group-service'; -import { randomId } from '../../../lib/util/random-id'; import { BadDataError } from '../../../lib/error'; import PasswordMismatch from '../../../lib/error/password-mismatch'; import { EventService } from '../../../lib/services'; @@ -21,6 +20,8 @@ import { CREATE_ADDON, type IUnleashStores, type IUserStore, + SYSTEM_USER_AUDIT, + TEST_AUDIT_USER, USER_CREATED, USER_DELETED, USER_UPDATED, @@ -68,17 +69,20 @@ beforeAll(async () => { const rootRoles = await accessService.getRootRoles(); adminRole = rootRoles.find((r) => r.name === RoleName.ADMIN)!; viewerRole = rootRoles.find((r) => r.name === RoleName.VIEWER)!; - customRole = await accessService.createRole({ - name: 'Custom role', - type: CUSTOM_ROOT_ROLE_TYPE, - description: 'A custom role', - permissions: [ - { - name: CREATE_ADDON, - }, - ], - createdByUserId: 1, - }); + customRole = await accessService.createRole( + { + name: 'Custom role', + type: CUSTOM_ROOT_ROLE_TYPE, + description: 'A custom role', + permissions: [ + { + name: CREATE_ADDON, + }, + ], + createdByUserId: 1, + }, + SYSTEM_USER_AUDIT, + ); }); afterAll(async () => { @@ -106,11 +110,14 @@ test('should create initial admin user', async () => { }); test('should not init default user if we already have users', async () => { - await userService.createUser({ - username: 'test', - password: 'A very strange P4ssw0rd_', - rootRole: adminRole.id, - }); + await userService.createUser( + { + username: 'test', + password: 'A very strange P4ssw0rd_', + rootRole: adminRole.id, + }, + SYSTEM_USER_AUDIT, + ); await userService.initAdminUser({ createAdminUser: true, initialAdminUser: { @@ -129,16 +136,22 @@ test('should not init default user if we already have users', async () => { test('should not be allowed to create existing user', async () => { await userStore.insert({ username: 'test', name: 'Hans Mola' }); await expect(async () => - userService.createUser({ username: 'test', rootRole: adminRole.id }), + userService.createUser( + { username: 'test', rootRole: adminRole.id }, + TEST_AUDIT_USER, + ), ).rejects.toThrow(Error); }); test('should create user with password', async () => { - await userService.createUser({ - username: 'test', - password: 'A very strange P4ssw0rd_', - rootRole: adminRole.id, - }); + await userService.createUser( + { + username: 'test', + password: 'A very strange P4ssw0rd_', + rootRole: adminRole.id, + }, + TEST_AUDIT_USER, + ); const user = await userService.loginUser( 'test', 'A very strange P4ssw0rd_', @@ -147,10 +160,13 @@ test('should create user with password', async () => { }); test('should create user with rootRole in audit-log', async () => { - const user = await userService.createUser({ - username: 'test', - rootRole: viewerRole.id, - }); + const user = await userService.createUser( + { + username: 'test', + rootRole: viewerRole.id, + }, + TEST_AUDIT_USER, + ); const { events } = await eventService.getEvents(); expect(events[0].type).toBe(USER_CREATED); @@ -160,12 +176,18 @@ test('should create user with rootRole in audit-log', async () => { }); test('should update user with rootRole in audit-log', async () => { - const user = await userService.createUser({ - username: 'test', - rootRole: viewerRole.id, - }); + const user = await userService.createUser( + { + username: 'test', + rootRole: viewerRole.id, + }, + TEST_AUDIT_USER, + ); - await userService.updateUser({ id: user.id, rootRole: adminRole.id }); + await userService.updateUser( + { id: user.id, rootRole: adminRole.id }, + TEST_AUDIT_USER, + ); const { events } = await eventService.getEvents(); expect(events[0].type).toBe(USER_UPDATED); @@ -175,12 +197,15 @@ test('should update user with rootRole in audit-log', async () => { }); test('should remove user with rootRole in audit-log', async () => { - const user = await userService.createUser({ - username: 'test', - rootRole: viewerRole.id, - }); + const user = await userService.createUser( + { + username: 'test', + rootRole: viewerRole.id, + }, + TEST_AUDIT_USER, + ); - await userService.deleteUser(user.id); + await userService.deleteUser(user.id, TEST_AUDIT_USER); const { events } = await eventService.getEvents(); expect(events[0].type).toBe(USER_DELETED); @@ -190,13 +215,16 @@ test('should remove user with rootRole in audit-log', async () => { }); test('should not be able to login with deleted user', async () => { - const user = await userService.createUser({ - username: 'deleted_user', - password: 'unleash4all', - rootRole: adminRole.id, - }); + const user = await userService.createUser( + { + username: 'deleted_user', + password: 'unleash4all', + rootRole: adminRole.id, + }, + TEST_AUDIT_USER, + ); - await userService.deleteUser(user.id); + await userService.deleteUser(user.id, TEST_AUDIT_USER); await expect( userService.loginUser('deleted_user', 'unleash4all'), @@ -208,11 +236,14 @@ test('should not be able to login with deleted user', async () => { }); test('should not be able to login without password_hash on user', async () => { - const user = await userService.createUser({ - username: 'deleted_user', - password: 'unleash4all', - rootRole: adminRole.id, - }); + const user = await userService.createUser( + { + username: 'deleted_user', + password: 'unleash4all', + rootRole: adminRole.id, + }, + TEST_AUDIT_USER, + ); /*@ts-ignore: we are testing for null on purpose! */ await userStore.setPasswordHash(user.id, null); @@ -230,16 +261,18 @@ test('should not login user if simple auth is disabled', async () => { await settingService.insert( simpleAuthSettingsKey, { disabled: true }, - randomId(), - -9999, + TEST_AUDIT_USER, true, ); - await userService.createUser({ - username: 'test_no_pass', - password: 'A very strange P4ssw0rd_', - rootRole: adminRole.id, - }); + await userService.createUser( + { + username: 'test_no_pass', + password: 'A very strange P4ssw0rd_', + rootRole: adminRole.id, + }, + TEST_AUDIT_USER, + ); await expect(async () => { await userService.loginUser('test_no_pass', 'A very strange P4ssw0rd_'); @@ -250,22 +283,28 @@ test('should not login user if simple auth is disabled', async () => { test('should login for user _without_ password', async () => { const email = 'some@test.com'; - await userService.createUser({ - email, - password: 'A very strange P4ssw0rd_', - rootRole: adminRole.id, - }); + await userService.createUser( + { + email, + password: 'A very strange P4ssw0rd_', + rootRole: adminRole.id, + }, + TEST_AUDIT_USER, + ); const user = await userService.loginUserWithoutPassword(email); expect(user.email).toBe(email); }); test('should get user with root role', async () => { const email = 'some@test.com'; - const u = await userService.createUser({ - email, - password: 'A very strange P4ssw0rd_', - rootRole: adminRole.id, - }); + const u = await userService.createUser( + { + email, + password: 'A very strange P4ssw0rd_', + rootRole: adminRole.id, + }, + TEST_AUDIT_USER, + ); const user = await userService.getUser(u.id); expect(user.email).toBe(email); expect(user.id).toBe(u.id); @@ -274,11 +313,14 @@ test('should get user with root role', async () => { test('should get user with root role by name', async () => { const email = 'some2@test.com'; - const u = await userService.createUser({ - email, - password: 'A very strange P4ssw0rd_', - rootRole: RoleName.ADMIN, - }); + const u = await userService.createUser( + { + email, + password: 'A very strange P4ssw0rd_', + rootRole: RoleName.ADMIN, + }, + TEST_AUDIT_USER, + ); const user = await userService.getUser(u.id); expect(user.email).toBe(email); expect(user.id).toBe(u.id); @@ -287,11 +329,14 @@ test('should get user with root role by name', async () => { test("deleting a user should delete the user's sessions", async () => { const email = 'some@test.com'; - const user = await userService.createUser({ - email, - password: 'A very strange P4ssw0rd_', - rootRole: adminRole.id, - }); + const user = await userService.createUser( + { + email, + password: 'A very strange P4ssw0rd_', + rootRole: adminRole.id, + }, + TEST_AUDIT_USER, + ); const testComSession = { sid: 'xyz321', sess: { @@ -308,7 +353,7 @@ test("deleting a user should delete the user's sessions", async () => { await sessionService.insertSession(testComSession); const userSessions = await sessionService.getSessionsForUser(user.id); expect(userSessions.length).toBe(1); - await userService.deleteUser(user.id); + await userService.deleteUser(user.id, TEST_AUDIT_USER); await expect(async () => sessionService.getSessionsForUser(user.id), ).rejects.toThrow(NotFoundError); @@ -316,16 +361,22 @@ test("deleting a user should delete the user's sessions", async () => { test('updating a user without an email should not strip the email', async () => { const email = 'some@test.com'; - const user = await userService.createUser({ - email, - password: 'A very strange P4ssw0rd_', - rootRole: adminRole.id, - }); + const user = await userService.createUser( + { + email, + password: 'A very strange P4ssw0rd_', + rootRole: adminRole.id, + }, + TEST_AUDIT_USER, + ); - await userService.updateUser({ - id: user.id, - name: 'some', - }); + await userService.updateUser( + { + id: user.id, + name: 'some', + }, + TEST_AUDIT_USER, + ); const updatedUser = await userService.getUser(user.id); expect(updatedUser.email).toBe(email); @@ -362,11 +413,14 @@ test('should throw if rootRole is wrong via SSO', async () => { test('should update user name when signing in via SSO', async () => { const email = 'some@test.com'; - const originalUser = await userService.createUser({ - email, - rootRole: RoleName.VIEWER, - name: 'some', - }); + const originalUser = await userService.createUser( + { + email, + rootRole: RoleName.VIEWER, + name: 'some', + }, + TEST_AUDIT_USER, + ); await userService.loginUserSSO({ email, @@ -384,11 +438,14 @@ test('should update user name when signing in via SSO', async () => { test('should update name if it is different via SSO', async () => { const email = 'some@test.com'; - const originalUser = await userService.createUser({ - email, - rootRole: RoleName.VIEWER, - name: 'some', - }); + const originalUser = await userService.createUser( + { + email, + rootRole: RoleName.VIEWER, + name: 'some', + }, + TEST_AUDIT_USER, + ); await userService.loginUserSSO({ email, diff --git a/src/test/e2e/stores/event-store.e2e.test.ts b/src/test/e2e/stores/event-store.e2e.test.ts index d10d57502bc..f66d38f6641 100644 --- a/src/test/e2e/stores/event-store.e2e.test.ts +++ b/src/test/e2e/stores/event-store.e2e.test.ts @@ -10,13 +10,19 @@ import { import dbInit, { type ITestDb } from '../helpers/database-init'; import getLogger from '../../fixtures/no-logger'; import type { IEventStore } from '../../../lib/types/stores/event-store'; -import type { IUnleashStores } from '../../../lib/types'; +import type { IAuditUser, IUnleashStores } from '../../../lib/types'; let db: ITestDb; let stores: IUnleashStores; let eventStore: IEventStore; const TEST_USER_ID = -9999; +const testAudit: IAuditUser = { + id: TEST_USER_ID, + username: 'test@example.com', + ip: '127.0.0.1', +}; + beforeAll(async () => { db = await dbInit('event_store_serial', getLogger); stores = db.stores; @@ -37,6 +43,7 @@ test('Should include id and createdAt when saving', async () => { type: APPLICATION_CREATED, createdBy: '127.0.0.1', createdByUserId: TEST_USER_ID, + ip: '127.0.0.1', data: { clientIp: '127.0.0.1', appName: 'test1', @@ -60,6 +67,7 @@ test('Should include empty tags array for new event', async () => { type: FEATURE_CREATED, createdBy: 'me@mail.com', createdByUserId: TEST_USER_ID, + ip: '127.0.0.1', data: { name: 'someName', enabled: true, @@ -112,7 +120,7 @@ test('Should be able to store multiple events at once', async () => { }, tags: [{ type: 'simple', value: 'mytest' }], }; - const seen = []; + const seen: IEvent[] = []; eventStore.on(APPLICATION_CREATED, (e) => seen.push(e)); await eventStore.batchStore([event1, event2, event3]); await eventStore.publishUnannouncedEvents(); @@ -205,8 +213,7 @@ test('Should get all events of type', async () => { ? new FeatureCreatedEvent({ project: data.project, featureName: data.name, - createdBy: 'test-user', - createdByUserId: TEST_USER_ID, + auditUser: testAudit, data, }) @@ -214,8 +221,7 @@ test('Should get all events of type', async () => { project: data.project, preData: data, featureName: data.name, - createdBy: 'test-user', - createdByUserId: TEST_USER_ID, + auditUser: testAudit, tags: [], }); diff --git a/src/test/e2e/users/inactive/inactive-users-service.test.ts b/src/test/e2e/users/inactive/inactive-users-service.test.ts index cc28c1a3306..6259a3c2a1c 100644 --- a/src/test/e2e/users/inactive/inactive-users-service.test.ts +++ b/src/test/e2e/users/inactive/inactive-users-service.test.ts @@ -14,6 +14,7 @@ import UserService from '../../../../lib/services/user-service'; import { ADMIN, type IUnleashStores, type IUser } from '../../../../lib/types'; import type { InactiveUsersService } from '../../../../lib/users/inactive/inactive-users-service'; import { createInactiveUsersService } from '../../../../lib/users'; +import { extractAuditInfoFromUser } from '../../../../lib/util'; let db: ITestDb; let stores: IUnleashStores; @@ -160,7 +161,7 @@ describe('Inactive users service', () => { .getInactiveUsers() .then((users) => users.map((user) => user.id)); await inactiveUserService.deleteInactiveUsers( - deletionUser, + extractAuditInfoFromUser(deletionUser), usersToDelete, ); await expect( @@ -175,7 +176,7 @@ describe('Inactive users service', () => { .getInactiveUsers() .then((users) => users.map((user) => user.id)); await inactiveUserService.deleteInactiveUsers( - deletionUser, + extractAuditInfoFromUser(deletionUser), usersToDelete, ); await expect( @@ -190,7 +191,7 @@ describe('Inactive users service', () => { .getInactiveUsers() .then((users) => users.map((user) => user.id)); await inactiveUserService.deleteInactiveUsers( - deletionUser, + extractAuditInfoFromUser(deletionUser), usersToDelete, ); await expect(userService.getUser(9595)).resolves.toBeTruthy(); @@ -203,7 +204,7 @@ describe('Inactive users service', () => { .getInactiveUsers() .then((users) => users.map((user) => user.id)); await inactiveUserService.deleteInactiveUsers( - deletionUser, + extractAuditInfoFromUser(deletionUser), usersToDelete, ); await expect(userService.getUser(9595)).resolves.toBeTruthy(); @@ -218,7 +219,7 @@ describe('Inactive users service', () => { .getInactiveUsers() .then((users) => users.map((user) => user.id)); await inactiveUserService.deleteInactiveUsers( - deletionUser, + extractAuditInfoFromUser(deletionUser), usersToDelete, ); await expect(userService.getUser(9595)).rejects.toBeTruthy();