1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-10-27 11:02:16 +01:00

feat: adds created_by_user_id to all events (#5619)

### What
Adds `createdByUserId` to all events exposed by unleash. In addition
this PR updates all tests and usages of the methods in this codebase to
include the required number.
This commit is contained in:
Christopher Kolstad 2023-12-14 13:45:25 +01:00 committed by GitHub
parent 772682176e
commit bfa82d79bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
88 changed files with 1340 additions and 287 deletions

View File

@ -45,6 +45,7 @@ test('Should call datadog webhook', async () => {
createdAt: new Date(),
type: FEATURE_CREATED,
createdBy: 'some@user.com',
createdByUserId: -1337,
featureName: 'some-toggle',
data: {
name: 'some-toggle',
@ -74,6 +75,7 @@ test('Should call datadog webhook for archived toggle', async () => {
createdAt: new Date(),
type: FEATURE_ARCHIVED,
createdBy: 'some@user.com',
createdByUserId: -1337,
featureName: 'some-toggle',
data: {
name: 'some-toggle',
@ -102,6 +104,7 @@ test('Should call datadog webhook for archived toggle with project info', async
type: FEATURE_ARCHIVED,
createdBy: 'some@user.com',
featureName: 'some-toggle',
createdByUserId: -1337,
project: 'some-project',
data: {
name: 'some-toggle',
@ -129,6 +132,7 @@ test('Should call datadog webhook for toggled environment', async () => {
createdAt: new Date(),
type: FEATURE_ENVIRONMENT_DISABLED,
createdBy: 'some@user.com',
createdByUserId: -1337,
environment: 'development',
project: 'default',
featureName: 'some-toggle',
@ -160,6 +164,7 @@ test('Should include customHeaders in headers when calling service', async () =>
type: FEATURE_ENVIRONMENT_DISABLED,
createdBy: 'some@user.com',
environment: 'development',
createdByUserId: -1337,
project: 'default',
featureName: 'some-toggle',
data: {
@ -190,6 +195,7 @@ test('Should not include source_type_name when included in the config', async ()
createdAt: new Date(),
type: FEATURE_ENVIRONMENT_DISABLED,
createdBy: 'some@user.com',
createdByUserId: -1337,
environment: 'development',
project: 'default',
featureName: 'some-toggle',
@ -224,6 +230,7 @@ test('Should call datadog webhook with JSON when template set', async () => {
createdAt: new Date(),
type: FEATURE_CREATED,
createdBy: 'some@user.com',
createdByUserId: -1337,
featureName: 'some-toggle',
data: {
name: 'some-toggle',

View File

@ -6,6 +6,7 @@ import {
FEATURE_STRATEGY_REMOVE,
FEATURE_STRATEGY_UPDATE,
IEvent,
SYSTEM_USER_ID,
} from '../types';
import { FeatureEventFormatterMd } from './feature-event-formatter-md';
@ -34,6 +35,7 @@ const testCases: [string, IEvent][] = [
id: 920,
type: FEATURE_STRATEGY_UPDATE,
createdBy: 'user@company.com',
createdByUserId: SYSTEM_USER_ID,
createdAt: new Date('2022-06-01T10:03:11.549Z'),
data: {
id: '3f4bf713-696c-43a4-8ce7-d6c607108858',
@ -67,6 +69,7 @@ const testCases: [string, IEvent][] = [
id: 920,
type: FEATURE_STRATEGY_UPDATE,
createdBy: 'user@company.com',
createdByUserId: SYSTEM_USER_ID,
createdAt: new Date('2022-06-01T10:03:11.549Z'),
data: {
id: '3f4bf713-696c-43a4-8ce7-d6c607108858',
@ -100,6 +103,7 @@ const testCases: [string, IEvent][] = [
id: 920,
type: FEATURE_STRATEGY_UPDATE,
createdBy: 'user@company.com',
createdByUserId: SYSTEM_USER_ID,
createdAt: new Date('2022-06-01T10:03:11.549Z'),
data: {
id: '3f4bf713-696c-43a4-8ce7-d6c607108858',
@ -133,6 +137,7 @@ const testCases: [string, IEvent][] = [
id: 920,
type: FEATURE_STRATEGY_UPDATE,
createdBy: 'user@company.com',
createdByUserId: SYSTEM_USER_ID,
createdAt: new Date('2022-06-01T10:03:11.549Z'),
data: {
id: '3f4bf713-696c-43a4-8ce7-d6c607108858',
@ -174,6 +179,7 @@ const testCases: [string, IEvent][] = [
id: 920,
type: FEATURE_STRATEGY_UPDATE,
createdBy: 'user@company.com',
createdByUserId: SYSTEM_USER_ID,
createdAt: new Date('2022-06-01T10:03:11.549Z'),
data: {
id: '3f4bf713-696c-43a4-8ce7-d6c607108858',
@ -207,6 +213,7 @@ const testCases: [string, IEvent][] = [
id: 919,
type: FEATURE_STRATEGY_ADD,
createdBy: 'user@company.com',
createdByUserId: SYSTEM_USER_ID,
createdAt: new Date('2022-06-01T10:03:08.290Z'),
data: {
id: '3f4bf713-696c-43a4-8ce7-d6c607108858',
@ -231,6 +238,7 @@ const testCases: [string, IEvent][] = [
id: 918,
type: FEATURE_STRATEGY_REMOVE,
createdBy: 'user@company.com',
createdByUserId: SYSTEM_USER_ID,
createdAt: new Date('2022-06-01T10:03:00.229Z'),
data: null,
preData: {
@ -253,6 +261,7 @@ const testCases: [string, IEvent][] = [
id: 39,
type: FEATURE_STRATEGY_UPDATE,
createdBy: 'admin',
createdByUserId: SYSTEM_USER_ID,
createdAt: new Date('2023-02-20T20:23:28.791Z'),
data: {
id: 'f2d34aac-52ec-49d2-82d3-08d710e89eaa',
@ -310,6 +319,7 @@ const testCases: [string, IEvent][] = [
type: FEATURE_STRATEGY_UPDATE,
createdBy: 'admin',
createdAt: new Date('2023-02-20T20:23:28.791Z'),
createdByUserId: SYSTEM_USER_ID,
data: {
id: 'f2d34aac-52ec-49d2-82d3-08d710e89eaa',
name: 'default',
@ -346,6 +356,7 @@ const testCases: [string, IEvent][] = [
id: 920,
type: FEATURE_STRATEGY_UPDATE,
createdBy: 'user@company.com',
createdByUserId: SYSTEM_USER_ID,
createdAt: new Date('2022-06-01T10:03:11.549Z'),
data: {
name: 'userWithId',
@ -385,6 +396,7 @@ const testCases: [string, IEvent][] = [
id: 920,
type: FEATURE_STRATEGY_UPDATE,
createdBy: 'user@company.com',
createdByUserId: SYSTEM_USER_ID,
createdAt: new Date('2022-06-01T10:03:11.549Z'),
data: {
name: 'remoteAddress',
@ -421,6 +433,7 @@ const testCases: [string, IEvent][] = [
type: FEATURE_STRATEGY_UPDATE,
createdBy: 'user@company.com',
createdAt: new Date('2022-06-01T10:03:11.549Z'),
createdByUserId: SYSTEM_USER_ID,
data: {
name: 'applicationHostname',
constraints: [
@ -456,6 +469,7 @@ const testCases: [string, IEvent][] = [
type: FEATURE_STRATEGY_UPDATE,
createdBy: 'user@company.com',
createdAt: new Date('2022-06-01T10:03:11.549Z'),
createdByUserId: SYSTEM_USER_ID,
data: {
name: 'newStrategy',
constraints: [
@ -491,6 +505,7 @@ const testCases: [string, IEvent][] = [
type: CHANGE_REQUEST_SCHEDULED,
createdBy: 'user@company.com',
createdAt: new Date('2022-06-01T10:03:11.549Z'),
createdByUserId: SYSTEM_USER_ID,
data: {
changeRequestId: 1,
},
@ -508,6 +523,7 @@ const testCases: [string, IEvent][] = [
type: CHANGE_REQUEST_SCHEDULED_APPLICATION_SUCCESS,
createdBy: 'user@company.com',
createdAt: new Date('2022-06-01T10:03:11.549Z'),
createdByUserId: SYSTEM_USER_ID,
data: {
changeRequestId: 1,
},
@ -524,6 +540,7 @@ const testCases: [string, IEvent][] = [
id: 920,
type: CHANGE_REQUEST_SCHEDULED_APPLICATION_FAILURE,
createdBy: 'user@company.com',
createdByUserId: SYSTEM_USER_ID,
createdAt: new Date('2022-06-01T10:03:11.549Z'),
data: {
changeRequestId: 1,

View File

@ -1,6 +1,7 @@
import { IEvent, FEATURE_ENVIRONMENT_ENABLED } from '../types/events';
import SlackAppAddon from './slack-app';
import { ChatPostMessageArguments, ErrorCode } from '@slack/web-api';
import { SYSTEM_USER_ID } from '../types';
const slackApiCalls: ChatPostMessageArguments[] = [];
@ -44,6 +45,7 @@ describe('SlackAppAddon', () => {
id: 1,
createdAt: new Date(),
type: FEATURE_ENVIRONMENT_ENABLED,
createdByUserId: SYSTEM_USER_ID,
createdBy: 'some@user.com',
project: 'default',
featureName: 'some-toggle',

View File

@ -9,6 +9,7 @@ import { Logger } from '../logger';
import SlackAddon from './slack';
import noLogger from '../../test/fixtures/no-logger';
import { SYSTEM_USER_ID } from '../types';
let fetchRetryCalls: any[] = [];
@ -44,6 +45,7 @@ test('Should call slack webhook', async () => {
id: 1,
createdAt: new Date(),
type: FEATURE_CREATED,
createdByUserId: SYSTEM_USER_ID,
createdBy: 'some@user.com',
project: 'default',
featureName: 'some-toggle',
@ -74,6 +76,7 @@ test('Should call slack webhook for archived toggle', async () => {
const event: IEvent = {
id: 2,
createdAt: new Date(),
createdByUserId: SYSTEM_USER_ID,
type: FEATURE_ARCHIVED,
featureName: 'some-toggle',
createdBy: 'some@user.com',
@ -101,6 +104,7 @@ test('Should call slack webhook for archived toggle with project info', async ()
const event: IEvent = {
id: 2,
createdAt: new Date(),
createdByUserId: SYSTEM_USER_ID,
type: FEATURE_ARCHIVED,
featureName: 'some-toggle',
project: 'some-project',
@ -129,6 +133,7 @@ test(`Should call webhook for toggled environment`, async () => {
const event: IEvent = {
id: 2,
createdAt: new Date(),
createdByUserId: SYSTEM_USER_ID,
type: FEATURE_ENVIRONMENT_DISABLED,
createdBy: 'some@user.com',
environment: 'development',
@ -159,6 +164,7 @@ test('Should use default channel', async () => {
const event: IEvent = {
id: 3,
createdAt: new Date(),
createdByUserId: SYSTEM_USER_ID,
type: FEATURE_CREATED,
createdBy: 'some@user.com',
featureName: 'some-toggle',
@ -189,6 +195,7 @@ test('Should override default channel with data from tag', async () => {
const event: IEvent = {
id: 4,
createdAt: new Date(),
createdByUserId: SYSTEM_USER_ID,
type: FEATURE_CREATED,
createdBy: 'some@user.com',
featureName: 'some-toggle',
@ -225,6 +232,7 @@ test('Should post to all channels in tags', async () => {
const event: IEvent = {
id: 5,
createdAt: new Date(),
createdByUserId: SYSTEM_USER_ID,
type: FEATURE_CREATED,
createdBy: 'some@user.com',
featureName: 'some-toggle',
@ -269,6 +277,7 @@ test('Should include custom headers from parameters in call to service', async (
id: 2,
createdAt: new Date(),
type: FEATURE_ENVIRONMENT_DISABLED,
createdByUserId: SYSTEM_USER_ID,
createdBy: 'some@user.com',
environment: 'development',
project: 'default',

View File

@ -10,6 +10,7 @@ import {
import TeamsAddon from './teams';
import noLogger from '../../test/fixtures/no-logger';
import { SYSTEM_USER_ID } from '../types';
let fetchRetryCalls: any[];
@ -45,6 +46,7 @@ test('Should call teams webhook', async () => {
id: 1,
createdAt: new Date(),
type: FEATURE_CREATED,
createdByUserId: SYSTEM_USER_ID,
createdBy: 'some@user.com',
featureName: 'some-toggle',
data: {
@ -72,6 +74,7 @@ test('Should call teams webhook for archived toggle', async () => {
const event: IEvent = {
id: 1,
createdAt: new Date(),
createdByUserId: SYSTEM_USER_ID,
type: FEATURE_ARCHIVED,
createdBy: 'some@user.com',
featureName: 'some-toggle',
@ -98,6 +101,7 @@ test('Should call teams webhook for archived toggle with project info', async ()
const event: IEvent = {
id: 1,
createdAt: new Date(),
createdByUserId: SYSTEM_USER_ID,
type: FEATURE_ARCHIVED,
createdBy: 'some@user.com',
featureName: 'some-toggle',
@ -125,6 +129,7 @@ test(`Should call teams webhook for toggled environment`, async () => {
const event: IEvent = {
id: 2,
createdAt: new Date(),
createdByUserId: SYSTEM_USER_ID,
type: FEATURE_ENVIRONMENT_DISABLED,
createdBy: 'some@user.com',
environment: 'development',
@ -154,6 +159,7 @@ test('Should include custom headers in call to teams', async () => {
const event: IEvent = {
id: 2,
createdAt: new Date(),
createdByUserId: SYSTEM_USER_ID,
type: FEATURE_ENVIRONMENT_DISABLED,
createdBy: 'some@user.com',
environment: 'development',

View File

@ -5,6 +5,7 @@ import { FEATURE_CREATED, IEvent } from '../types/events';
import WebhookAddon from './webhook';
import noLogger from '../../test/fixtures/no-logger';
import { SYSTEM_USER_ID } from '../types';
let fetchRetryCalls: any[] = [];
@ -36,6 +37,7 @@ test('Should handle event without "bodyTemplate"', () => {
const event: IEvent = {
id: 1,
createdAt: new Date(),
createdByUserId: SYSTEM_USER_ID,
type: FEATURE_CREATED,
createdBy: 'some@user.com',
featureName: 'some-toggle',
@ -61,6 +63,7 @@ test('Should format event with "bodyTemplate"', () => {
const event: IEvent = {
id: 1,
createdAt: new Date(),
createdByUserId: SYSTEM_USER_ID,
type: FEATURE_CREATED,
createdBy: 'some@user.com',
featureName: 'some-toggle',
@ -90,6 +93,7 @@ test('Should format event with "authorization"', () => {
const event: IEvent = {
id: 1,
createdAt: new Date(),
createdByUserId: SYSTEM_USER_ID,
type: FEATURE_CREATED,
createdBy: 'some@user.com',
featureName: 'some-toggle',
@ -120,6 +124,7 @@ test('Should handle custom headers', async () => {
const event: IEvent = {
id: 1,
createdAt: new Date(),
createdByUserId: SYSTEM_USER_ID,
type: FEATURE_CREATED,
createdBy: 'some@user.com',
featureName: 'some-toggle',

View File

@ -20,6 +20,7 @@ const EVENT_COLUMNS = [
'type',
'created_by',
'created_at',
'created_by_user_id',
'data',
'pre_data',
'tags',
@ -74,6 +75,7 @@ export interface IEventTable {
type: string;
created_by: string;
created_at: Date;
created_by_user_id: number;
data?: any;
pre_data?: any;
feature_name?: string;
@ -364,6 +366,7 @@ class EventStore implements IEventStore {
type: row.type as IEventType,
createdBy: row.created_by,
createdAt: row.created_at,
createdByUserId: row.created_by_user_id,
data: row.data,
preData: row.pre_data,
tags: row.tags || [],
@ -377,6 +380,7 @@ class EventStore implements IEventStore {
return {
type: e.type,
created_by: e.createdBy ?? 'admin',
created_by_user_id: e.createdByUserId,
data: Array.isArray(e.data) ? JSON.stringify(e.data) : e.data,
pre_data: Array.isArray(e.preData)
? JSON.stringify(e.preData)

View File

@ -50,6 +50,7 @@ export class DependentFeaturesService {
projectId,
}: { featureName: string; newFeatureName: string; projectId: string },
user: string,
userId: number,
) {
const parents =
await this.dependentFeaturesReadModel.getParents(featureName);
@ -63,6 +64,7 @@ export class DependentFeaturesService {
variants: parent.variants,
},
user,
userId,
),
),
);
@ -79,6 +81,7 @@ export class DependentFeaturesService {
{ child, projectId },
dependentFeature,
extractUsernameFromUser(user),
user.id,
);
}
@ -86,6 +89,7 @@ export class DependentFeaturesService {
{ child, projectId }: { child: string; projectId: string },
dependentFeature: CreateDependentFeatureSchema,
user: string,
userId: number,
): Promise<void> {
const { enabled, feature: parent, variants } = dependentFeature;
@ -146,6 +150,7 @@ export class DependentFeaturesService {
project: projectId,
featureName: child,
createdBy: user,
createdByUserId: userId,
data: {
feature: parent,
enabled: featureDependency.enabled,
@ -165,6 +170,7 @@ export class DependentFeaturesService {
dependency,
projectId,
extractUsernameFromUser(user),
user.id,
);
}
@ -172,6 +178,7 @@ export class DependentFeaturesService {
dependency: FeatureDependencyId,
projectId: string,
user: string,
userId: number,
): Promise<void> {
await this.dependentFeaturesStore.delete(dependency);
await this.eventService.storeEvent({
@ -179,6 +186,7 @@ export class DependentFeaturesService {
project: projectId,
featureName: dependency.child,
createdBy: user,
createdByUserId: userId,
data: { feature: dependency.parent },
});
}
@ -194,6 +202,7 @@ export class DependentFeaturesService {
features,
projectId,
extractUsernameFromUser(user),
user.id,
);
}
@ -201,6 +210,7 @@ export class DependentFeaturesService {
features: string[],
projectId: string,
user: string,
userId: number,
): Promise<void> {
await this.dependentFeaturesStore.deleteAll(features);
await this.eventService.storeEvents(
@ -209,6 +219,7 @@ export class DependentFeaturesService {
project: projectId,
featureName: feature,
createdBy: user,
createdByUserId: userId,
})),
);
}

View File

@ -120,7 +120,11 @@ class ExportImportController extends Controller {
const query = req.body;
const userName = extractUsername(req);
const data = await this.exportService.export(query, userName);
const data = await this.exportService.export(
query,
userName,
req.user.id,
);
this.openApiService.respondWithValidation(
200,

View File

@ -67,6 +67,7 @@ export type IExportService = {
export(
query: ExportQuerySchema,
userName: string,
userId: number,
): Promise<ExportResultSchema>;
};
@ -287,6 +288,7 @@ export default class ExportImportService
environment: cleanedDto.environment,
type: FEATURES_IMPORTED,
createdBy: extractUsernameFromUser(user),
createdByUserId: user.id,
});
}
@ -387,6 +389,7 @@ export default class ExportImportService
value: tag.tagValue,
},
extractUsernameFromUser(user),
user.id,
);
}
}
@ -404,6 +407,7 @@ export default class ExportImportService
stickiness: contextField.stickiness,
},
extractUsernameFromUser(user),
user.id,
),
),
);
@ -417,6 +421,7 @@ export default class ExportImportService
? this.tagTypeService.createTagType(
tagType,
extractUsernameFromUser(user),
user.id,
)
: Promise.resolve();
}),
@ -457,6 +462,7 @@ export default class ExportImportService
rest as FeatureToggleDTO,
username,
feature.name,
user.id,
);
} else {
await this.featureToggleService.validateName(feature.name);
@ -465,6 +471,7 @@ export default class ExportImportService
dto.project,
rest as FeatureToggleDTO,
username,
user.id,
);
}
}
@ -777,6 +784,7 @@ export default class ExportImportService
async export(
query: ExportQuerySchema,
userName: string,
userId: number,
): Promise<ExportResultSchema> {
const featureNames =
typeof query.tag === 'string'
@ -901,6 +909,7 @@ export default class ExportImportService
await this.eventService.storeEvent({
type: FEATURES_EXPORTED,
createdBy: userName,
createdByUserId: userId,
data: result,
});

View File

@ -20,7 +20,6 @@ import {
import { DEFAULT_ENV } from '../../util';
import {
ContextFieldSchema,
CreateDependentFeatureSchema,
ImportTogglesSchema,
UpsertSegmentSchema,
VariantsSchema,
@ -63,11 +62,13 @@ const createToggle = async (
tags: string[] = [],
projectId: string = 'default',
username: string = 'test',
userId: number = -9999,
) => {
await app.services.featureToggleServiceV2.createFeatureToggle(
projectId,
toggle,
username,
-9999,
);
if (strategy) {
await app.services.featureToggleServiceV2.createStrategy(
@ -89,6 +90,7 @@ const createToggle = async (
value: tag,
},
username,
userId,
);
}),
);

View File

@ -176,7 +176,7 @@ export default class ArchiveController extends Controller {
): Promise<void> {
const { featureName } = req.params;
const user = extractUsername(req);
await this.featureService.deleteFeature(featureName, user);
await this.featureService.deleteFeature(featureName, user, req.user.id);
res.status(200).end();
}
@ -191,6 +191,7 @@ export default class ArchiveController extends Controller {
this.transactionalFeatureToggleService(tx).reviveFeature(
featureName,
userName,
req.user.id,
),
);
res.status(200).end();

View File

@ -659,6 +659,7 @@ export default class ProjectFeaturesController extends Controller {
projectId,
name,
userName,
req.user.id,
replaceGroupId,
);
@ -684,6 +685,7 @@ export default class ProjectFeaturesController extends Controller {
description: req.body.description || undefined,
},
userName,
req.user.id,
);
this.openApiService.respondWithValidation(
@ -733,6 +735,7 @@ export default class ProjectFeaturesController extends Controller {
},
userName,
featureName,
req.user.id,
);
this.openApiService.respondWithValidation(
@ -758,6 +761,7 @@ export default class ProjectFeaturesController extends Controller {
featureName,
extractUsername(req),
req.body,
req.user.id,
);
this.openApiService.respondWithValidation(
200,
@ -800,6 +804,7 @@ export default class ProjectFeaturesController extends Controller {
stale,
userName,
projectId,
req.user.id,
);
res.status(202).end();
}
@ -1108,6 +1113,7 @@ export default class ProjectFeaturesController extends Controller {
value,
{ environment, projectId, featureName },
userName,
req.user.id,
);
res.status(200).json(updatedStrategy);
}
@ -1123,6 +1129,7 @@ export default class ProjectFeaturesController extends Controller {
tags.addedTags,
tags.removedTags,
userName,
req.user.id,
);
res.status(200).end();
}

View File

@ -44,6 +44,7 @@ import {
SKIP_CHANGE_REQUEST,
StrategiesOrderChangedEvent,
StrategyIds,
SYSTEM_USER_ID,
Unsaved,
WeightType,
} from '../../types';
@ -403,6 +404,7 @@ class FeatureToggleService {
featureName: string,
createdBy: string,
operations: Operation[],
createdByUserId: number,
): Promise<FeatureToggle> {
const featureToggle = await this.getFeatureMetadata(featureName);
@ -421,6 +423,7 @@ class FeatureToggleService {
newDocument,
createdBy,
featureName,
createdByUserId,
);
if (featureToggle.stale !== newDocument.stale) {
@ -430,6 +433,7 @@ class FeatureToggleService {
project,
featureName,
createdBy,
createdByUserId,
}),
);
}
@ -472,6 +476,7 @@ class FeatureToggleService {
context,
sortOrders,
createdBy,
user?.id || SYSTEM_USER_ID,
);
}
@ -479,6 +484,7 @@ class FeatureToggleService {
context: IFeatureStrategyContext,
sortOrders: SetStrategySortOrderSchema,
createdBy: string,
createdByUserId: number,
): Promise<Saved<any>> {
const { featureName, environment, projectId: project } = context;
const existingOrder = (
@ -536,6 +542,7 @@ class FeatureToggleService {
createdBy,
preData: eventPreData,
data: eventData,
createdByUserId,
});
await this.eventService.storeEvent(event);
}
@ -555,6 +562,7 @@ class FeatureToggleService {
strategyConfig,
context,
createdBy,
user?.id || SYSTEM_USER_ID,
);
}
@ -562,6 +570,7 @@ class FeatureToggleService {
strategyConfig: Unsaved<IStrategyConfig>,
context: IFeatureStrategyContext,
createdBy: string,
createdByUserId: number,
): Promise<Saved<IStrategyConfig>> {
const { featureName, projectId, environment } = context;
await this.validateFeatureBelongsToProject(context);
@ -638,6 +647,7 @@ class FeatureToggleService {
createdBy,
environment,
data: strategy,
createdByUserId,
}),
);
return strategy;
@ -674,7 +684,13 @@ class FeatureToggleService {
context.environment,
user,
);
return this.unprotectedUpdateStrategy(id, updates, context, userName);
return this.unprotectedUpdateStrategy(
id,
updates,
context,
userName,
user,
);
}
async optionallyDisableFeature(
@ -682,6 +698,7 @@ class FeatureToggleService {
environment: string,
projectId: string,
userName: string,
user?: IUser,
): Promise<void> {
const feature = await this.getFeature({ featureName });
@ -696,6 +713,7 @@ class FeatureToggleService {
environment,
false,
userName,
user,
);
}
}
@ -705,6 +723,7 @@ class FeatureToggleService {
updates: Partial<IStrategyConfig>,
context: IFeatureStrategyContext,
userName: string,
user?: IUser,
): Promise<Saved<IStrategyConfig>> {
const { projectId, environment, featureName } = context;
const existingStrategy = await this.featureStrategiesStore.get(id);
@ -760,6 +779,7 @@ class FeatureToggleService {
createdBy: userName,
data,
preData,
createdByUserId: user?.id || SYSTEM_USER_ID,
}),
);
await this.optionallyDisableFeature(
@ -767,6 +787,7 @@ class FeatureToggleService {
environment,
projectId,
userName,
user,
);
return data;
}
@ -779,6 +800,7 @@ class FeatureToggleService {
value: string | number,
context: IFeatureStrategyContext,
userName: string,
createdByUserId: number,
): Promise<Saved<IStrategyConfig>> {
const { projectId, environment, featureName } = context;
@ -809,6 +831,7 @@ class FeatureToggleService {
createdBy: userName,
data,
preData,
createdByUserId,
}),
);
return data;
@ -837,13 +860,14 @@ class FeatureToggleService {
context.environment,
user,
);
return this.unprotectedDeleteStrategy(id, context, createdBy);
return this.unprotectedDeleteStrategy(id, context, createdBy, user);
}
async unprotectedDeleteStrategy(
id: string,
context: IFeatureStrategyContext,
createdBy: string,
createdByUser?: IUser,
): Promise<void> {
const existingStrategy = await this.featureStrategiesStore.get(id);
const { featureName, projectId, environment } = context;
@ -870,6 +894,7 @@ class FeatureToggleService {
environment,
false,
createdBy,
createdByUser,
);
}
@ -881,6 +906,7 @@ class FeatureToggleService {
project: projectId,
environment,
createdBy,
createdByUserId: createdByUser?.id || SYSTEM_USER_ID,
preData,
}),
);
@ -1053,6 +1079,7 @@ class FeatureToggleService {
*
* Used to retrieve metadata of all feature toggles defined in Unleash.
* @param query - Allow you to limit search based on criteria such as project, tags, namePrefix. See @IFeatureToggleQuery
* @param userId - Used to find / mark features as favorite based on users preferences
* @param archived - Return archived or active toggles
* @returns
*/
@ -1106,6 +1133,7 @@ class FeatureToggleService {
projectId: string,
value: FeatureToggleDTO,
createdBy: string,
createdByUserId: number,
isValidated: boolean = false,
): Promise<FeatureToggle> {
this.logger.info(`${createdBy} creates feature toggle ${value.name}`);
@ -1155,6 +1183,7 @@ class FeatureToggleService {
createdBy,
project: projectId,
data: createdToggle,
createdByUserId,
}),
);
@ -1232,6 +1261,7 @@ class FeatureToggleService {
projectId: string,
newFeatureName: string,
userName: string,
userId: number,
replaceGroupId: boolean = true,
): Promise<FeatureToggle> {
const changeRequestEnabled =
@ -1262,6 +1292,7 @@ class FeatureToggleService {
projectId,
newToggle,
userName,
userId,
);
const variantTasks = newToggle.environments.map((e) => {
@ -1294,6 +1325,7 @@ class FeatureToggleService {
this.dependentFeaturesService.cloneDependencies(
{ featureName, newFeatureName, projectId },
userName,
userId,
);
await Promise.all([
@ -1310,6 +1342,7 @@ class FeatureToggleService {
updatedFeature: FeatureToggleDTO,
userName: string,
featureName: string,
userId: number,
): Promise<FeatureToggle> {
await this.validateFeatureBelongsToProject({ featureName, projectId });
@ -1328,6 +1361,7 @@ class FeatureToggleService {
await this.eventService.storeEvent(
new FeatureMetadataUpdateEvent({
createdBy: userName,
createdByUserId: userId,
data: featureToggle,
preData,
featureName,
@ -1452,6 +1486,7 @@ class FeatureToggleService {
featureName: string,
isStale: boolean,
createdBy: string,
createdByUserId: number,
): Promise<any> {
const feature = await this.featureToggleStore.get(featureName);
const { project } = feature;
@ -1464,6 +1499,7 @@ class FeatureToggleService {
project,
featureName,
createdBy,
createdByUserId,
}),
);
@ -1485,6 +1521,7 @@ class FeatureToggleService {
await this.unprotectedArchiveToggle(
featureName,
extractUsernameFromUser(user),
user.id,
projectId,
);
}
@ -1492,6 +1529,7 @@ class FeatureToggleService {
async unprotectedArchiveToggle(
featureName: string,
createdBy: string,
createdByUserId: number,
projectId?: string,
): Promise<void> {
const feature = await this.featureToggleStore.get(featureName);
@ -1512,6 +1550,7 @@ class FeatureToggleService {
[featureName],
projectId,
createdBy,
createdByUserId,
);
}
@ -1519,6 +1558,7 @@ class FeatureToggleService {
new FeatureArchivedEvent({
featureName,
createdBy,
createdByUserId,
project: feature.project,
}),
);
@ -1534,6 +1574,7 @@ class FeatureToggleService {
featureNames,
extractUsernameFromUser(user),
projectId,
user.id,
);
}
@ -1559,6 +1600,7 @@ class FeatureToggleService {
featureNames: string[],
createdBy: string,
projectId: string,
createdByUserId: number,
): Promise<void> {
await Promise.all([
this.validateFeaturesContext(featureNames, projectId),
@ -1572,6 +1614,7 @@ class FeatureToggleService {
featureNames,
projectId,
createdBy,
createdByUserId,
);
await this.eventService.storeEvents(
@ -1580,6 +1623,7 @@ class FeatureToggleService {
new FeatureArchivedEvent({
featureName: feature.name,
createdBy,
createdByUserId,
project: feature.project,
}),
),
@ -1591,6 +1635,7 @@ class FeatureToggleService {
stale: boolean,
createdBy: string,
projectId: string,
createdByUserId: number,
): Promise<void> {
await this.validateFeaturesContext(featureNames, projectId);
@ -1612,6 +1657,7 @@ class FeatureToggleService {
project: projectId,
featureName: feature.name,
createdBy,
createdByUserId,
}),
),
);
@ -1666,6 +1712,7 @@ class FeatureToggleService {
environment,
enabled,
createdBy,
user,
shouldActivateDisabledStrategies,
);
}
@ -1676,6 +1723,7 @@ class FeatureToggleService {
environment: string,
enabled: boolean,
createdBy: string,
user?: IUser,
shouldActivateDisabledStrategies = false,
): Promise<FeatureToggle> {
const hasEnvironment =
@ -1712,6 +1760,7 @@ class FeatureToggleService {
featureName,
},
createdBy,
user,
),
),
);
@ -1746,6 +1795,7 @@ class FeatureToggleService {
featureName,
},
createdBy,
user?.id || SYSTEM_USER_ID,
);
}
}
@ -1765,6 +1815,7 @@ class FeatureToggleService {
featureName,
environment,
createdBy,
createdByUserId: user?.id || SYSTEM_USER_ID,
}),
);
}
@ -1775,6 +1826,7 @@ class FeatureToggleService {
async storeFeatureUpdatedEventLegacy(
featureName: string,
createdBy: string,
createdByUserId: number,
): Promise<FeatureToggleLegacy> {
const feature = await this.getFeatureToggleLegacy(featureName);
@ -1783,6 +1835,7 @@ class FeatureToggleService {
await this.eventService.storeEvent({
type: FEATURE_UPDATED,
createdBy,
createdByUserId,
featureName,
data: feature,
project: feature.project,
@ -1831,6 +1884,7 @@ class FeatureToggleService {
featureName: string,
newProject: string,
createdBy: string,
createdByUserId: number,
): Promise<void> {
const changeRequestEnabled =
await this.changeRequestAccessReadModel.isChangeRequestsEnabledForProject(
@ -1858,6 +1912,7 @@ class FeatureToggleService {
await this.eventService.storeEvent(
new FeatureChangeProjectEvent({
createdBy,
createdByUserId,
oldProject,
newProject,
featureName,
@ -1870,7 +1925,11 @@ class FeatureToggleService {
}
// TODO: add project id.
async deleteFeature(featureName: string, createdBy: string): Promise<void> {
async deleteFeature(
featureName: string,
createdBy: string,
createdByUserId: number,
): Promise<void> {
await this.validateNoChildren(featureName);
const toggle = await this.featureToggleStore.get(featureName);
const tags = await this.tagStore.getAllTagsForFeature(featureName);
@ -1881,6 +1940,7 @@ class FeatureToggleService {
featureName,
project: toggle.project,
createdBy,
createdByUserId,
preData: toggle,
tags,
}),
@ -1891,6 +1951,7 @@ class FeatureToggleService {
featureNames: string[],
projectId: string,
createdBy: string,
createdByUserId: number,
): Promise<void> {
await this.validateFeaturesContext(featureNames, projectId);
await this.validateNoOrphanParents(featureNames);
@ -1912,6 +1973,7 @@ class FeatureToggleService {
new FeatureDeletedEvent({
featureName: feature.name,
createdBy,
createdByUserId,
project: feature.project,
preData: feature,
tags: tags
@ -1929,6 +1991,7 @@ class FeatureToggleService {
featureNames: string[],
projectId: string,
createdBy: string,
createdByUserId: number,
): Promise<void> {
await this.validateFeaturesContext(featureNames, projectId);
@ -1952,6 +2015,7 @@ class FeatureToggleService {
new FeatureRevivedEvent({
featureName: feature.name,
createdBy,
createdByUserId,
project: feature.project,
}),
),
@ -1959,7 +2023,11 @@ class FeatureToggleService {
}
// TODO: add project id.
async reviveFeature(featureName: string, createdBy: string): Promise<void> {
async reviveFeature(
featureName: string,
createdBy: string,
createdByUserId: number,
): Promise<void> {
const toggle = await this.featureToggleStore.revive(featureName);
await this.featureToggleStore.disableAllEnvironmentsForFeatures([
featureName,
@ -1967,6 +2035,7 @@ class FeatureToggleService {
await this.eventService.storeEvent(
new FeatureRevivedEvent({
createdBy,
createdByUserId,
featureName,
project: toggle.project,
}),
@ -2072,6 +2141,7 @@ class FeatureToggleService {
project: string,
newVariants: IVariant[],
createdBy: string,
createdByUserId: number,
): Promise<FeatureToggle> {
await variantsArraySchema.validateAsync(newVariants);
const fixedVariants = this.fixVariantWeights(newVariants);
@ -2088,6 +2158,7 @@ class FeatureToggleService {
project,
featureName,
createdBy,
createdByUserId,
oldVariants,
newVariants: featureToggle.variants as IVariant[],
}),
@ -2119,6 +2190,7 @@ class FeatureToggleService {
new EnvironmentVariantEvent({
featureName,
environment,
createdByUserId: user.id,
project: projectId,
createdBy: user,
oldVariants: theOldVariants,
@ -2199,6 +2271,7 @@ class FeatureToggleService {
createdBy: user,
oldVariants: oldVariants[environment],
newVariants: fixedVariants,
createdByUserId: user.id,
}),
),
);
@ -2317,6 +2390,7 @@ class FeatureToggleService {
({ name, project }) =>
new PotentiallyStaleOnEvent({
featureName: name,
createdByUserId: SYSTEM_USER_ID,
project,
}),
),

View File

@ -260,6 +260,7 @@ class FeatureController extends Controller {
featureName,
req.body,
userName,
req.user.id,
);
res.status(201).header('location', `${featureName}/tags`).json(tag);
}
@ -279,13 +280,23 @@ class FeatureController extends Controller {
await Promise.all(
addedTags.map((addedTag) =>
this.tagService.addTag(featureName, addedTag, userName),
this.tagService.addTag(
featureName,
addedTag,
userName,
req.user.id,
),
),
);
await Promise.all(
removedTags.map((removedTag) =>
this.tagService.removeTag(featureName, removedTag, userName),
this.tagService.removeTag(
featureName,
removedTag,
userName,
req.user.id,
),
),
);
@ -300,7 +311,12 @@ class FeatureController extends Controller {
): Promise<void> {
const { featureName, type, value } = req.params;
const userName = extractUsername(req);
await this.tagService.removeTag(featureName, { type, value }, userName);
await this.tagService.removeTag(
featureName,
{ type, value },
userName,
req.user.id,
);
res.status(200).end();
}
@ -328,6 +344,7 @@ class FeatureController extends Controller {
project,
validatedToggle,
userName,
req.user.id,
true,
);
const strategies = await Promise.all(
@ -351,7 +368,13 @@ class FeatureController extends Controller {
enabled,
userName,
);
await this.service.saveVariants(name, project, variants, userName);
await this.service.saveVariants(
name,
project,
variants,
userName,
req.user.id,
);
res.status(201).json({
...createdFeature,
@ -376,6 +399,7 @@ class FeatureController extends Controller {
value,
userName,
featureName,
req.user.id,
);
await this.service.removeAllStrategiesForEnv(featureName);
@ -385,7 +409,11 @@ class FeatureController extends Controller {
updatedFeature.strategies.map(async (s) =>
this.service.createStrategy(
s,
{ projectId, featureName, environment: DEFAULT_ENV },
{
projectId: projectId!!,
featureName,
environment: DEFAULT_ENV,
},
userName,
req.user,
),
@ -393,22 +421,25 @@ class FeatureController extends Controller {
);
}
await this.service.updateEnabled(
projectId,
projectId!!,
featureName,
DEFAULT_ENV,
updatedFeature.enabled,
userName,
req.user,
);
await this.service.saveVariants(
featureName,
projectId,
projectId!!,
value.variants || [],
userName,
req.user.id,
);
const feature = await this.service.storeFeatureUpdatedEventLegacy(
featureName,
userName,
req.user.id,
);
res.status(200).json(feature);
@ -432,6 +463,7 @@ class FeatureController extends Controller {
await this.service.storeFeatureUpdatedEventLegacy(
featureName,
userName,
req.user.id,
);
res.status(200).json(feature);
}
@ -450,6 +482,7 @@ class FeatureController extends Controller {
await this.service.storeFeatureUpdatedEventLegacy(
featureName,
userName,
req.user.id,
);
res.json(feature);
}
@ -468,6 +501,7 @@ class FeatureController extends Controller {
await this.service.storeFeatureUpdatedEventLegacy(
featureName,
userName,
req.user.id,
);
res.json(feature);
}
@ -475,7 +509,12 @@ class FeatureController extends Controller {
async staleOn(req: IAuthRequest, res: Response): Promise<void> {
const { featureName } = req.params;
const userName = extractUsername(req);
await this.service.updateStale(featureName, true, userName);
await this.service.updateStale(
featureName,
true,
userName,
req.user.id,
);
const feature = await this.service.getFeatureToggleLegacy(featureName);
res.json(feature);
}
@ -483,7 +522,12 @@ class FeatureController extends Controller {
async staleOff(req: IAuthRequest, res: Response): Promise<void> {
const { featureName } = req.params;
const userName = extractUsername(req);
await this.service.updateStale(featureName, false, userName);
await this.service.updateStale(
featureName,
false,
userName,
req.user.id,
);
const feature = await this.service.getFeatureToggleLegacy(featureName);
res.json(feature);
}

View File

@ -9,6 +9,8 @@ import {
IUnleashStores,
IVariant,
SKIP_CHANGE_REQUEST,
SYSTEM_USER,
SYSTEM_USER_ID,
} from '../../../types';
import EnvironmentService from '../../project-environments/environment-service';
import { ForbiddenError, PatternError, PermissionError } from '../../../error';
@ -27,7 +29,7 @@ let segmentService: ISegmentService;
let eventService: EventService;
let environmentService: EnvironmentService;
let unleashConfig;
const TEST_USER_ID = -9999;
const mockConstraints = (): IConstraint[] => {
return Array.from({ length: 5 }).map(() => ({
values: ['x', 'y', 'z'],
@ -62,7 +64,6 @@ afterAll(async () => {
beforeEach(async () => {
await db.rawDatabase('change_request_settings').del();
});
test('Should create feature toggle strategy configuration', async () => {
const projectId = 'default';
const username = 'feature-toggle';
@ -78,6 +79,7 @@ test('Should create feature toggle strategy configuration', async () => {
name: 'Demo',
},
'test',
TEST_USER_ID,
);
const createdConfig = await service.createStrategy(
@ -106,6 +108,7 @@ test('Should be able to update existing strategy configuration', async () => {
name: featureName,
},
'test',
TEST_USER_ID,
);
const createdConfig = await service.createStrategy(
@ -142,6 +145,7 @@ test('Should be able to get strategy by id', async () => {
name: featureName,
},
userName,
TEST_USER_ID,
);
const createdConfig = await service.createStrategy(
@ -167,6 +171,7 @@ test('should ignore name in the body when updating feature toggle', async () =>
description: 'First toggle',
},
userName,
TEST_USER_ID,
);
await service.createFeatureToggle(
@ -176,6 +181,7 @@ test('should ignore name in the body when updating feature toggle', async () =>
description: 'Second toggle',
},
userName,
TEST_USER_ID,
);
const update = {
@ -183,7 +189,13 @@ test('should ignore name in the body when updating feature toggle', async () =>
description: "I'm changed",
};
await service.updateFeatureToggle(projectId, update, userName, featureName);
await service.updateFeatureToggle(
projectId,
update,
userName,
featureName,
TEST_USER_ID,
);
const featureOne = await service.getFeature({ featureName });
const featureTwo = await service.getFeature({
featureName: secondFeatureName,
@ -205,6 +217,7 @@ test('should not get empty rows as features', async () => {
description: 'First toggle',
},
userName,
TEST_USER_ID,
);
await service.createFeatureToggle(
@ -214,6 +227,7 @@ test('should not get empty rows as features', async () => {
description: 'Second toggle',
},
userName,
TEST_USER_ID,
);
const user = { email: 'test@example.com' } as User;
@ -254,6 +268,7 @@ test('adding and removing an environment preserves variants when variants per en
],
},
'random_user',
TEST_USER_ID,
);
//force the variantEnvironments flag off so that we can test legacy behavior
@ -269,9 +284,24 @@ test('adding and removing an environment preserves variants when variants per en
eventService,
);
await environmentService.addEnvironmentToProject(prodEnv, 'default');
await environmentService.removeEnvironmentFromProject(prodEnv, 'default');
await environmentService.addEnvironmentToProject(prodEnv, 'default');
await environmentService.addEnvironmentToProject(
prodEnv,
'default',
SYSTEM_USER.username,
SYSTEM_USER.id,
);
await environmentService.removeEnvironmentFromProject(
prodEnv,
'default',
SYSTEM_USER.username,
SYSTEM_USER.id,
);
await environmentService.addEnvironmentToProject(
prodEnv,
'default',
SYSTEM_USER.username,
SYSTEM_USER.id,
);
const toggle = await service.getFeature({
featureName,
@ -292,6 +322,7 @@ test('cloning a feature toggle copies variant environments correctly', async ()
name: newToggleName,
},
'test',
TEST_USER_ID,
);
await stores.environmentStore.create({
@ -322,6 +353,7 @@ test('cloning a feature toggle copies variant environments correctly', async ()
'default',
clonedToggleName,
'test-user',
SYSTEM_USER_ID,
true,
);
@ -335,8 +367,8 @@ test('cloning a feature toggle copies variant environments correctly', async ()
);
const newEnv = clonedToggle.environments.find((x) => x.name === targetEnv);
expect(defaultEnv.variants).toHaveLength(0);
expect(newEnv.variants).toHaveLength(1);
expect(defaultEnv!!.variants).toHaveLength(0);
expect(newEnv!!.variants).toHaveLength(1);
});
test('cloning a feature toggle not allowed for change requests enabled', async () => {
@ -350,6 +382,7 @@ test('cloning a feature toggle not allowed for change requests enabled', async (
'default',
'clonedToggleName',
'test-user',
SYSTEM_USER_ID,
true,
),
).rejects.toEqual(
@ -365,7 +398,7 @@ test('changing to a project with change requests enabled should not be allowed',
environment: 'default',
});
await expect(
service.changeProject('newToggleName', 'default', 'user'),
service.changeProject('newToggleName', 'default', 'user', TEST_USER_ID),
).rejects.toEqual(
new ForbiddenError(
`Changing project not allowed. Project default has change requests enabled.`,
@ -393,6 +426,7 @@ test('Cloning a feature toggle also clones segments correctly', async () => {
name: featureName,
},
'test-user',
TEST_USER_ID,
);
const config: Omit<FeatureStrategySchema, 'id'> = {
@ -413,6 +447,7 @@ test('Cloning a feature toggle also clones segments correctly', async () => {
'default',
clonedFeatureName,
'test-user',
SYSTEM_USER_ID,
true,
);
@ -431,6 +466,7 @@ test('If change requests are enabled, cannot change variants without going via C
'default',
{ name: featureName },
'test-user',
TEST_USER_ID,
);
// Force all feature flags on to make sure we have Change requests on
@ -510,6 +546,7 @@ test('If CRs are protected for any environment in the project stops bulk update
project.id,
{ name: 'crOnVariantToggle' },
user.username,
user.id,
);
const variant: IVariant = {
@ -580,6 +617,7 @@ test('getPlaygroundFeatures should return ids and titles (if they exist) on clie
name: featureName,
},
userName,
TEST_USER_ID,
);
await service.createStrategy(
@ -673,6 +711,7 @@ test('Should return last seen at per environment', async () => {
name: featureName,
},
userName,
TEST_USER_ID,
);
const date = await insertFeatureEnvironmentsLastSeen(

View File

@ -88,6 +88,7 @@ test('Should not be possible auto-enable feature toggle without CREATE_FEATURE_S
'default',
{ name },
'me',
-9999,
true,
);

View File

@ -84,6 +84,7 @@ export default class MaintenanceController extends Controller {
await this.maintenanceService.toggleMaintenanceMode(
req.body,
extractUsername(req),
req.user.id,
);
res.status(204).end();
}

View File

@ -42,6 +42,7 @@ test('Scheduler should not run scheduled functions if maintenance mode is on', a
await maintenanceService.toggleMaintenanceMode(
{ enabled: true },
'irrelevant user',
-9999,
);
const job = jest.fn();

View File

@ -39,11 +39,13 @@ export default class MaintenanceService implements IMaintenanceStatus {
async toggleMaintenanceMode(
setting: MaintenanceSchema,
user: string,
toggledByUserId: number,
): Promise<void> {
return this.settingService.insert(
maintenanceSettingsKey,
setting,
user,
toggledByUserId,
false,
);
}

View File

@ -2,7 +2,7 @@ import EnvironmentService from './environment-service';
import { createTestConfig } from '../../../test/config/test-config';
import dbInit from '../../../test/e2e/helpers/database-init';
import NotFoundError from '../../error/notfound-error';
import { IUnleashStores } from '../../types';
import { IUnleashStores, SYSTEM_USER } from '../../types';
import NameExistsError from '../../error/name-exists-error';
import { EventService } from '../../services';
@ -53,7 +53,12 @@ test('Can connect environment to project', async () => {
description: '',
stale: false,
});
await service.addEnvironmentToProject('test-connection', 'default', 'user');
await service.addEnvironmentToProject(
'test-connection',
'default',
SYSTEM_USER.username,
SYSTEM_USER.id,
);
const overview = await stores.featureStrategiesStore.getFeatureOverview({
projectId: 'default',
});
@ -76,7 +81,8 @@ test('Can connect environment to project', async () => {
type: 'project-environment-added',
project: 'default',
environment: 'test-connection',
createdBy: 'user',
createdBy: SYSTEM_USER.username,
createdByUserId: SYSTEM_USER.id,
});
});
@ -88,8 +94,18 @@ test('Can remove environment from project', async () => {
await stores.featureToggleStore.create('default', {
name: 'removal-test',
});
await service.removeEnvironmentFromProject('test-connection', 'default');
await service.addEnvironmentToProject('removal-test', 'default');
await service.removeEnvironmentFromProject(
'test-connection',
'default',
SYSTEM_USER.username,
SYSTEM_USER.id,
);
await service.addEnvironmentToProject(
'removal-test',
'default',
SYSTEM_USER.username,
SYSTEM_USER.id,
);
let overview = await stores.featureStrategiesStore.getFeatureOverview({
projectId: 'default',
});
@ -111,7 +127,8 @@ test('Can remove environment from project', async () => {
await service.removeEnvironmentFromProject(
'removal-test',
'default',
'user',
SYSTEM_USER.username,
SYSTEM_USER.id,
);
overview = await stores.featureStrategiesStore.getFeatureOverview({
projectId: 'default',
@ -125,7 +142,8 @@ test('Can remove environment from project', async () => {
type: 'project-environment-removed',
project: 'default',
environment: 'removal-test',
createdBy: 'user',
createdBy: SYSTEM_USER.username,
createdByUserId: SYSTEM_USER.id,
});
});
@ -134,13 +152,33 @@ test('Adding same environment twice should throw a NameExistsError', async () =>
name: 'uniqueness-test',
type: 'production',
});
await service.addEnvironmentToProject('uniqueness-test', 'default');
await service.addEnvironmentToProject(
'uniqueness-test',
'default',
SYSTEM_USER.username,
SYSTEM_USER.id,
);
await service.removeEnvironmentFromProject('test-connection', 'default');
await service.removeEnvironmentFromProject('removal-test', 'default');
await service.removeEnvironmentFromProject(
'test-connection',
'default',
SYSTEM_USER.username,
SYSTEM_USER.id,
);
await service.removeEnvironmentFromProject(
'removal-test',
'default',
SYSTEM_USER.username,
SYSTEM_USER.id,
);
return expect(async () =>
service.addEnvironmentToProject('uniqueness-test', 'default'),
service.addEnvironmentToProject(
'uniqueness-test',
'default',
SYSTEM_USER.username,
SYSTEM_USER.id,
),
).rejects.toThrow(
new NameExistsError(
'default already has the environment uniqueness-test enabled',
@ -153,6 +191,8 @@ test('Removing environment not connected to project should be a noop', async ()
service.removeEnvironmentFromProject(
'some-non-existing-environment',
'default',
SYSTEM_USER.username,
SYSTEM_USER.id,
),
).resolves);
@ -247,7 +287,12 @@ test('When given overrides should remap projects to override environments', asyn
stale: false,
});
await service.addEnvironmentToProject(disabledEnvName, 'default');
await service.addEnvironmentToProject(
disabledEnvName,
'default',
SYSTEM_USER.username,
SYSTEM_USER.id,
);
await service.overrideEnabledProjects([enabledEnvName]);

View File

@ -10,6 +10,7 @@ import {
IUnleashStores,
PROJECT_ENVIRONMENT_ADDED,
PROJECT_ENVIRONMENT_REMOVED,
SYSTEM_USER,
} from '../../types';
import { Logger } from '../../logger';
import { BadDataError, UNIQUE_CONSTRAINT_VIOLATION } from '../../error';
@ -100,7 +101,8 @@ export default class EnvironmentService {
async addEnvironmentToProject(
environment: string,
projectId: string,
username = 'unknown',
username: string,
userId: number,
): Promise<void> {
try {
await this.featureEnvironmentStore.connectProject(
@ -116,6 +118,7 @@ export default class EnvironmentService {
project: projectId,
environment,
createdBy: username,
createdByUserId: userId,
});
} catch (e) {
if (e.code === UNIQUE_CONSTRAINT_VIOLATION) {
@ -132,6 +135,7 @@ export default class EnvironmentService {
projectId: string,
strategy: CreateFeatureStrategySchema,
username: string,
userId: number,
): Promise<CreateFeatureStrategySchema> {
if (strategy.name !== 'flexibleRollout') {
throw new BadDataError(
@ -152,6 +156,7 @@ export default class EnvironmentService {
createdBy: username,
preData: previousDefaultStrategy,
data: defaultStrategy,
createdByUserId: userId,
});
return defaultStrategy;
@ -217,7 +222,12 @@ export default class EnvironmentService {
const linkTasks = uniqueProjects.flatMap((project) => {
return toEnable.map((enabledEnv) => {
return this.addEnvironmentToProject(enabledEnv.name, project);
return this.addEnvironmentToProject(
enabledEnv.name,
project,
SYSTEM_USER.username,
SYSTEM_USER.id,
);
});
});
@ -241,7 +251,8 @@ export default class EnvironmentService {
async removeEnvironmentFromProject(
environment: string,
projectId: string,
username = 'unknown',
username: string,
userId: number,
): Promise<void> {
const projectEnvs =
await this.projectStore.getEnvironmentsForProject(projectId);
@ -256,6 +267,7 @@ export default class EnvironmentService {
project: projectId,
environment,
createdBy: username,
createdByUserId: userId,
});
return;
}

View File

@ -4,6 +4,7 @@ import {
IUnleashConfig,
IUnleashServices,
serializeDates,
SYSTEM_USER_ID,
UPDATE_PROJECT,
} from '../../types';
import { Logger } from '../../logger';
@ -145,6 +146,7 @@ export default class EnvironmentsController extends Controller {
environment,
projectId,
extractUsername(req),
req.user.id,
),
);
@ -162,6 +164,7 @@ export default class EnvironmentsController extends Controller {
environment,
projectId,
extractUsername(req),
req.user.id,
),
);
@ -184,6 +187,7 @@ export default class EnvironmentsController extends Controller {
projectId,
strategy,
extractUsername(req),
req.user.id || SYSTEM_USER_ID,
),
);

View File

@ -35,6 +35,7 @@ const toggleMaintenanceMode = async (
await maintenanceService.toggleMaintenanceMode(
{ enabled },
'irrelevant user',
-9999,
);
};

View File

@ -13,6 +13,7 @@ import { Logger } from '../../logger';
import { ITagType, ITagTypeStore } from './tag-type-store-type';
import { IUnleashConfig } from '../../types/option';
import EventService from '../../services/event-service';
import { SYSTEM_USER } from '../../types';
export default class TagTypeService {
private tagTypeStore: ITagTypeStore;
@ -42,6 +43,7 @@ export default class TagTypeService {
async createTagType(
newTagType: ITagType,
userName: string,
userId: number,
): Promise<ITagType> {
const data = (await tagTypeSchema.validateAsync(
newTagType,
@ -50,7 +52,8 @@ export default class TagTypeService {
await this.tagTypeStore.createTagType(data);
await this.eventService.storeEvent({
type: TAG_TYPE_CREATED,
createdBy: userName || 'unleash-system',
createdBy: userName || SYSTEM_USER.username,
createdByUserId: userId,
data,
});
return data;
@ -73,12 +76,17 @@ export default class TagTypeService {
}
}
async deleteTagType(name: string, userName: string): Promise<void> {
async deleteTagType(
name: string,
userName: string,
userId: number,
): Promise<void> {
const tagType = await this.tagTypeStore.get(name);
await this.tagTypeStore.delete(name);
await this.eventService.storeEvent({
type: TAG_TYPE_DELETED,
createdBy: userName || 'unleash-system',
createdBy: userName || SYSTEM_USER.username,
createdByUserId: userId,
preData: tagType,
});
}
@ -86,12 +94,14 @@ export default class TagTypeService {
async updateTagType(
updatedTagType: ITagType,
userName: string,
userId: number,
): Promise<ITagType> {
const data = await tagTypeSchema.validateAsync(updatedTagType);
await this.tagTypeStore.updateTagType(data);
await this.eventService.storeEvent({
type: TAG_TYPE_UPDATED,
createdBy: userName || 'unleash-system',
createdBy: userName || SYSTEM_USER.username,
createdByUserId: userId,
data,
});
return data;

View File

@ -203,7 +203,7 @@ class TagTypeController extends Controller {
): Promise<void> {
const userName = extractUsername(req);
const tagType = await this.tagTypeService.transactional((service) =>
service.createTagType(req.body, userName),
service.createTagType(req.body, userName, req.user.id),
);
res.status(201)
.header('location', `tag-types/${tagType.name}`)
@ -219,7 +219,11 @@ class TagTypeController extends Controller {
const userName = extractUsername(req);
await this.tagTypeService.transactional((service) =>
service.updateTagType({ name, description, icon }, userName),
service.updateTagType(
{ name, description, icon },
userName,
req.user.id,
),
);
res.status(200).end();
}
@ -235,7 +239,7 @@ class TagTypeController extends Controller {
const { name } = req.params;
const userName = extractUsername(req);
await this.tagTypeService.transactional((service) =>
service.deleteTagType(name, userName),
service.deleteTagType(name, userName, req.user.id),
);
res.status(200).end();
}

View File

@ -9,6 +9,7 @@ import { ISettingStore } from '../../lib/types';
import { frontendSettingsKey } from '../../lib/types/settings/frontend-settings';
import FakeFeatureTagStore from '../../test/fixtures/fake-feature-tag-store';
const TEST_USER_ID = -9999;
const createSettingService = (
frontendApiOrigins: string[],
): { proxyService: ProxyService; settingStore: ISettingStore } => {
@ -52,6 +53,7 @@ test('corsOriginMiddleware origin validation', async () => {
proxyService.setFrontendSettings(
{ frontendApiOrigins: ['a'] },
userName,
TEST_USER_ID,
),
).rejects.toThrow('Invalid origin: a');
});
@ -65,6 +67,7 @@ test('corsOriginMiddleware without config', async () => {
await proxyService.setFrontendSettings(
{ frontendApiOrigins: [] },
userName,
TEST_USER_ID,
);
expect(await proxyService.getFrontendSettings(false)).toEqual({
frontendApiOrigins: [],
@ -72,6 +75,7 @@ test('corsOriginMiddleware without config', async () => {
await proxyService.setFrontendSettings(
{ frontendApiOrigins: ['*'] },
userName,
TEST_USER_ID,
);
expect(await proxyService.getFrontendSettings(false)).toEqual({
frontendApiOrigins: ['*'],
@ -91,6 +95,7 @@ test('corsOriginMiddleware with config', async () => {
await proxyService.setFrontendSettings(
{ frontendApiOrigins: [] },
userName,
TEST_USER_ID,
);
expect(await proxyService.getFrontendSettings(false)).toEqual({
frontendApiOrigins: [],
@ -98,6 +103,7 @@ test('corsOriginMiddleware with config', async () => {
await proxyService.setFrontendSettings(
{ frontendApiOrigins: ['https://example.com', 'https://example.org'] },
userName,
TEST_USER_ID,
);
expect(await proxyService.getFrontendSettings(false)).toEqual({
frontendApiOrigins: ['https://example.com', 'https://example.org'],
@ -120,6 +126,7 @@ test('corsOriginMiddleware with caching enabled', async () => {
await proxyService.setFrontendSettings(
{ frontendApiOrigins: ['*'] },
userName,
TEST_USER_ID,
);
//still get cached value

View File

@ -52,6 +52,12 @@ export const eventSchema = {
description: 'Which user created this event',
example: 'johndoe',
},
createdByUserId: {
type: 'number',
description: 'The is of the user that created this event',
example: 1337,
nullable: true,
},
environment: {
type: 'string',
description:

View File

@ -181,7 +181,12 @@ Note: passing \`null\` as a value for the description property will set it to an
const createdBy = extractUsername(req);
const data = req.body;
const addon = await this.addonService.updateAddon(id, data, createdBy);
const addon = await this.addonService.updateAddon(
id,
data,
createdBy,
req.user.id,
);
this.openApiService.respondWithValidation(
200,
@ -197,7 +202,11 @@ Note: passing \`null\` as a value for the description property will set it to an
): Promise<void> {
const createdBy = extractUsername(req);
const data = req.body;
const addon = await this.addonService.createAddon(data, createdBy);
const addon = await this.addonService.createAddon(
data,
createdBy,
req.user.id,
);
this.openApiService.respondWithValidation(
201,
@ -213,7 +222,7 @@ Note: passing \`null\` as a value for the description property will set it to an
): Promise<void> {
const { id } = req.params;
const username = extractUsername(req);
await this.addonService.removeAddon(id, username);
await this.addonService.removeAddon(id, username, req.user.id);
res.status(200).end();
}

View File

@ -363,6 +363,7 @@ export class ApiTokenController extends Controller {
token,
new Date(expiresAt),
extractUsername(req),
req.user.id,
);
return res.status(200).end();
@ -393,7 +394,11 @@ export class ApiTokenController extends Controller {
`You do not have the required access [${permissionRequired}] to perform this operation`,
);
}
await this.apiTokenService.delete(token, extractUsername(req));
await this.apiTokenService.delete(
token,
extractUsername(req),
req.user.id,
);
await this.proxyService.deleteClientForProxyToken(token);
res.status(200).end();
}

View File

@ -160,6 +160,7 @@ class ConfigController extends Controller {
await this.proxyService.setFrontendSettings(
req.body.frontendSettings,
extractUsername(req),
req.user.id,
);
res.sendStatus(204);
return;

View File

@ -248,6 +248,7 @@ export class ContextController extends Controller {
const result = await this.contextService.createContextField(
value,
userName,
req.user.id,
);
this.openApiService.respondWithValidation(
@ -270,6 +271,7 @@ export class ContextController extends Controller {
await this.contextService.updateContextField(
{ ...contextField, name },
userName,
req.user.id,
);
res.status(200).end();
}
@ -281,7 +283,11 @@ export class ContextController extends Controller {
const name = req.params.contextField;
const userName = extractUsername(req);
await this.contextService.deleteContextField(name, userName);
await this.contextService.deleteContextField(
name,
userName,
req.user.id,
);
res.status(200).end();
}

View File

@ -11,7 +11,7 @@ import {
ProjectUserAddedEvent,
ProjectUserRemovedEvent,
} from '../../types/events';
const TEST_USER_ID = -9999;
async function getSetup(anonymise: boolean = false) {
const base = `/random${Math.round(Math.random() * 1000)}`;
const stores = createStores();
@ -49,6 +49,7 @@ test('should get events list via admin', async () => {
data: { name: 'test', project: 'default' },
featureName: 'test',
project: 'default',
createdByUserId: TEST_USER_ID,
}),
);
const { body } = await request
@ -68,6 +69,7 @@ test('should anonymise events list via admin', async () => {
data: { name: 'test', project: 'default' },
featureName: 'test',
project: 'default',
createdByUserId: TEST_USER_ID,
}),
);
const { body } = await request
@ -87,6 +89,7 @@ test('should also anonymise email fields in data and preData properties', async
eventService.storeEvent(
new ProjectUserAddedEvent({
createdBy: 'some@email.com',
createdByUserId: TEST_USER_ID,
data: { name: 'test', project: 'default', email: email1 },
project: 'default',
}),
@ -94,6 +97,7 @@ test('should also anonymise email fields in data and preData properties', async
eventService.storeEvent(
new ProjectUserRemovedEvent({
createdBy: 'some@email.com',
createdByUserId: TEST_USER_ID,
preData: { name: 'test', project: 'default', email: email2 },
project: 'default',
}),
@ -115,6 +119,7 @@ test('should anonymise any PII fields, no matter the depth', async () => {
eventService.storeEvent(
new ProjectAccessAddedEvent({
createdBy: 'some@email.com',
createdByUserId: TEST_USER_ID,
data: {
groups: [
{

View File

@ -220,7 +220,11 @@ export class ProjectApiTokenController extends Controller {
(storedToken.projects.length === 1 &&
storedToken.project[0] === projectId))
) {
await this.apiTokenService.delete(token, extractUsername(req));
await this.apiTokenService.delete(
token,
extractUsername(req),
user.id,
);
await this.proxyService.deleteClientForProxyToken(token);
res.status(200).end();
} else if (!storedToken) {

View File

@ -166,7 +166,12 @@ export default class ProjectArchiveController extends Controller {
const { projectId } = req.params;
const { features } = req.body;
const user = extractUsername(req);
await this.featureService.deleteFeatures(features, projectId, user);
await this.featureService.deleteFeatures(
features,
projectId,
user,
req.user.id,
);
res.status(200).end();
}
@ -182,6 +187,7 @@ export default class ProjectArchiveController extends Controller {
features,
projectId,
user,
req.user.id,
),
);
res.status(200).end();

View File

@ -257,6 +257,7 @@ The backend will also distribute remaining weight up to 1000 after adding the va
projectId,
req.body,
userName,
req.user.id,
);
res.status(200).json({
version: 1,

View File

@ -190,6 +190,7 @@ export class PublicSignupController extends Controller {
await this.publicSignupTokenService.createNewPublicSignupToken(
req.body,
username,
req.user.id,
);
this.openApiService.respondWithValidation(
201,
@ -219,6 +220,7 @@ export class PublicSignupController extends Controller {
...(expiresAt ? { expiresAt: new Date(expiresAt) } : {}),
},
extractUsername(req),
req.user.id,
);
this.openApiService.respondWithValidation(

View File

@ -122,6 +122,7 @@ class StateController extends Controller {
userName,
dropBeforeImport: paramToBool(drop, false),
keepExisting: paramToBool(keep, true),
userId: req.user.id,
});
res.sendStatus(202);
}

View File

@ -233,7 +233,11 @@ class StrategyController extends Controller {
const strategyName = req.params.name;
const userName = extractUsername(req);
await this.strategyService.removeStrategy(strategyName, userName);
await this.strategyService.removeStrategy(
strategyName,
userName,
req.user.id,
);
res.status(200).end();
}
@ -246,6 +250,7 @@ class StrategyController extends Controller {
const strategy = await this.strategyService.createStrategy(
req.body,
userName,
req.user.id,
);
this.openApiService.respondWithValidation(
201,
@ -265,6 +270,7 @@ class StrategyController extends Controller {
await this.strategyService.updateStrategy(
{ ...req.body, name: req.params.name },
userName,
req.user.id,
);
res.status(200).end();
}
@ -276,7 +282,11 @@ class StrategyController extends Controller {
const userName = extractUsername(req);
const { strategyName } = req.params;
await this.strategyService.deprecateStrategy(strategyName, userName);
await this.strategyService.deprecateStrategy(
strategyName,
userName,
req.user.id,
);
res.status(200).end();
}
@ -287,7 +297,11 @@ class StrategyController extends Controller {
const userName = extractUsername(req);
const { strategyName } = req.params;
await this.strategyService.reactivateStrategy(strategyName, userName);
await this.strategyService.reactivateStrategy(
strategyName,
userName,
req.user.id,
);
res.status(200).end();
}
}

View File

@ -200,7 +200,11 @@ class TagController extends Controller {
res: Response<TagWithVersionSchema>,
): Promise<void> {
const userName = extractUsername(req);
const tag = await this.tagService.createTag(req.body, userName);
const tag = await this.tagService.createTag(
req.body,
userName,
req.user.id,
);
res.status(201)
.header('location', `tags/${tag.type}/${tag.value}`)
.json({ version, tag })
@ -213,7 +217,7 @@ class TagController extends Controller {
): Promise<void> {
const { type, value } = req.params;
const userName = extractUsername(req);
await this.tagService.deleteTag({ type, value }, userName);
await this.tagService.deleteTag({ type, value }, userName, req.user.id);
res.status(200).end();
}
}

View File

@ -19,6 +19,7 @@ import {
IUnleashServices,
RoleName,
CustomAuthHandler,
SYSTEM_USER,
} from './types';
import User, { IUser } from './types/user';
@ -93,6 +94,7 @@ async function createApp(
dropBeforeImport: config.import.dropBeforeImport,
userName: 'import',
keepExisting: config.import.keepExisting,
userId: SYSTEM_USER.id,
});
}

View File

@ -16,7 +16,7 @@ import AccessStoreMock from '../../test/fixtures/fake-access-store';
import { GroupService } from '../services/group-service';
import FakeEventStore from '../../test/fixtures/fake-event-store';
import { IRole } from '../../lib/types/stores/access-store';
import { IGroup, ROLE_CREATED } from '../../lib/types';
import { IGroup, ROLE_CREATED, SYSTEM_USER } from '../../lib/types';
import EventService from './event-service';
import FakeFeatureTagStore from '../../test/fixtures/fake-feature-tag-store';
import BadDataError from '../../lib/error/bad-data-error';
@ -40,6 +40,7 @@ test('should fail when name exists', async () => {
name: 'existing role',
description: 'description',
permissions: [],
createdByUserId: -9999,
});
expect(accessService.validateRole(existingRole)).rejects.toThrow(
@ -172,6 +173,7 @@ test('user with custom root role should get a user root role', async () => {
name: 'custom-root-role',
description: 'test custom root role',
type: CUSTOM_ROOT_ROLE_TYPE,
createdByUserId: -9999,
permissions: [
{
id: 1,
@ -198,6 +200,7 @@ test('user with custom root role should get a user root role', async () => {
expect(events[0]).toEqual({
type: ROLE_CREATED,
createdBy: 'unknown',
createdByUserId: -9999,
data: {
id: 0,
name: 'custom-root-role',
@ -259,7 +262,7 @@ test('throws error when trying to delete a project role in use by group', async
);
try {
await accessService.deleteRole(1);
await accessService.deleteRole(1, SYSTEM_USER.username, SYSTEM_USER.id);
} catch (e) {
expect(e.toString()).toBe(
'RoleInUseError: Role is in use by users(0) or groups(1). You cannot delete a role that is in use without first removing the role from the users and groups.',

View File

@ -46,6 +46,7 @@ import {
ROLE_CREATED,
ROLE_DELETED,
ROLE_UPDATED,
SYSTEM_USER,
} from '../types';
import EventService from './event-service';
@ -70,6 +71,7 @@ export interface IRoleCreation {
type?: 'root-custom' | 'custom';
permissions?: PermissionRef[];
createdBy?: string;
createdByUserId: number;
}
export interface IRoleValidation {
@ -85,6 +87,7 @@ export interface IRoleUpdate {
type?: 'root-custom' | 'custom';
permissions?: PermissionRef[];
createdBy?: string;
createdByUserId: number;
}
export interface AccessWithRoles {
@ -674,6 +677,7 @@ export class AccessService {
this.eventService.storeEvent({
type: ROLE_CREATED,
createdBy: role.createdBy || 'unknown',
createdByUserId: role.createdByUserId,
data: {
...newRole,
permissions: this.sanitizePermissions(addedPermissions),
@ -729,7 +733,8 @@ export class AccessService {
);
this.eventService.storeEvent({
type: ROLE_UPDATED,
createdBy: role.createdBy || 'unknown',
createdBy: role.createdBy || SYSTEM_USER.username,
createdByUserId: role.createdByUserId,
data: {
...updatedRole,
permissions: this.sanitizePermissions(updatedPermissions),
@ -754,7 +759,11 @@ export class AccessService {
});
}
async deleteRole(id: number, deletedBy = 'unknown'): Promise<void> {
async deleteRole(
id: number,
deletedBy: string,
deletedByUserId: number,
): Promise<void> {
await this.validateRoleIsNotBuiltIn(id);
const roleUsers = await this.getUsersForRole(id);
@ -772,6 +781,7 @@ export class AccessService {
this.eventService.storeEvent({
type: ROLE_DELETED,
createdBy: deletedBy,
createdByUserId: deletedByUserId,
preData: {
...existingRole,
permissions: this.sanitizePermissions(existingPermissions),

View File

@ -15,9 +15,12 @@ import { IAddonDto } from '../types/stores/addon-store';
import SimpleAddon from './addon-service-test-simple-addon';
import { IAddonProviders } from '../addons';
import EventService from './event-service';
import { SYSTEM_USER } from '../types';
const MASKED_VALUE = '*****';
const TEST_USER_ID = -9999;
let addonProvider: IAddonProviders;
function getSetup() {
@ -64,7 +67,7 @@ test('should load provider definitions', async () => {
const simple = providerDefinitions.find((p) => p.name === 'simple');
expect(providerDefinitions.length).toBe(1);
expect(simple.name).toBe('simple');
expect(simple!.name).toBe('simple');
});
test('should not allow addon-config for unknown provider', async () => {
@ -80,6 +83,7 @@ test('should not allow addon-config for unknown provider', async () => {
description: '',
},
'test',
TEST_USER_ID,
);
}).rejects.toThrow(ValidationError);
});
@ -98,12 +102,13 @@ test('should trigger simple-addon eventHandler', async () => {
description: '',
};
await addonService.createAddon(config, 'me@mail.com');
await addonService.createAddon(config, 'me@mail.com', TEST_USER_ID);
// Feature toggle was created
await eventService.storeEvent({
type: FEATURE_CREATED,
createdBy: 'some@user.com',
createdBy: SYSTEM_USER.username,
createdByUserId: SYSTEM_USER.id,
data: {
name: 'some-toggle',
enabled: false,
@ -133,10 +138,11 @@ test('should not trigger event handler if project of event is different from add
},
};
await addonService.createAddon(config, 'me@mail.com');
await addonService.createAddon(config, 'me@mail.com', TEST_USER_ID);
await eventService.storeEvent({
type: FEATURE_CREATED,
createdBy: 'some@user.com',
createdBy: SYSTEM_USER.username,
createdByUserId: SYSTEM_USER.id,
project: 'someotherproject',
data: {
name: 'some-toggle',
@ -166,10 +172,11 @@ test('should trigger event handler if project for event is one of the desired pr
},
};
await addonService.createAddon(config, 'me@mail.com');
await addonService.createAddon(config, 'me@mail.com', TEST_USER_ID);
await eventService.storeEvent({
type: FEATURE_CREATED,
createdBy: 'some@user.com',
createdBy: SYSTEM_USER.username,
createdByUserId: SYSTEM_USER.id,
project: desiredProject,
data: {
name: 'some-toggle',
@ -179,7 +186,8 @@ test('should trigger event handler if project for event is one of the desired pr
});
await eventService.storeEvent({
type: FEATURE_CREATED,
createdBy: 'some@user.com',
createdBy: SYSTEM_USER.username,
createdByUserId: SYSTEM_USER.id,
project: otherProject,
data: {
name: 'other-toggle',
@ -211,10 +219,11 @@ test('should trigger events for multiple projects if addon is setup to filter mu
},
};
await addonService.createAddon(config, 'me@mail.com');
await addonService.createAddon(config, 'me@mail.com', TEST_USER_ID);
await eventService.storeEvent({
type: FEATURE_CREATED,
createdBy: 'some@user.com',
createdBy: SYSTEM_USER.username,
createdByUserId: SYSTEM_USER.id,
project: desiredProjects[0],
data: {
name: 'some-toggle',
@ -224,7 +233,8 @@ test('should trigger events for multiple projects if addon is setup to filter mu
});
await eventService.storeEvent({
type: FEATURE_CREATED,
createdBy: 'some@user.com',
createdBy: SYSTEM_USER.username,
createdByUserId: SYSTEM_USER.id,
project: otherProject,
data: {
name: 'other-toggle',
@ -234,7 +244,8 @@ test('should trigger events for multiple projects if addon is setup to filter mu
});
await eventService.storeEvent({
type: FEATURE_CREATED,
createdBy: 'some@user.com',
createdBy: SYSTEM_USER.username,
createdByUserId: SYSTEM_USER.id,
project: desiredProjects[1],
data: {
name: 'third-toggle',
@ -269,10 +280,11 @@ test('should filter events on environment if addon is setup to filter for it', a
},
};
await addonService.createAddon(config, 'me@mail.com');
await addonService.createAddon(config, 'me@mail.com', TEST_USER_ID);
await eventService.storeEvent({
type: FEATURE_CREATED,
createdBy: 'some@user.com',
createdBy: SYSTEM_USER.username,
createdByUserId: SYSTEM_USER.id,
project: desiredEnvironment,
environment: desiredEnvironment,
data: {
@ -283,7 +295,8 @@ test('should filter events on environment if addon is setup to filter for it', a
});
await eventService.storeEvent({
type: FEATURE_CREATED,
createdBy: 'some@user.com',
createdBy: SYSTEM_USER.username,
createdByUserId: SYSTEM_USER.id,
environment: otherEnvironment,
data: {
name: 'other-toggle',
@ -317,7 +330,8 @@ test('should not filter out global events (no specific environment) even if addo
const globalEventWithNoEnvironment = {
type: FEATURE_CREATED,
createdBy: 'some@user.com',
createdBy: SYSTEM_USER.username,
createdByUserId: SYSTEM_USER.id,
project: 'some-project',
data: {
name: 'some-toggle',
@ -326,7 +340,7 @@ test('should not filter out global events (no specific environment) even if addo
},
};
await addonService.createAddon(config, 'me@mail.com');
await addonService.createAddon(config, 'me@mail.com', TEST_USER_ID);
await eventService.storeEvent(globalEventWithNoEnvironment);
const simpleProvider = addonService.addonProviders.simple;
// @ts-expect-error
@ -354,7 +368,8 @@ test('should not filter out global events (no specific project) even if addon is
const globalEventWithNoProject = {
type: FEATURE_CREATED,
createdBy: 'some@user.com',
createdBy: SYSTEM_USER.username,
createdByUserId: SYSTEM_USER.id,
data: {
name: 'some-toggle',
enabled: false,
@ -362,7 +377,7 @@ test('should not filter out global events (no specific project) even if addon is
},
};
await addonService.createAddon(config, 'me@mail.com');
await addonService.createAddon(config, 'me@mail.com', TEST_USER_ID);
await eventService.storeEvent(globalEventWithNoProject);
const simpleProvider = addonService.addonProviders.simple;
// @ts-expect-error
@ -388,10 +403,11 @@ test('should support wildcard option for filtering addons', async () => {
},
};
await addonService.createAddon(config, 'me@mail.com');
await addonService.createAddon(config, 'me@mail.com', TEST_USER_ID);
await eventService.storeEvent({
type: FEATURE_CREATED,
createdBy: 'some@user.com',
createdBy: SYSTEM_USER.username,
createdByUserId: SYSTEM_USER.id,
project: desiredProjects[0],
data: {
name: 'some-toggle',
@ -401,7 +417,8 @@ test('should support wildcard option for filtering addons', async () => {
});
await eventService.storeEvent({
type: FEATURE_CREATED,
createdBy: 'some@user.com',
createdBy: SYSTEM_USER.username,
createdByUserId: SYSTEM_USER.id,
project: otherProject,
data: {
name: 'other-toggle',
@ -411,7 +428,8 @@ test('should support wildcard option for filtering addons', async () => {
});
await eventService.storeEvent({
type: FEATURE_CREATED,
createdBy: 'some@user.com',
createdBy: SYSTEM_USER.username,
createdByUserId: SYSTEM_USER.id,
project: desiredProjects[1],
data: {
name: 'third-toggle',
@ -452,10 +470,11 @@ test('Should support filtering by both project and environment', async () => {
'desired-toggle2',
'desired-toggle3',
];
await addonService.createAddon(config, 'me@mail.com');
await addonService.createAddon(config, 'me@mail.com', TEST_USER_ID);
await eventService.storeEvent({
type: FEATURE_CREATED,
createdBy: 'some@user.com',
createdBy: SYSTEM_USER.username,
createdByUserId: SYSTEM_USER.id,
project: desiredProjects[0],
environment: desiredEnvironments[0],
data: {
@ -466,7 +485,8 @@ test('Should support filtering by both project and environment', async () => {
});
await eventService.storeEvent({
type: FEATURE_CREATED,
createdBy: 'some@user.com',
createdBy: SYSTEM_USER.username,
createdByUserId: SYSTEM_USER.id,
project: desiredProjects[0],
environment: 'wrongenvironment',
data: {
@ -477,7 +497,8 @@ test('Should support filtering by both project and environment', async () => {
});
await eventService.storeEvent({
type: FEATURE_CREATED,
createdBy: 'some@user.com',
createdBy: SYSTEM_USER.username,
createdByUserId: SYSTEM_USER.id,
project: desiredProjects[2],
environment: desiredEnvironments[1],
data: {
@ -488,7 +509,8 @@ test('Should support filtering by both project and environment', async () => {
});
await eventService.storeEvent({
type: FEATURE_CREATED,
createdBy: 'some@user.com',
createdBy: SYSTEM_USER.username,
createdByUserId: SYSTEM_USER.id,
project: desiredProjects[2],
environment: desiredEnvironments[2],
data: {
@ -499,7 +521,8 @@ test('Should support filtering by both project and environment', async () => {
});
await eventService.storeEvent({
type: FEATURE_CREATED,
createdBy: 'some@user.com',
createdBy: SYSTEM_USER.username,
createdByUserId: SYSTEM_USER.id,
project: 'wrongproject',
environment: desiredEnvironments[0],
data: {
@ -536,7 +559,7 @@ test('should create simple-addon config', async () => {
description: '',
};
await addonService.createAddon(config, 'me@mail.com');
await addonService.createAddon(config, 'me@mail.com', TEST_USER_ID);
const addons = await addonService.getAddons();
expect(addons.length).toBe(1);
@ -557,7 +580,7 @@ test('should create tag type for simple-addon', async () => {
description: '',
};
await addonService.createAddon(config, 'me@mail.com');
await addonService.createAddon(config, 'me@mail.com', TEST_USER_ID);
const tagType = await tagTypeService.getTagType('me');
expect(tagType.name).toBe('me');
@ -577,7 +600,7 @@ test('should store ADDON_CONFIG_CREATE event', async () => {
description: '',
};
await addonService.createAddon(config, 'me@mail.com');
await addonService.createAddon(config, 'me@mail.com', TEST_USER_ID);
const { events } = await eventService.getEvents();
@ -600,10 +623,19 @@ test('should store ADDON_CONFIG_UPDATE event', async () => {
events: [FEATURE_CREATED],
};
const addonConfig = await addonService.createAddon(config, 'me@mail.com');
const addonConfig = await addonService.createAddon(
config,
'me@mail.com',
TEST_USER_ID,
);
const updated = { ...addonConfig, description: 'test' };
await addonService.updateAddon(addonConfig.id, updated, 'me@mail.com');
await addonService.updateAddon(
addonConfig.id,
updated,
'me@mail.com',
TEST_USER_ID,
);
const { events } = await eventService.getEvents();
@ -626,9 +658,13 @@ test('should store ADDON_CONFIG_REMOVE event', async () => {
events: [FEATURE_CREATED],
};
const addonConfig = await addonService.createAddon(config, 'me@mail.com');
const addonConfig = await addonService.createAddon(
config,
'me@mail.com',
TEST_USER_ID,
);
await addonService.removeAddon(addonConfig.id, 'me@mail.com');
await addonService.removeAddon(addonConfig.id, 'me@mail.com', TEST_USER_ID);
const { events } = await eventService.getEvents();
@ -652,7 +688,11 @@ test('should hide sensitive fields when fetching', async () => {
events: [FEATURE_CREATED],
};
const createdConfig = await addonService.createAddon(config, 'me@mail.com');
const createdConfig = await addonService.createAddon(
config,
'me@mail.com',
TEST_USER_ID,
);
const addons = await addonService.getAddons();
const addonRetrieved = await addonService.getAddon(createdConfig.id);
@ -677,14 +717,23 @@ test('should not overwrite masked values when updating', async () => {
description: '',
};
const addonConfig = await addonService.createAddon(config, 'me@mail.com');
const addonConfig = await addonService.createAddon(
config,
'me@mail.com',
TEST_USER_ID,
);
const updated = {
...addonConfig,
parameters: { url: MASKED_VALUE, var: 'some-new-value' },
description: 'test',
};
await addonService.updateAddon(addonConfig.id, updated, 'me@mail.com');
await addonService.updateAddon(
addonConfig.id,
updated,
'me@mail.com',
TEST_USER_ID,
);
const updatedConfig = await stores.addonStore.get(addonConfig.id);
// @ts-ignore
@ -707,7 +756,7 @@ test('should reject addon config with missing required parameter when creating',
};
await expect(async () =>
addonService.createAddon(config, 'me@mail.com'),
addonService.createAddon(config, 'me@mail.com', TEST_USER_ID),
).rejects.toThrow(ValidationError);
});
@ -725,14 +774,23 @@ test('should reject updating addon config with missing required parameter', asyn
description: '',
};
const config = await addonService.createAddon(addonConfig, 'me@mail.com');
const config = await addonService.createAddon(
addonConfig,
'me@mail.com',
TEST_USER_ID,
);
const updated = {
...config,
parameters: { var: 'some-new-value' },
description: 'test',
};
await expect(async () =>
addonService.updateAddon(config.id, updated, 'me@mail.com'),
addonService.updateAddon(
config.id,
updated,
'me@mail.com',
TEST_USER_ID,
),
).rejects.toThrow(ValidationError);
});
@ -751,6 +809,6 @@ test('Should reject addon config if a required parameter is just the empty strin
};
await expect(async () =>
addonService.createAddon(config, 'me@mail.com'),
addonService.createAddon(config, 'me@mail.com', TEST_USER_ID),
).rejects.toThrow(ValidationError);
});

View File

@ -8,7 +8,7 @@ import { IFeatureToggleStore } from '../features/feature-toggle/types/feature-to
import { Logger } from '../logger';
import TagTypeService from '../features/tag-type/tag-type-service';
import { IAddon, IAddonDto, IAddonStore } from '../types/stores/addon-store';
import { IUnleashStores, IUnleashConfig } from '../types';
import { IUnleashStores, IUnleashConfig, SYSTEM_USER } from '../types';
import { IAddonDefinition } from '../types/model';
import { minutesToMilliseconds } from 'date-fns';
import EventService from './event-service';
@ -179,6 +179,7 @@ export default class AddonService {
await this.tagTypeService.createTagType(
tagType,
providerName,
SYSTEM_USER.id,
);
} catch (err) {
if (!(err instanceof NameExistsError)) {
@ -191,7 +192,11 @@ export default class AddonService {
return Promise.resolve();
}
async createAddon(data: IAddonDto, userName: string): Promise<IAddon> {
async createAddon(
data: IAddonDto,
userName: string,
userId: number,
): Promise<IAddon> {
const addonConfig = await addonSchema.validateAsync(data);
await this.validateKnownProvider(addonConfig);
await this.validateRequiredParameters(addonConfig);
@ -206,6 +211,7 @@ export default class AddonService {
await this.eventService.storeEvent({
type: events.ADDON_CONFIG_CREATED,
createdBy: userName,
createdByUserId: userId,
data: omitKeys(createdAddon, 'parameters'),
});
@ -216,6 +222,7 @@ export default class AddonService {
id: number,
data: IAddonDto,
userName: string,
userId: number,
): Promise<IAddon> {
const existingConfig = await this.addonStore.get(id); // because getting an early 404 here makes more sense
const addonConfig = await addonSchema.validateAsync(data);
@ -239,6 +246,7 @@ export default class AddonService {
await this.eventService.storeEvent({
type: events.ADDON_CONFIG_UPDATED,
createdBy: userName,
createdByUserId: userId,
preData: omitKeys(existingConfig, 'parameters'),
data: omitKeys(result, 'parameters'),
});
@ -246,12 +254,17 @@ export default class AddonService {
return result;
}
async removeAddon(id: number, userName: string): Promise<void> {
async removeAddon(
id: number,
userName: string,
removedByuserId: number,
): Promise<void> {
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}`);

View File

@ -62,7 +62,7 @@ test("Shouldn't return frontend token when secret is undefined", async () => {
secret: '*:*:some-random-string',
type: ApiTokenType.FRONTEND,
tokenName: 'front',
expiresAt: null,
expiresAt: undefined,
};
const config: IUnleashConfig = createTestConfig({});
@ -94,7 +94,6 @@ test("Shouldn't return frontend token when secret is undefined", async () => {
await apiTokenService.createApiTokenWithProjects(token);
await apiTokenService.fetchActiveTokens();
expect(apiTokenService.getUserForToken(undefined)).toEqual(undefined);
expect(apiTokenService.getUserForToken('')).toEqual(undefined);
});
@ -105,7 +104,7 @@ test('Api token operations should all have events attached', async () => {
secret: '*:*:some-random-string',
type: ApiTokenType.FRONTEND,
tokenName: 'front',
expiresAt: null,
expiresAt: undefined,
};
const config: IUnleashConfig = createTestConfig({});
@ -135,8 +134,8 @@ test('Api token operations should all have events attached', async () => {
);
const saved = await apiTokenService.createApiTokenWithProjects(token);
const newExpiry = addDays(new Date(), 30);
await apiTokenService.updateExpiry(saved.secret, newExpiry, 'test');
await apiTokenService.delete(saved.secret, 'test');
await apiTokenService.updateExpiry(saved.secret, newExpiry, 'test', -9999);
await apiTokenService.delete(saved.secret, 'test', -9999);
const { events } = await eventService.getEvents();
const createdApiTokenEvents = events.filter(
(e) => e.type === API_TOKEN_CREATED,

View File

@ -23,6 +23,8 @@ import {
ApiTokenCreatedEvent,
ApiTokenDeletedEvent,
ApiTokenUpdatedEvent,
SYSTEM_USER,
SYSTEM_USER_ID,
} from '../types';
import { omitKeys } from '../util';
import EventService from './event-service';
@ -114,7 +116,13 @@ export class ApiTokenService {
try {
const createAll = tokens
.map(mapLegacyTokenWithSecret)
.map((t) => this.insertNewApiToken(t, 'init-api-tokens'));
.map((t) =>
this.insertNewApiToken(
t,
'init-api-tokens',
SYSTEM_USER_ID,
),
);
await Promise.all(createAll);
} catch (e) {
this.logger.error('Unable to create initial Admin API tokens');
@ -162,12 +170,14 @@ export class ApiTokenService {
secret: string,
expiresAt: Date,
updatedBy: string,
updatedById: number,
): Promise<IApiToken> {
const previous = await this.store.get(secret);
const token = await this.store.setExpiry(secret, expiresAt);
await this.eventService.storeEvent(
new ApiTokenUpdatedEvent({
createdBy: updatedBy,
createdByUserId: updatedById,
previousToken: omitKeys(previous, 'secret'),
apiToken: omitKeys(token, 'secret'),
}),
@ -175,13 +185,18 @@ export class ApiTokenService {
return token;
}
public async delete(secret: string, deletedBy: string): Promise<void> {
public async delete(
secret: string,
deletedBy: string,
deletedByUserId: number,
): Promise<void> {
if (await this.store.exists(secret)) {
const token = await this.store.get(secret);
await this.store.delete(secret);
await this.eventService.storeEvent(
new ApiTokenDeletedEvent({
createdBy: deletedBy,
createdByUserId: deletedByUserId,
apiToken: omitKeys(token, 'secret'),
}),
);
@ -193,15 +208,21 @@ export class ApiTokenService {
*/
public async createApiToken(
newToken: Omit<ILegacyApiTokenCreate, 'secret'>,
createdBy: string = 'unleash-system',
createdBy: string = SYSTEM_USER.username,
createdByUserId: number = SYSTEM_USER.id,
): Promise<IApiToken> {
const token = mapLegacyToken(newToken);
return this.createApiTokenWithProjects(token, createdBy);
return this.createApiTokenWithProjects(
token,
createdBy,
createdByUserId,
);
}
public async createApiTokenWithProjects(
newToken: Omit<IApiTokenCreate, 'secret'>,
createdBy: string = 'unleash-system',
createdBy: string = SYSTEM_USER.username,
createdByUserId: number = SYSTEM_USER.id,
): Promise<IApiToken> {
validateApiToken(newToken);
const environments = await this.environmentStore.getAll();
@ -209,7 +230,11 @@ export class ApiTokenService {
const secret = this.generateSecretKey(newToken);
const createNewToken = { ...newToken, secret };
return this.insertNewApiToken(createNewToken, createdBy);
return this.insertNewApiToken(
createNewToken,
createdBy,
createdByUserId,
);
}
// TODO: Remove this service method after embedded proxy has been released in
@ -221,12 +246,17 @@ export class ApiTokenService {
const secret = this.generateSecretKey(newToken);
const createNewToken = { ...newToken, secret };
return this.insertNewApiToken(createNewToken, 'system-migration');
return this.insertNewApiToken(
createNewToken,
'system-migration',
SYSTEM_USER_ID,
);
}
private async insertNewApiToken(
newApiToken: IApiTokenCreate,
createdBy: string,
createdByUserId: number,
): Promise<IApiToken> {
try {
const token = await this.store.insert(newApiToken);
@ -234,6 +264,7 @@ export class ApiTokenService {
await this.eventService.storeEvent(
new ApiTokenCreatedEvent({
createdBy,
createdByUserId,
apiToken: omitKeys(token, 'secret'),
}),
);

View File

@ -14,18 +14,18 @@ import { IApplicationQuery } from '../../types/query';
import { IClientApp } from '../../types/model';
import { clientRegisterSchema } from './schema';
import { minutesToMilliseconds, secondsToMilliseconds } from 'date-fns';
import { IClientMetricsStoreV2 } from '../../types/stores/client-metrics-store-v2';
import { clientMetricsSchema } from './schema';
import { PartialSome } from '../../types/partial';
import { IPrivateProjectChecker } from '../../features/private-project/privateProjectCheckerType';
import { IFlagResolver } from '../../types';
import { IFlagResolver, SYSTEM_USER } from '../../types';
import { ALL_PROJECTS } from '../../util';
import { Logger } from '../../logger';
export default class ClientInstanceService {
apps = {};
logger = null;
logger: Logger;
seenClients: Record<string, IClientApp> = {};
@ -112,8 +112,9 @@ export default class ClientInstanceService {
if (appsToAnnounce.length > 0) {
const events = appsToAnnounce.map((app) => ({
type: APPLICATION_CREATED,
createdBy: app.createdBy || 'unknown',
createdBy: app.createdBy || SYSTEM_USER.username,
data: app,
createdByUserId: app.createdByUserId || SYSTEM_USER.id,
}));
await this.eventStore.batchStore(events);
}
@ -132,7 +133,7 @@ export default class ClientInstanceService {
this.clientInstanceStore
) {
const uniqueRegistrations = Object.values(this.seenClients);
const uniqueApps = Object.values(
const uniqueApps: Partial<IClientApplication>[] = Object.values(
uniqueRegistrations.reduce((soFar, reg) => {
// eslint-disable-next-line no-param-reassign
soFar[reg.appName] = reg;

View File

@ -44,6 +44,7 @@ test('should clean unknown feature toggle names from last seen store', async ()
'default',
{ name: featureName },
'user',
-9999,
),
),
);
@ -99,6 +100,7 @@ test('should clean unknown feature toggle environments from last seen store', as
'default',
{ name: feature.name },
'user',
-9999,
),
),
);

View File

@ -110,6 +110,7 @@ class ContextService {
async createContextField(
value: IContextFieldDto,
userName: string,
createdByUserId: number,
): Promise<IContextField> {
// validations
await this.validateUniqueName(value);
@ -120,6 +121,7 @@ class ContextService {
await this.eventService.storeEvent({
type: CONTEXT_FIELD_CREATED,
createdBy: userName,
createdByUserId,
data: contextField,
});
@ -129,6 +131,7 @@ class ContextService {
async updateContextField(
updatedContextField: IContextFieldDto,
userName: string,
updatedByUserId: number,
): Promise<void> {
const contextField = await this.contextFieldStore.get(
updatedContextField.name,
@ -140,12 +143,17 @@ class ContextService {
await this.eventService.storeEvent({
type: CONTEXT_FIELD_UPDATED,
createdBy: userName,
createdByUserId: updatedByUserId,
preData: contextField,
data: value,
});
}
async deleteContextField(name: string, userName: string): Promise<void> {
async deleteContextField(
name: string,
userName: string,
deletedByUserId: number,
): Promise<void> {
const contextField = await this.contextFieldStore.get(name);
// delete
@ -153,6 +161,7 @@ class ContextService {
await this.eventService.storeEvent({
type: CONTEXT_FIELD_DELETED,
createdBy: userName,
createdByUserId: deletedByUserId,
preData: contextField,
});
}

View File

@ -65,6 +65,7 @@ export class FavoritesService {
type: FEATURE_FAVORITED,
featureName: feature,
createdBy: extractUsernameFromUser(user),
createdByUserId: user.id,
data: {
feature,
},
@ -84,6 +85,7 @@ export class FavoritesService {
type: FEATURE_UNFAVORITED,
featureName: feature,
createdBy: extractUsernameFromUser(user),
createdByUserId: user.id,
data: {
feature,
},
@ -102,6 +104,7 @@ export class FavoritesService {
await this.eventService.storeEvent({
type: PROJECT_FAVORITED,
createdBy: extractUsernameFromUser(user),
createdByUserId: user.id,
data: {
project,
},
@ -120,6 +123,7 @@ export class FavoritesService {
await this.eventService.storeEvent({
type: PROJECT_UNFAVORITED,
createdBy: extractUsernameFromUser(user),
createdByUserId: user.id,
data: {
project,
},

View File

@ -56,15 +56,17 @@ class FeatureTagService {
featureName: string,
tag: ITag,
userName: string,
addedByUserId: number,
): Promise<ITag> {
const featureToggle = await this.featureToggleStore.get(featureName);
const validatedTag = await tagSchema.validateAsync(tag);
await this.createTagIfNeeded(validatedTag, userName);
await this.createTagIfNeeded(validatedTag, userName, addedByUserId);
await this.featureTagStore.tagFeature(featureName, validatedTag);
await this.eventService.storeEvent({
type: FEATURE_TAGGED,
createdBy: userName,
createdByUserId: addedByUserId,
featureName,
project: featureToggle.project,
data: validatedTag,
@ -77,11 +79,14 @@ class FeatureTagService {
addedTags: ITag[],
removedTags: ITag[],
userName: string,
updatedByUserId: number,
): Promise<void> {
const featureToggles =
await this.featureToggleStore.getAllByNames(featureNames);
await Promise.all(
addedTags.map((tag) => this.createTagIfNeeded(tag, userName)),
addedTags.map((tag) =>
this.createTagIfNeeded(tag, userName, updatedByUserId),
),
);
const createdFeatureTags: IFeatureTag[] = featureNames.flatMap(
(featureName) =>
@ -112,6 +117,7 @@ class FeatureTagService {
featureName: featureToggle.name,
project: featureToggle.project,
data: addedTag,
createdByUserId: updatedByUserId,
})),
);
@ -122,6 +128,7 @@ class FeatureTagService {
featureName: featureToggle.name,
project: featureToggle.project,
preData: removedTag,
createdByUserId: updatedByUserId,
})),
);
@ -131,7 +138,11 @@ class FeatureTagService {
]);
}
async createTagIfNeeded(tag: ITag, userName: string): Promise<void> {
async createTagIfNeeded(
tag: ITag,
userName: string,
createdByUserId: number,
): Promise<void> {
try {
await this.tagStore.getTag(tag.type, tag.value);
} catch (error) {
@ -141,6 +152,7 @@ class FeatureTagService {
await this.eventService.storeEvent({
type: TAG_CREATED,
createdBy: userName,
createdByUserId,
data: tag,
});
} catch (err) {
@ -159,6 +171,7 @@ class FeatureTagService {
featureName: string,
tag: ITag,
userName: string,
removedByUserId: number,
): Promise<void> {
const featureToggle = await this.featureToggleStore.get(featureName);
const tags =
@ -167,6 +180,7 @@ class FeatureTagService {
await this.eventService.storeEvent({
type: FEATURE_UNTAGGED,
createdBy: userName,
createdByUserId: removedByUserId,
featureName,
project: featureToggle.project,
preData: tag,

View File

@ -57,6 +57,7 @@ export default class FeatureTypeService {
await this.eventService.storeEvent({
type: FEATURE_TYPE_UPDATED,
createdBy: extractUsernameFromUser(user),
createdByUserId: user.id,
data: { ...featureType, lifetimeDays: translatedLifetime },
preData: featureType,
});

View File

@ -94,6 +94,7 @@ export class GroupService {
async createGroup(
group: ICreateGroupModel,
userName: string,
createdByUserId: number,
): Promise<IGroup> {
await this.validateGroup(group);
@ -111,13 +112,18 @@ export class GroupService {
await this.eventService.storeEvent({
type: GROUP_CREATED,
createdBy: userName,
createdByUserId,
data: { ...group, users: newUserIds },
});
return newGroup;
}
async updateGroup(group: IGroupModel, userName: string): Promise<IGroup> {
async updateGroup(
group: IGroupModel,
userName: string,
createdByUserId: number,
): Promise<IGroup> {
const existingGroup = await this.groupStore.get(group.id);
await this.validateGroup(group, existingGroup);
@ -149,6 +155,7 @@ export class GroupService {
await this.eventService.storeEvent({
type: GROUP_UPDATED,
createdBy: userName,
createdByUserId,
data: { ...newGroup, users: newUserIds },
preData: { ...existingGroup, users: existingUserIds },
});
@ -183,7 +190,11 @@ export class GroupService {
return [];
}
async deleteGroup(id: number, userName: string): Promise<void> {
async deleteGroup(
id: number,
userName: string,
createdByUserId: number,
): Promise<void> {
const group = await this.groupStore.get(id);
const existingUsers = await this.groupStore.getAllUsersByGroups([
@ -196,6 +207,7 @@ export class GroupService {
await this.eventService.storeEvent({
type: GROUP_DELETED,
createdBy: userName,
createdByUserId,
preData: { ...group, users: existingUserIds },
});
}
@ -219,6 +231,57 @@ export class GroupService {
return this.groupStore.getProjectGroupRoles(projectId);
}
async syncExternalGroups(
userId: number,
externalGroups: string[],
createdBy?: string,
createdByUserId?: number,
): Promise<void> {
if (Array.isArray(externalGroups)) {
const newGroups = await this.groupStore.getNewGroupsForExternalUser(
userId,
externalGroups,
);
await this.groupStore.addUserToGroups(
userId,
newGroups.map((g) => g.id),
createdBy,
);
const oldGroups = await this.groupStore.getOldGroupsForExternalUser(
userId,
externalGroups,
);
await this.groupStore.deleteUsersFromGroup(oldGroups);
const events: IBaseEvent[] = [];
for (const group of newGroups) {
events.push({
type: GROUP_USER_ADDED,
createdBy: createdBy ?? 'unknown',
createdByUserId: createdByUserId ?? -9999,
data: {
groupId: group.id,
userId,
},
});
}
for (const group of oldGroups) {
events.push({
type: GROUP_USER_REMOVED,
createdBy: createdBy ?? 'unknown',
createdByUserId: createdByUserId ?? -9999,
preData: {
groupId: group.groupId,
userId,
},
});
}
await this.eventService.storeEvents(events);
}
}
private mapGroupWithUsers(
group: IGroup,
allGroupUsers: IGroupUser[],
@ -242,54 +305,6 @@ export class GroupService {
return { ...group, users: finalUsers };
}
async syncExternalGroups(
userId: number,
externalGroups: string[],
createdBy?: string,
): Promise<void> {
if (Array.isArray(externalGroups)) {
const newGroups = await this.groupStore.getNewGroupsForExternalUser(
userId,
externalGroups,
);
await this.groupStore.addUserToGroups(
userId,
newGroups.map((g) => g.id),
createdBy,
);
const oldGroups = await this.groupStore.getOldGroupsForExternalUser(
userId,
externalGroups,
);
await this.groupStore.deleteUsersFromGroup(oldGroups);
const events: IBaseEvent[] = [];
for (const group of newGroups) {
events.push({
type: GROUP_USER_ADDED,
createdBy: createdBy ?? 'unknown',
data: {
groupId: group.id,
userId,
},
});
}
for (const group of oldGroups) {
events.push({
type: GROUP_USER_REMOVED,
createdBy: createdBy ?? 'unknown',
preData: {
groupId: group.groupId,
userId,
},
});
}
await this.eventService.storeEvents(events);
}
}
async getGroupsForUser(userId: number): Promise<IGroup[]> {
return this.groupStore.getGroupsForUser(userId);
}

View File

@ -45,6 +45,7 @@ export default class PatService {
await this.eventService.storeEvent({
type: PAT_CREATED,
createdBy: editor.email || editor.username,
createdByUserId: editor.id,
data: pat,
});
@ -66,6 +67,7 @@ export default class PatService {
await this.eventService.storeEvent({
type: PAT_DELETED,
createdBy: editor.email || editor.username,
createdByUserId: editor.id,
data: pat,
});

View File

@ -41,6 +41,7 @@ import {
CreateProject,
IProjectUpdate,
IProjectHealth,
SYSTEM_USER,
} from '../types';
import {
IProjectQuery,
@ -255,6 +256,7 @@ export default class ProjectService {
await this.eventService.storeEvent({
type: PROJECT_CREATED,
createdBy: getCreatedBy(user),
createdByUserId: user.id,
data,
project: newProject.id,
});
@ -273,10 +275,11 @@ export default class ProjectService {
// updated project contains instructions to update the project but it may not represent a whole project
const afterData = await this.projectStore.get(updatedProject.id);
await this.eventStore.store({
await this.eventService.storeEvent({
type: PROJECT_UPDATED,
project: updatedProject.id,
createdBy: getCreatedBy(user),
createdByUserId: user.id,
data: afterData,
preData,
});
@ -300,6 +303,7 @@ export default class ProjectService {
type: PROJECT_UPDATED,
project: updatedProject.id,
createdBy: getCreatedBy(user),
createdByUserId: user.id,
data: { ...preData, ...updatedProject },
preData,
});
@ -363,6 +367,7 @@ export default class ProjectService {
featureName,
newProjectId,
getCreatedBy(user),
user.id,
);
await this.featureToggleService.updateFeatureStrategyProject(
featureName,
@ -399,6 +404,7 @@ export default class ProjectService {
archivedToggles.map((toggle) => toggle.name),
id,
user.name,
user.id,
);
await this.projectStore.delete(id);
@ -407,6 +413,7 @@ export default class ProjectService {
type: PROJECT_DELETED,
createdBy: getCreatedBy(user),
project: id,
createdByUserId: user.id,
});
await this.accessService.removeDefaultProjectRoles(user, id);
@ -460,7 +467,8 @@ export default class ProjectService {
await this.eventService.storeEvent(
new ProjectUserAddedEvent({
project: projectId,
createdBy: createdBy || 'system-user',
createdBy: createdBy || SYSTEM_USER.username,
createdByUserId: user.id || SYSTEM_USER.id,
data: {
roleId,
userId,
@ -479,6 +487,7 @@ export default class ProjectService {
roleId: number,
userId: number,
createdBy: string,
createdByUserId: number,
): Promise<void> {
const role = await this.findProjectRole(projectId, roleId);
@ -492,6 +501,7 @@ export default class ProjectService {
new ProjectUserRemovedEvent({
project: projectId,
createdBy,
createdByUserId,
preData: {
roleId,
userId,
@ -506,6 +516,7 @@ export default class ProjectService {
projectId: string,
userId: number,
createdBy: string,
createdByUserId: number,
): Promise<void> {
const existingRoles = await this.accessService.getProjectRolesForUser(
projectId,
@ -526,6 +537,7 @@ export default class ProjectService {
new ProjectAccessUserRolesDeleted({
project: projectId,
createdBy,
createdByUserId,
preData: {
roles: existingRoles,
userId,
@ -538,6 +550,7 @@ export default class ProjectService {
projectId: string,
groupId: number,
createdBy: string,
createdByUserId: number,
): Promise<void> {
const existingRoles = await this.accessService.getProjectRolesForGroup(
projectId,
@ -558,6 +571,7 @@ export default class ProjectService {
new ProjectAccessUserRolesDeleted({
project: projectId,
createdBy,
createdByUserId,
preData: {
roles: existingRoles,
groupId,
@ -571,6 +585,7 @@ export default class ProjectService {
roleId: number,
groupId: number,
modifiedBy: string,
modifiedById: number,
): Promise<void> {
const role = await this.accessService.getRole(roleId);
const group = await this.groupService.getGroup(groupId);
@ -593,6 +608,7 @@ export default class ProjectService {
new ProjectGroupAddedEvent({
project: project.id,
createdBy: modifiedBy,
createdByUserId: modifiedById,
data: {
groupId: group.id,
projectId: project.id,
@ -610,6 +626,7 @@ export default class ProjectService {
roleId: number,
groupId: number,
modifiedBy: string,
modifiedById: number,
): Promise<void> {
const group = await this.groupService.getGroup(groupId);
const role = await this.accessService.getRole(roleId);
@ -633,6 +650,7 @@ export default class ProjectService {
new ProjectGroupRemovedEvent({
project: projectId,
createdBy: modifiedBy,
createdByUserId: modifiedById,
preData: {
groupId: group.id,
projectId: project.id,
@ -647,6 +665,7 @@ export default class ProjectService {
roleId: number,
usersAndGroups: IProjectAccessModel,
createdBy: string,
createdByUserId: number,
): Promise<void> {
await this.accessService.addRoleAccessToProject(
usersAndGroups.users,
@ -660,6 +679,7 @@ export default class ProjectService {
new ProjectAccessAddedEvent({
project: projectId,
createdBy,
createdByUserId,
data: {
roleId,
groups: usersAndGroups.groups.map(({ id }) => id),
@ -675,6 +695,7 @@ export default class ProjectService {
groups: number[],
users: number[],
createdBy: string,
createdByUserId: number,
): Promise<void> {
await this.accessService.addAccessToProject(
roles,
@ -688,6 +709,7 @@ export default class ProjectService {
new ProjectAccessAddedEvent({
project: projectId,
createdBy,
createdByUserId,
data: {
roles,
groups,
@ -702,6 +724,7 @@ export default class ProjectService {
userId: number,
newRoles: number[],
createdByUserName: string,
createdByUserId: number,
): Promise<void> {
const currentRoles = await this.accessService.getProjectRolesForUser(
projectId,
@ -727,6 +750,7 @@ export default class ProjectService {
new ProjectAccessUserRolesUpdated({
project: projectId,
createdBy: createdByUserName,
createdByUserId,
data: {
roles: newRoles,
userId,
@ -744,6 +768,7 @@ export default class ProjectService {
groupId: number,
newRoles: number[],
createdBy: string,
createdByUserId: number,
): Promise<void> {
const currentRoles = await this.accessService.getProjectRolesForGroup(
projectId,
@ -769,6 +794,7 @@ export default class ProjectService {
new ProjectAccessGroupRolesUpdated({
project: projectId,
createdBy,
createdByUserId,
data: {
roles: newRoles,
groupId,
@ -863,6 +889,7 @@ export default class ProjectService {
roleId: number,
userId: number,
createdBy: string,
createdByUserId: number,
): Promise<void> {
const usersWithRoles = await this.getAccessToProject(projectId);
const user = usersWithRoles.users.find((u) => u.id === userId);
@ -896,6 +923,7 @@ export default class ProjectService {
new ProjectUserUpdateRoleEvent({
project: projectId,
createdBy,
createdByUserId,
preData: {
userId,
roleId: currentRole.id,
@ -917,6 +945,7 @@ export default class ProjectService {
roleId: number,
userId: number,
createdBy: string,
createdByUserId: number,
): Promise<void> {
const usersWithRoles = await this.getAccessToProject(projectId);
const user = usersWithRoles.groups.find((u) => u.id === userId);
@ -949,6 +978,7 @@ export default class ProjectService {
new ProjectGroupUpdateRoleEvent({
project: projectId,
createdBy,
createdByUserId,
preData: {
userId,
roleId: currentRole.id,

View File

@ -162,6 +162,7 @@ export class ProxyService {
async setFrontendSettings(
value: FrontendSettings,
createdBy: string,
createdByUserId: number,
): Promise<void> {
const error = validateOrigins(value.frontendApiOrigins);
if (error) {
@ -171,6 +172,7 @@ export class ProxyService {
frontendSettingsKey,
value,
createdBy,
createdByUserId,
false,
);
}

View File

@ -1,6 +1,6 @@
import crypto from 'crypto';
import { Logger } from '../logger';
import { IUnleashConfig, IUnleashStores } from '../types';
import { IUnleashConfig, IUnleashStores, SYSTEM_USER } from '../types';
import { IPublicSignupTokenStore } from '../types/stores/public-signup-token-store';
import { PublicSignupTokenSchema } from '../openapi/spec/public-signup-token-schema';
import { IRoleStore } from '../types/stores/role-store';
@ -77,11 +77,13 @@ export class PublicSignupTokenService {
secret: string,
{ expiresAt, enabled }: { expiresAt?: Date; enabled?: boolean },
createdBy: string,
createdByUserId: number,
): Promise<PublicSignupTokenSchema> {
const result = await this.store.update(secret, { expiresAt, enabled });
await this.eventService.storeEvent(
new PublicSignupTokenUpdatedEvent({
createdBy,
createdByUserId,
data: { secret, enabled, expiresAt },
}),
);
@ -100,7 +102,8 @@ export class PublicSignupTokenService {
await this.store.addTokenUser(secret, user.id);
await this.eventService.storeEvent(
new PublicSignupTokenUserAddedEvent({
createdBy: 'System',
createdBy: SYSTEM_USER.username,
createdByUserId: SYSTEM_USER.id,
data: { secret, userId: user.id },
}),
);
@ -110,6 +113,7 @@ export class PublicSignupTokenService {
public async createNewPublicSignupToken(
tokenCreate: PublicSignupTokenCreateSchema,
createdBy: string,
createdByUserId: number,
): Promise<PublicSignupTokenSchema> {
const viewerRole = await this.roleStore.getRoleByName(RoleName.VIEWER);
const secret = this.generateSecretKey();
@ -131,6 +135,7 @@ export class PublicSignupTokenService {
await this.eventService.storeEvent(
new PublicSignupTokenCreatedEvent({
createdBy: createdBy,
createdByUserId,
data: token,
}),
);

View File

@ -4,6 +4,7 @@ import {
IFlagResolver,
IUnleashStores,
SKIP_CHANGE_REQUEST,
SYSTEM_USER,
} from '../types';
import { Logger } from '../logger';
import NameExistsError from '../error/name-exists-error';
@ -143,7 +144,7 @@ export class SegmentService implements ISegmentService {
async create(
data: unknown,
user: Partial<Pick<User, 'username' | 'email'>>,
user: Partial<Pick<User, 'id' | 'username' | 'email'>>,
): Promise<ISegment> {
const input = await segmentSchema.validateAsync(data);
this.validateSegmentValuesLimit(input);
@ -152,7 +153,8 @@ export class SegmentService implements ISegmentService {
await this.eventService.storeEvent({
type: SEGMENT_CREATED,
createdBy: user.email || user.username || 'unknown',
createdBy: user.email || user.username || SYSTEM_USER.username,
createdByUserId: user.id || SYSTEM_USER.id,
data: segment,
project: segment.project,
});
@ -186,6 +188,7 @@ export class SegmentService implements ISegmentService {
await this.eventService.storeEvent({
type: SEGMENT_UPDATED,
createdBy: user.email || user.username || 'unknown',
createdByUserId: user.id,
data: segment,
preData,
project: segment.project,
@ -199,6 +202,7 @@ export class SegmentService implements ISegmentService {
await this.eventService.storeEvent({
type: SEGMENT_DELETED,
createdBy: user.email || user.username,
createdByUserId: user.id,
preData: segment,
project: segment.project,
});
@ -210,6 +214,7 @@ export class SegmentService implements ISegmentService {
await this.eventService.storeEvent({
type: SEGMENT_DELETED,
createdBy: user.email || user.username,
createdByUserId: user.id,
preData: segment,
});
}

View File

@ -46,6 +46,7 @@ export default class SettingService {
id: string,
value: object,
createdBy: string,
createdByUserId: number,
hideEventDetails: boolean = true,
): Promise<void> {
const existingSettings = await this.settingStore.get<object>(id);
@ -65,6 +66,7 @@ export default class SettingService {
{
createdBy,
data,
createdByUserId,
},
preData,
),
@ -73,6 +75,7 @@ export default class SettingService {
await this.settingStore.insert(id, value);
await this.eventService.storeEvent(
new SettingCreatedEvent({
createdByUserId,
createdBy,
data,
}),
@ -80,10 +83,15 @@ export default class SettingService {
}
}
async delete(id: string, createdBy: string): Promise<void> {
async delete(
id: string,
createdBy: string,
createdByUserId: number,
): Promise<void> {
await this.settingStore.delete(id);
await this.eventService.storeEvent(
new SettingDeletedEvent({
createdByUserId,
createdBy,
data: {
id,

View File

@ -14,6 +14,7 @@ import {
import { GLOBAL_ENV } from '../types/environment';
import variantsExportV3 from '../../test/examples/variantsexport_v3.json';
import EventService from './event-service';
import { SYSTEM_USER_ID } from '../types';
const oldExportExample = require('./state-service-export-v1.json');
function getSetup() {
@ -93,7 +94,7 @@ test('should import a feature', async () => {
],
};
await stateService.import({ data });
await stateService.import({ userId: SYSTEM_USER_ID, data });
const events = await stores.eventStore.getEvents();
expect(events).toHaveLength(1);
@ -116,7 +117,11 @@ test('should not import an existing feature', async () => {
await stores.featureToggleStore.create('default', data.features[0]);
await stateService.import({ data, keepExisting: true });
await stateService.import({
data,
keepExisting: true,
userId: SYSTEM_USER_ID,
});
const events = await stores.eventStore.getEvents();
expect(events).toHaveLength(0);
@ -141,6 +146,7 @@ test('should not keep existing feature if drop-before-import', async () => {
data,
keepExisting: true,
dropBeforeImport: true,
userId: SYSTEM_USER_ID,
});
const events = await stores.eventStore.getEvents();
@ -162,7 +168,11 @@ test('should drop feature before import if specified', async () => {
],
};
await stateService.import({ data, dropBeforeImport: true });
await stateService.import({
data,
dropBeforeImport: true,
userId: SYSTEM_USER_ID,
});
const events = await stores.eventStore.getEvents();
expect(events).toHaveLength(2);
@ -183,7 +193,7 @@ test('should import a strategy', async () => {
],
};
await stateService.import({ data });
await stateService.import({ userId: SYSTEM_USER_ID, data });
const events = await stores.eventStore.getEvents();
expect(events).toHaveLength(1);
@ -205,7 +215,11 @@ test('should not import an existing strategy', async () => {
await stores.strategyStore.createStrategy(data.strategies[0]);
await stateService.import({ data, keepExisting: true });
await stateService.import({
data,
userId: SYSTEM_USER_ID,
keepExisting: true,
});
const events = await stores.eventStore.getEvents();
expect(events).toHaveLength(0);
@ -223,7 +237,11 @@ test('should drop strategies before import if specified', async () => {
],
};
await stateService.import({ data, dropBeforeImport: true });
await stateService.import({
data,
userId: SYSTEM_USER_ID,
dropBeforeImport: true,
});
const events = await stores.eventStore.getEvents();
expect(events).toHaveLength(2);
@ -237,7 +255,11 @@ test('should drop neither features nor strategies when neither is imported', asy
const data = {};
await stateService.import({ data, dropBeforeImport: true });
await stateService.import({
data,
userId: SYSTEM_USER_ID,
dropBeforeImport: true,
});
const events = await stores.eventStore.getEvents();
expect(events).toHaveLength(0);
@ -253,11 +275,11 @@ test('should not accept gibberish', async () => {
const data2 = '{somerandomtext/';
await expect(async () =>
stateService.import({ data: data1 }),
stateService.import({ userId: SYSTEM_USER_ID, data: data1 }),
).rejects.toThrow();
await expect(async () =>
stateService.import({ data: data2 }),
stateService.import({ userId: SYSTEM_USER_ID, data: data2 }),
).rejects.toThrow();
});
@ -349,7 +371,7 @@ test('should import a tag and tag type', async () => {
tags: [{ type: 'simple', value: 'test' }],
};
await stateService.import({ data });
await stateService.import({ userId: SYSTEM_USER_ID, data });
const events = await stores.eventStore.getEvents();
expect(events).toHaveLength(2);
@ -380,7 +402,11 @@ test('Should not import an existing tag', async () => {
type: data.featureTags[0].tagType,
value: data.featureTags[0].tagValue,
});
await stateService.import({ data, keepExisting: true });
await stateService.import({
data,
userId: SYSTEM_USER_ID,
keepExisting: true,
});
const events = await stores.eventStore.getEvents();
expect(events).toHaveLength(0);
});
@ -413,7 +439,11 @@ test('Should not keep existing tags if drop-before-import', async () => {
},
],
};
await stateService.import({ data, dropBeforeImport: true });
await stateService.import({
data,
userId: SYSTEM_USER_ID,
dropBeforeImport: true,
});
const tagTypes = await stores.tagTypeStore.getAll();
expect(tagTypes).toHaveLength(1);
});
@ -513,7 +543,7 @@ test('should import a project', async () => {
],
};
await stateService.import({ data });
await stateService.import({ userId: SYSTEM_USER_ID, data });
const events = await stores.eventStore.getEvents();
expect(events).toHaveLength(1);
@ -536,11 +566,15 @@ test('Should not import an existing project', async () => {
};
await stores.projectStore.create(data.projects[0]);
await stateService.import({ data, keepExisting: true });
await stateService.import({
data,
userId: SYSTEM_USER_ID,
keepExisting: true,
});
const events = await stores.eventStore.getEvents();
expect(events).toHaveLength(0);
await stateService.import({ data });
await stateService.import({ userId: SYSTEM_USER_ID, data });
});
test('Should drop projects before import if specified', async () => {
@ -561,7 +595,11 @@ test('Should drop projects before import if specified', async () => {
description: 'Not expected to be seen after import',
mode: 'open' as const,
});
await stateService.import({ data, dropBeforeImport: true });
await stateService.import({
data,
userId: SYSTEM_USER_ID,
dropBeforeImport: true,
});
const hasProject = await stores.projectStore.hasProject('fancy');
expect(hasProject).toBe(false);
});
@ -695,6 +733,7 @@ test('featureStrategies can keep existing', async () => {
const exported = await stateService.export({});
await stateService.import({
data: exported,
userId: SYSTEM_USER_ID,
userName: 'testing',
keepExisting: true,
});
@ -746,6 +785,7 @@ test('featureStrategies should not keep existing if dropBeforeImport', async ()
exported.featureStrategies = [];
await stateService.import({
data: exported,
userId: SYSTEM_USER_ID,
userName: 'testing',
keepExisting: true,
dropBeforeImport: true,
@ -757,6 +797,7 @@ test('Import v1 and exporting v2 should work', async () => {
const { stateService } = getSetup();
await stateService.import({
data: oldExportExample,
userId: SYSTEM_USER_ID,
dropBeforeImport: true,
userName: 'testing',
});
@ -793,6 +834,7 @@ test('Importing states with deprecated strategies should keep their deprecated s
};
await stateService.import({
data: deprecatedStrategyExample,
userId: SYSTEM_USER_ID,
userName: 'strategy-importer',
dropBeforeImport: true,
keepExisting: false,
@ -807,6 +849,7 @@ test('Exporting a deprecated strategy and then importing it should keep correct
await stateService.import({
data: variantsExportV3,
keepExisting: false,
userId: SYSTEM_USER_ID,
dropBeforeImport: true,
userName: 'strategy importer',
});

View File

@ -118,6 +118,7 @@ export default class StateService {
file,
dropBeforeImport = false,
userName = 'import-user',
userId,
keepExisting = true,
}: IImportFile): Promise<void> {
return readFile(file)
@ -128,6 +129,7 @@ export default class StateService {
userName,
dropBeforeImport,
keepExisting,
userId,
}),
);
}
@ -168,6 +170,7 @@ export default class StateService {
async import({
data,
userName = 'importUser',
userId,
dropBeforeImport = false,
keepExisting = true,
}: IImportData): Promise<void> {
@ -186,6 +189,7 @@ export default class StateService {
userName,
dropBeforeImport,
keepExisting,
userId,
});
}
@ -196,6 +200,7 @@ export default class StateService {
userName,
dropBeforeImport,
keepExisting,
userId,
});
}
@ -215,6 +220,7 @@ export default class StateService {
dropBeforeImport,
keepExisting,
featureEnvironments,
userId,
});
if (featureEnvironments) {
@ -236,6 +242,7 @@ export default class StateService {
userName,
dropBeforeImport,
keepExisting,
userId,
});
}
@ -258,6 +265,7 @@ export default class StateService {
userName,
dropBeforeImport,
keepExisting,
userId,
});
}
@ -265,6 +273,7 @@ export default class StateService {
await this.importSegments(
data.segments,
userName,
userId,
dropBeforeImport,
);
}
@ -361,6 +370,7 @@ export default class StateService {
async importFeatures({
features,
userName,
userId,
dropBeforeImport,
keepExisting,
featureEnvironments,
@ -376,6 +386,7 @@ export default class StateService {
await this.eventService.storeEvent({
type: DROP_FEATURES,
createdBy: userName,
createdByUserId: userId,
data: { name: 'all-features' },
});
}
@ -393,6 +404,7 @@ export default class StateService {
);
await this.eventService.storeEvent({
type: FEATURE_IMPORT,
createdByUserId: userId,
createdBy: userName,
data: feature,
});
@ -404,6 +416,7 @@ export default class StateService {
async importStrategies({
strategies,
userName,
userId,
dropBeforeImport,
keepExisting,
}): Promise<void> {
@ -418,6 +431,7 @@ export default class StateService {
await this.eventService.storeEvent({
type: DROP_STRATEGIES,
createdBy: userName,
createdByUserId: userId,
data: { name: 'all-strategies' },
});
}
@ -431,6 +445,7 @@ export default class StateService {
this.eventService.storeEvent({
type: STRATEGY_IMPORT,
createdBy: userName,
createdByUserId: userId,
data: strategy,
});
}),
@ -442,6 +457,7 @@ export default class StateService {
async importEnvironments({
environments,
userName,
userId,
dropBeforeImport,
keepExisting,
}): Promise<IEnvironment[]> {
@ -455,19 +471,21 @@ export default class StateService {
await this.eventService.storeEvent({
type: DROP_ENVIRONMENTS,
createdBy: userName,
createdByUserId: userId,
data: { name: 'all-environments' },
});
}
const envsImport = environments.filter((env) =>
keepExisting ? !oldEnvs.some((old) => old.name === env.name) : true,
);
let importedEnvs = [];
let importedEnvs: IEnvironment[] = [];
if (envsImport.length > 0) {
importedEnvs =
await this.environmentStore.importEnvironments(envsImport);
const importedEnvironmentEvents = importedEnvs.map((env) => ({
type: ENVIRONMENT_IMPORT,
createdBy: userName,
createdByUserId: userId,
data: env,
}));
await this.eventService.storeEvents(importedEnvironmentEvents);
@ -480,6 +498,7 @@ export default class StateService {
projects,
importedEnvironments,
userName,
userId,
dropBeforeImport,
keepExisting,
}): Promise<void> {
@ -493,6 +512,7 @@ export default class StateService {
await this.eventService.storeEvent({
type: DROP_PROJECTS,
createdBy: userName,
createdByUserId: userId,
data: { name: 'all-projects' },
});
}
@ -509,6 +529,7 @@ export default class StateService {
const importedProjectEvents = importedProjects.map((project) => ({
type: PROJECT_IMPORT,
createdBy: userName,
createdByUserId: userId,
data: project,
}));
await this.eventService.storeEvents(importedProjectEvents);
@ -521,6 +542,7 @@ export default class StateService {
tags,
featureTags,
userName,
userId,
dropBeforeImport,
keepExisting,
}): Promise<void> {
@ -545,16 +567,19 @@ export default class StateService {
{
type: DROP_FEATURE_TAGS,
createdBy: userName,
createdByUserId: userId,
data: { name: 'all-feature-tags' },
},
{
type: DROP_TAGS,
createdBy: userName,
createdByUserId: userId,
data: { name: 'all-tags' },
},
{
type: DROP_TAG_TYPES,
createdBy: userName,
createdByUserId: userId,
data: { name: 'all-tag-types' },
},
]);
@ -564,13 +589,15 @@ export default class StateService {
keepExisting,
oldTagTypes,
userName,
userId,
);
await this.importTags(tags, keepExisting, oldTags, userName);
await this.importTags(tags, keepExisting, oldTags, userName, userId);
await this.importFeatureTags(
featureTags,
keepExisting,
oldFeatureTags,
userName,
userId,
);
}
@ -587,6 +614,7 @@ export default class StateService {
keepExisting: boolean,
oldFeatureTags: IFeatureTag[],
userName: string,
userId: number,
): Promise<void> {
const featureTagsToInsert = featureTags.filter((tag) =>
keepExisting
@ -601,6 +629,7 @@ export default class StateService {
const importedFeatureTagEvents = importedFeatureTags.map((tag) => ({
type: FEATURE_TAG_IMPORT,
createdBy: userName,
createdByUserId: userId,
data: tag,
}));
await this.eventService.storeEvents(importedFeatureTagEvents);
@ -615,6 +644,7 @@ export default class StateService {
keepExisting: boolean,
oldTags: ITag[],
userName: string,
userId: number,
): Promise<void> {
const tagsToInsert = tags.filter((tag) =>
keepExisting
@ -626,6 +656,7 @@ export default class StateService {
const importedTagEvents = importedTags.map((tag) => ({
type: TAG_IMPORT,
createdBy: userName,
createdByUserId: userId,
data: tag,
}));
await this.eventService.storeEvents(importedTagEvents);
@ -637,6 +668,7 @@ export default class StateService {
keepExisting: boolean,
oldTagTypes: ITagType[],
userName: string,
userId: number,
): Promise<void> {
const tagTypesToInsert = tagTypes.filter((tagType) =>
keepExisting
@ -649,6 +681,7 @@ export default class StateService {
const importedTagTypeEvents = importedTagTypes.map((tagType) => ({
type: TAG_TYPE_IMPORT,
createdBy: userName,
createdByUserId: userId,
data: tagType,
}));
await this.eventService.storeEvents(importedTagTypeEvents);
@ -658,6 +691,7 @@ export default class StateService {
async importSegments(
segments: PartialSome<ISegment, 'id'>[],
userName: string,
userId: number,
dropBeforeImport: boolean,
): Promise<void> {
if (dropBeforeImport) {

View File

@ -47,6 +47,7 @@ class StrategyService {
async removeStrategy(
strategyName: string,
userName: string,
userId: number,
): Promise<void> {
const strategy = await this.strategyStore.get(strategyName);
await this._validateEditable(strategy);
@ -54,6 +55,7 @@ class StrategyService {
await this.eventService.storeEvent({
type: STRATEGY_DELETED,
createdBy: userName,
createdByUserId: userId,
data: {
name: strategyName,
},
@ -63,6 +65,7 @@ class StrategyService {
async deprecateStrategy(
strategyName: string,
userName: string,
userId: number,
): Promise<void> {
if (await this.strategyStore.exists(strategyName)) {
// Check existence
@ -70,6 +73,7 @@ class StrategyService {
await this.eventService.storeEvent({
type: STRATEGY_DEPRECATED,
createdBy: userName,
createdByUserId: userId,
data: {
name: strategyName,
},
@ -84,12 +88,14 @@ class StrategyService {
async reactivateStrategy(
strategyName: string,
userName: string,
userId: number,
): Promise<void> {
await this.strategyStore.get(strategyName); // Check existence
await this.strategyStore.reactivateStrategy({ name: strategyName });
await this.eventService.storeEvent({
type: STRATEGY_REACTIVATED,
createdBy: userName,
createdByUserId: userId,
data: {
name: strategyName,
},
@ -99,6 +105,7 @@ class StrategyService {
async createStrategy(
value: IMinimalStrategy,
userName: string,
userId: number,
): Promise<IStrategy> {
const strategy = await strategySchema.validateAsync(value);
strategy.deprecated = false;
@ -108,6 +115,7 @@ class StrategyService {
type: STRATEGY_CREATED,
createdBy: userName,
data: strategy,
createdByUserId: userId,
});
return this.strategyStore.get(strategy.name);
}
@ -115,6 +123,7 @@ class StrategyService {
async updateStrategy(
input: IMinimalStrategy,
userName: string,
userId: number,
): Promise<void> {
const value = await strategySchema.validateAsync(input);
const strategy = await this.strategyStore.get(input.name);
@ -124,6 +133,7 @@ class StrategyService {
type: STRATEGY_UPDATED,
createdBy: userName,
data: value,
createdByUserId: userId,
});
}
@ -146,7 +156,7 @@ class StrategyService {
// This check belongs in the store.
_validateEditable(strategy: IStrategy): void {
if (strategy.editable === false) {
if (!strategy.editable) {
throw new Error(`Cannot edit strategy ${strategy.name}`);
}
}

View File

@ -50,23 +50,29 @@ export default class TagService {
return data;
}
async createTag(tag: ITag, userName: string): Promise<ITag> {
async createTag(
tag: ITag,
userName: string,
userId: number,
): Promise<ITag> {
const data = await this.validate(tag);
await this.tagStore.createTag(data);
await this.eventService.storeEvent({
type: TAG_CREATED,
createdBy: userName,
createdByUserId: userId,
data,
});
return data;
}
async deleteTag(tag: ITag, userName: string): Promise<void> {
async deleteTag(tag: ITag, userName: string, userId): Promise<void> {
await this.tagStore.delete(tag);
await this.eventService.storeEvent({
type: TAG_DELETED,
createdBy: userName,
createdByUserId: userId,
data: tag,
});
}

View File

@ -239,6 +239,7 @@ class UserService {
await this.eventService.storeEvent(
new UserCreatedEvent({
createdBy: this.getCreatedBy(updatedBy),
createdByUserId: user.id,
userCreated,
}),
);
@ -281,6 +282,7 @@ class UserService {
createdBy: this.getCreatedBy(updatedBy),
preUser: preUser,
postUser: storedUser,
createdByUserId: user.id,
}),
);
@ -298,6 +300,7 @@ class UserService {
new UserDeletedEvent({
createdBy: this.getCreatedBy(updatedBy),
deletedUser: user,
createdByUserId: updatedBy?.id || -1337,
}),
);
}

View File

@ -2,7 +2,7 @@ import { Request } from 'express';
import EventEmitter from 'events';
import * as https from 'https';
import * as http from 'http';
import User from './user';
import User, { IUser } from './user';
import { IUnleashConfig } from './option';
import { IUnleashStores } from './stores';
import { IUnleashServices } from './services';
@ -21,3 +21,14 @@ export interface IUnleash {
stop: () => Promise<void>;
version: string;
}
export const SYSTEM_USER: IUser = {
email: 'systemuser@getunleash.io',
id: -1337,
imageUrl: '',
isAPI: false,
name: 'Used by unleash internally for performing system actions that have no user',
permissions: [],
username: 'unleash_system_user',
};
export const SYSTEM_USER_ID: number = SYSTEM_USER.id;

View File

@ -102,6 +102,8 @@ export const ENVIRONMENT_DELETED = 'environment-deleted' as const;
export const SEGMENT_CREATED = 'segment-created' as const;
export const SEGMENT_UPDATED = 'segment-updated' as const;
export const SEGMENT_DELETED = 'segment-deleted' as const;
export const SEGMENT_IMPORT = 'segment-import' as const;
export const GROUP_CREATED = 'group-created' as const;
export const GROUP_UPDATED = 'group-updated' as const;
export const GROUP_DELETED = 'group-deleted' as const;
@ -306,12 +308,14 @@ export const IEventTypes = [
PROJECT_ENVIRONMENT_ADDED,
PROJECT_ENVIRONMENT_REMOVED,
DEFAULT_STRATEGY_UPDATED,
SEGMENT_IMPORT,
] as const;
export type IEventType = (typeof IEventTypes)[number];
export interface IBaseEvent {
type: IEventType;
createdBy: string;
createdByUserId: number;
project?: string;
environment?: string;
featureName?: string;
@ -335,15 +339,24 @@ class BaseEvent implements IBaseEvent {
readonly createdBy: string;
readonly createdByUserId: number;
/**
* @param type the type of the event we're creating.
* @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization
* @param createdByUserId accepts a number representing the internal id of the user creating this event
*/
constructor(type: IEventType, createdBy: string | IUser) {
constructor(
type: IEventType,
createdBy: string | IUser,
createdByUserId: number,
) {
this.type = type;
this.createdBy =
typeof createdBy === 'string'
? createdBy
: extractUsernameFromUser(createdBy);
this.createdByUserId = createdByUserId;
}
}
@ -360,8 +373,13 @@ export class FeatureStaleEvent extends BaseEvent {
project: string;
featureName: string;
createdBy: string | IUser;
createdByUserId: number;
}) {
super(p.stale ? FEATURE_STALE_ON : FEATURE_STALE_OFF, p.createdBy);
super(
p.stale ? FEATURE_STALE_ON : FEATURE_STALE_OFF,
p.createdBy,
p.createdByUserId,
);
this.project = p.project;
this.featureName = p.featureName;
}
@ -383,12 +401,14 @@ export class FeatureEnvironmentEvent extends BaseEvent {
featureName: string;
environment: string;
createdBy: string | IUser;
createdByUserId: number;
}) {
super(
p.enabled
? FEATURE_ENVIRONMENT_ENABLED
: FEATURE_ENVIRONMENT_DISABLED,
p.createdBy,
p.createdByUserId,
);
this.project = p.project;
this.featureName = p.featureName;
@ -417,8 +437,9 @@ export class StrategiesOrderChangedEvent extends BaseEvent {
createdBy: string | IUser;
data: StrategyIds;
preData: StrategyIds;
createdByUserId: number;
}) {
super(STRATEGY_ORDER_CHANGED, p.createdBy);
super(STRATEGY_ORDER_CHANGED, p.createdBy, p.createdByUserId);
const { project, featureName, environment, data, preData } = p;
this.project = project;
this.featureName = featureName;
@ -446,8 +467,9 @@ export class FeatureVariantEvent extends BaseEvent {
createdBy: string | IUser;
newVariants: IVariant[];
oldVariants: IVariant[];
createdByUserId: number;
}) {
super(FEATURE_VARIANTS_UPDATED, p.createdBy);
super(FEATURE_VARIANTS_UPDATED, p.createdBy, p.createdByUserId);
this.project = p.project;
this.featureName = p.featureName;
this.data = { variants: p.newVariants };
@ -476,8 +498,13 @@ export class EnvironmentVariantEvent extends BaseEvent {
createdBy: string | IUser;
newVariants: IVariant[];
oldVariants: IVariant[];
createdByUserId: number;
}) {
super(FEATURE_ENVIRONMENT_VARIANTS_UPDATED, p.createdBy);
super(
FEATURE_ENVIRONMENT_VARIANTS_UPDATED,
p.createdBy,
p.createdByUserId,
);
this.featureName = p.featureName;
this.environment = p.environment;
this.project = p.project;
@ -504,8 +531,9 @@ export class FeatureChangeProjectEvent extends BaseEvent {
newProject: string;
featureName: string;
createdBy: string | IUser;
createdByUserId: number;
}) {
super(FEATURE_PROJECT_CHANGE, p.createdBy);
super(FEATURE_PROJECT_CHANGE, p.createdBy, p.createdByUserId);
const { newProject, oldProject, featureName } = p;
this.project = newProject;
this.featureName = featureName;
@ -528,8 +556,9 @@ export class FeatureCreatedEvent extends BaseEvent {
featureName: string;
createdBy: string | IUser;
data: FeatureToggle;
createdByUserId: number;
}) {
super(FEATURE_CREATED, p.createdBy);
super(FEATURE_CREATED, p.createdBy, p.createdByUserId);
const { project, featureName, data } = p;
this.project = project;
this.featureName = featureName;
@ -549,8 +578,9 @@ export class FeatureArchivedEvent extends BaseEvent {
project: string;
featureName: string;
createdBy: string | IUser;
createdByUserId: number;
}) {
super(FEATURE_ARCHIVED, p.createdBy);
super(FEATURE_ARCHIVED, p.createdBy, p.createdByUserId);
const { project, featureName } = p;
this.project = project;
this.featureName = featureName;
@ -569,8 +599,9 @@ export class FeatureRevivedEvent extends BaseEvent {
project: string;
featureName: string;
createdBy: string | IUser;
createdByUserId: number;
}) {
super(FEATURE_REVIVED, p.createdBy);
super(FEATURE_REVIVED, p.createdBy, p.createdByUserId);
const { project, featureName } = p;
this.project = project;
this.featureName = featureName;
@ -595,8 +626,9 @@ export class FeatureDeletedEvent extends BaseEvent {
preData: FeatureToggle;
createdBy: string | IUser;
tags: ITag[];
createdByUserId: number;
}) {
super(FEATURE_DELETED, p.createdBy);
super(FEATURE_DELETED, p.createdBy, p.createdByUserId);
const { project, featureName, preData } = p;
this.project = project;
this.featureName = featureName;
@ -623,8 +655,9 @@ export class FeatureMetadataUpdateEvent extends BaseEvent {
project: string;
data: FeatureToggle;
preData: FeatureToggle;
createdByUserId: number;
}) {
super(FEATURE_METADATA_UPDATED, p.createdBy);
super(FEATURE_METADATA_UPDATED, p.createdBy, p.createdByUserId);
const { project, featureName, data, preData } = p;
this.project = project;
this.featureName = featureName;
@ -651,8 +684,9 @@ export class FeatureStrategyAddEvent extends BaseEvent {
environment: string;
createdBy: string | IUser;
data: IStrategyConfig;
createdByUserId: number;
}) {
super(FEATURE_STRATEGY_ADD, p.createdBy);
super(FEATURE_STRATEGY_ADD, p.createdBy, p.createdByUserId);
const { project, featureName, environment, data } = p;
this.project = project;
this.featureName = featureName;
@ -682,8 +716,9 @@ export class FeatureStrategyUpdateEvent extends BaseEvent {
createdBy: string | IUser;
data: IStrategyConfig;
preData: IStrategyConfig;
createdByUserId: number;
}) {
super(FEATURE_STRATEGY_UPDATE, p.createdBy);
super(FEATURE_STRATEGY_UPDATE, p.createdBy, p.createdByUserId);
const { project, featureName, environment, data, preData } = p;
this.project = project;
this.featureName = featureName;
@ -711,8 +746,9 @@ export class FeatureStrategyRemoveEvent extends BaseEvent {
environment: string;
createdBy: string | IUser;
preData: IStrategyConfig;
createdByUserId: number;
}) {
super(FEATURE_STRATEGY_REMOVE, p.createdBy);
super(FEATURE_STRATEGY_REMOVE, p.createdBy, p.createdByUserId);
const { project, featureName, environment, preData } = p;
this.project = project;
this.featureName = featureName;
@ -731,8 +767,13 @@ export class ProjectUserAddedEvent extends BaseEvent {
/**
* @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization
*/
constructor(p: { project: string; createdBy: string | IUser; data: any }) {
super(PROJECT_USER_ADDED, p.createdBy);
constructor(p: {
project: string;
createdBy: string | IUser;
data: any;
createdByUserId: number;
}) {
super(PROJECT_USER_ADDED, p.createdBy, p.createdByUserId);
const { project, data } = p;
this.project = project;
this.data = data;
@ -754,8 +795,9 @@ export class ProjectUserRemovedEvent extends BaseEvent {
project: string;
createdBy: string | IUser;
preData: any;
createdByUserId: number;
}) {
super(PROJECT_USER_REMOVED, p.createdBy);
super(PROJECT_USER_REMOVED, p.createdBy, p.createdByUserId);
const { project, preData } = p;
this.project = project;
this.data = null;
@ -778,8 +820,13 @@ export class ProjectUserUpdateRoleEvent extends BaseEvent {
createdBy: string | IUser;
data: any;
preData: any;
createdByUserId: number;
}) {
super(PROJECT_USER_ROLE_CHANGED, eventData.createdBy);
super(
PROJECT_USER_ROLE_CHANGED,
eventData.createdBy,
eventData.createdByUserId,
);
const { project, data, preData } = eventData;
this.project = project;
this.data = data;
@ -797,8 +844,13 @@ export class ProjectGroupAddedEvent extends BaseEvent {
/**
* @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization
*/
constructor(p: { project: string; createdBy: string | IUser; data: any }) {
super(PROJECT_GROUP_ADDED, p.createdBy);
constructor(p: {
project: string;
createdBy: string | IUser;
data: any;
createdByUserId: number;
}) {
super(PROJECT_GROUP_ADDED, p.createdBy, p.createdByUserId);
const { project, data } = p;
this.project = project;
this.data = data;
@ -820,8 +872,9 @@ export class ProjectGroupRemovedEvent extends BaseEvent {
project: string;
createdBy: string | IUser;
preData: any;
createdByUserId: number;
}) {
super(PROJECT_GROUP_REMOVED, p.createdBy);
super(PROJECT_GROUP_REMOVED, p.createdBy, p.createdByUserId);
const { project, preData } = p;
this.project = project;
this.data = null;
@ -844,8 +897,13 @@ export class ProjectGroupUpdateRoleEvent extends BaseEvent {
createdBy: string | IUser;
data: any;
preData: any;
createdByUserId: number;
}) {
super(PROJECT_GROUP_ROLE_CHANGED, eventData.createdBy);
super(
PROJECT_GROUP_ROLE_CHANGED,
eventData.createdBy,
eventData.createdByUserId,
);
const { project, data, preData } = eventData;
this.project = project;
this.data = data;
@ -863,8 +921,13 @@ export class ProjectAccessAddedEvent extends BaseEvent {
/**
* @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization
*/
constructor(p: { project: string; createdBy: string | IUser; data: any }) {
super(PROJECT_ACCESS_ADDED, p.createdBy);
constructor(p: {
project: string;
createdBy: string | IUser;
data: any;
createdByUserId: number;
}) {
super(PROJECT_ACCESS_ADDED, p.createdBy, p.createdByUserId);
const { project, data } = p;
this.project = project;
this.data = data;
@ -887,8 +950,13 @@ export class ProjectAccessUserRolesUpdated extends BaseEvent {
createdBy: string | IUser;
data: any;
preData: any;
createdByUserId: number;
}) {
super(PROJECT_ACCESS_USER_ROLES_UPDATED, p.createdBy);
super(
PROJECT_ACCESS_USER_ROLES_UPDATED,
p.createdBy,
p.createdByUserId,
);
const { project, data, preData } = p;
this.project = project;
this.data = data;
@ -911,8 +979,13 @@ export class ProjectAccessGroupRolesUpdated extends BaseEvent {
createdBy: string | IUser;
data: any;
preData: any;
createdByUserId: number;
}) {
super(PROJECT_ACCESS_GROUP_ROLES_UPDATED, p.createdBy);
super(
PROJECT_ACCESS_GROUP_ROLES_UPDATED,
p.createdBy,
p.createdByUserId,
);
const { project, data, preData } = p;
this.project = project;
this.data = data;
@ -934,8 +1007,13 @@ export class ProjectAccessUserRolesDeleted extends BaseEvent {
project: string;
createdBy: string | IUser;
preData: any;
createdByUserId: number;
}) {
super(PROJECT_ACCESS_USER_ROLES_DELETED, p.createdBy);
super(
PROJECT_ACCESS_USER_ROLES_DELETED,
p.createdBy,
p.createdByUserId,
);
const { project, preData } = p;
this.project = project;
this.data = null;
@ -957,8 +1035,13 @@ export class ProjectAccessGroupRolesDeleted extends BaseEvent {
project: string;
createdBy: string | IUser;
preData: any;
createdByUserId: number;
}) {
super(PROJECT_ACCESS_GROUP_ROLES_DELETED, p.createdBy);
super(
PROJECT_ACCESS_GROUP_ROLES_DELETED,
p.createdBy,
p.createdByUserId,
);
const { project, preData } = p;
this.project = project;
this.data = null;
@ -972,8 +1055,12 @@ export class SettingCreatedEvent extends BaseEvent {
/**
* @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization
*/
constructor(eventData: { createdBy: string | IUser; data: any }) {
super(SETTING_CREATED, eventData.createdBy);
constructor(eventData: {
createdBy: string | IUser;
data: any;
createdByUserId: number;
}) {
super(SETTING_CREATED, eventData.createdBy, eventData.createdByUserId);
this.data = eventData.data;
}
}
@ -984,8 +1071,12 @@ export class SettingDeletedEvent extends BaseEvent {
/**
* @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization
*/
constructor(eventData: { createdBy: string | IUser; data: any }) {
super(SETTING_DELETED, eventData.createdBy);
constructor(eventData: {
createdBy: string | IUser;
data: any;
createdByUserId: number;
}) {
super(SETTING_DELETED, eventData.createdBy, eventData.createdByUserId);
this.data = eventData.data;
}
}
@ -998,10 +1089,14 @@ export class SettingUpdatedEvent extends BaseEvent {
* @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization
*/
constructor(
eventData: { createdBy: string | IUser; data: any },
eventData: {
createdBy: string | IUser;
data: any;
createdByUserId: number;
},
preData: any,
) {
super(SETTING_UPDATED, eventData.createdBy);
super(SETTING_UPDATED, eventData.createdBy, eventData.createdByUserId);
this.data = eventData.data;
this.preData = preData;
}
@ -1013,8 +1108,16 @@ export class PublicSignupTokenCreatedEvent extends BaseEvent {
/**
* @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization
*/
constructor(eventData: { createdBy: string | IUser; data: any }) {
super(PUBLIC_SIGNUP_TOKEN_CREATED, eventData.createdBy);
constructor(eventData: {
createdBy: string | IUser;
data: any;
createdByUserId: number;
}) {
super(
PUBLIC_SIGNUP_TOKEN_CREATED,
eventData.createdBy,
eventData.createdByUserId,
);
this.data = eventData.data;
}
}
@ -1025,8 +1128,16 @@ export class PublicSignupTokenUpdatedEvent extends BaseEvent {
/**
* @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization
*/
constructor(eventData: { createdBy: string | IUser; data: any }) {
super(PUBLIC_SIGNUP_TOKEN_TOKEN_UPDATED, eventData.createdBy);
constructor(eventData: {
createdBy: string | IUser;
data: any;
createdByUserId: number;
}) {
super(
PUBLIC_SIGNUP_TOKEN_TOKEN_UPDATED,
eventData.createdBy,
eventData.createdByUserId,
);
this.data = eventData.data;
}
}
@ -1037,8 +1148,16 @@ export class PublicSignupTokenUserAddedEvent extends BaseEvent {
/**
* @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization
*/
constructor(eventData: { createdBy: string | IUser; data: any }) {
super(PUBLIC_SIGNUP_TOKEN_USER_ADDED, eventData.createdBy);
constructor(eventData: {
createdBy: string | IUser;
data: any;
createdByUserId: number;
}) {
super(
PUBLIC_SIGNUP_TOKEN_USER_ADDED,
eventData.createdBy,
eventData.createdByUserId,
);
this.data = eventData.data;
}
}
@ -1056,8 +1175,13 @@ export class ApiTokenCreatedEvent extends BaseEvent {
constructor(eventData: {
createdBy: string | IUser;
apiToken: Omit<IApiToken, 'secret'>;
createdByUserId: number;
}) {
super(API_TOKEN_CREATED, eventData.createdBy);
super(
API_TOKEN_CREATED,
eventData.createdBy,
eventData.createdByUserId,
);
this.data = eventData.apiToken;
this.environment = eventData.apiToken.environment;
this.project = eventData.apiToken.project;
@ -1077,8 +1201,13 @@ export class ApiTokenDeletedEvent extends BaseEvent {
constructor(eventData: {
createdBy: string | IUser;
apiToken: Omit<IApiToken, 'secret'>;
createdByUserId: number;
}) {
super(API_TOKEN_DELETED, eventData.createdBy);
super(
API_TOKEN_DELETED,
eventData.createdBy,
eventData.createdByUserId,
);
this.preData = eventData.apiToken;
this.environment = eventData.apiToken.environment;
this.project = eventData.apiToken.project;
@ -1101,8 +1230,13 @@ export class ApiTokenUpdatedEvent extends BaseEvent {
createdBy: string | IUser;
previousToken: Omit<IApiToken, 'secret'>;
apiToken: Omit<IApiToken, 'secret'>;
createdByUserId: number;
}) {
super(API_TOKEN_UPDATED, eventData.createdBy);
super(
API_TOKEN_UPDATED,
eventData.createdBy,
eventData.createdByUserId,
);
this.preData = eventData.previousToken;
this.data = eventData.apiToken;
this.environment = eventData.apiToken.environment;
@ -1115,8 +1249,16 @@ export class PotentiallyStaleOnEvent extends BaseEvent {
readonly project: string;
constructor(eventData: { featureName: string; project: string }) {
super(FEATURE_POTENTIALLY_STALE_ON, 'unleash-system');
constructor(eventData: {
featureName: string;
project: string;
createdByUserId: number;
}) {
super(
FEATURE_POTENTIALLY_STALE_ON,
'unleash-system',
eventData.createdByUserId,
);
this.featureName = eventData.featureName;
this.project = eventData.project;
}
@ -1128,8 +1270,9 @@ export class UserCreatedEvent extends BaseEvent {
constructor(eventData: {
createdBy: string | IUser;
userCreated: IUserWithRootRole;
createdByUserId: number;
}) {
super(USER_CREATED, eventData.createdBy);
super(USER_CREATED, eventData.createdBy, eventData.createdByUserId);
this.data = mapUserToData(eventData.userCreated);
}
}
@ -1142,8 +1285,9 @@ export class UserUpdatedEvent extends BaseEvent {
createdBy: string | IUser;
preUser: IUserWithRootRole;
postUser: IUserWithRootRole;
createdByUserId: number;
}) {
super(USER_UPDATED, eventData.createdBy);
super(USER_UPDATED, eventData.createdBy, eventData.createdByUserId);
this.preData = mapUserToData(eventData.preUser);
this.data = mapUserToData(eventData.postUser);
}
@ -1155,8 +1299,9 @@ export class UserDeletedEvent extends BaseEvent {
constructor(eventData: {
createdBy: string | IUser;
deletedUser: IUserWithRootRole;
createdByUserId: number;
}) {
super(USER_DELETED, eventData.createdBy);
super(USER_DELETED, eventData.createdBy, eventData.createdByUserId);
this.preData = mapUserToData(eventData.deletedUser);
}
}

View File

@ -443,6 +443,7 @@ interface ImportCommon {
dropBeforeImport?: boolean;
keepExisting?: boolean;
userName?: string;
userId: number;
}
export interface IImportData extends ImportCommon {

View File

@ -12,6 +12,7 @@ export interface IClientApplication {
lastSeen: Date;
description: string;
createdBy: string;
createdByUserId?: number;
announced: boolean;
url: string;
color: string;

View File

@ -9,6 +9,7 @@ import {
DELETE_CLIENT_API_TOKEN,
READ_CLIENT_API_TOKEN,
READ_FRONTEND_API_TOKEN,
SYSTEM_USER_ID,
UPDATE_CLIENT_API_TOKEN,
} from '../../../../lib/types';
import { addDays } from 'date-fns';
@ -197,6 +198,7 @@ test('A role with only CREATE_PROJECT_API_TOKEN can create project tokens', asyn
description: 'Can create client tokens',
permissions: [{ name: CREATE_PROJECT_API_TOKEN }],
type: 'root-custom',
createdByUserId: SYSTEM_USER_ID,
});
await accessService.addUserToRole(
user.id,

View File

@ -56,6 +56,7 @@ test('gets ui config with frontendSettings', async () => {
await app.services.proxyService.setFrontendSettings(
{ frontendApiOrigins },
randomId(),
-9999,
);
await app.request
.get('/api/admin/ui-config')

View File

@ -11,7 +11,7 @@ import { EventService } from '../../../../lib/services';
let app: IUnleashTest;
let db: ITestDb;
let eventService: EventService;
const TEST_USER_ID = -9999;
beforeAll(async () => {
db = await dbInit('event_api_serial', getLogger);
app = await setupAppWithCustomConfig(db.stores, {
@ -57,6 +57,7 @@ test('Can filter by project', async () => {
tags: [],
createdBy: 'test-user',
environment: 'test',
createdByUserId: TEST_USER_ID,
});
await eventService.storeEvent({
type: FEATURE_CREATED,
@ -65,6 +66,7 @@ test('Can filter by project', async () => {
tags: [],
createdBy: 'test-user',
environment: 'test',
createdByUserId: TEST_USER_ID,
});
await app.request
.get('/api/admin/events?project=default')
@ -83,6 +85,7 @@ test('can search for events', async () => {
data: { id: randomId() },
tags: [],
createdBy: randomId(),
createdByUserId: TEST_USER_ID,
},
{
type: FEATURE_CREATED,
@ -91,6 +94,7 @@ test('can search for events', async () => {
preData: { id: randomId() },
tags: [{ type: 'simple', value: randomId() }],
createdBy: randomId(),
createdByUserId: TEST_USER_ID,
},
];

View File

@ -1,18 +1,19 @@
import dbInit, { ITestDb } from '../../helpers/database-init';
import {
IUnleashTest,
setupApp,
setupAppWithCustomConfig,
} from '../../helpers/test-helper';
import getLogger from '../../../fixtures/no-logger';
import { DEFAULT_ENV } from '../../../../lib/util/constants';
import { collectIds } from '../../../../lib/util/collect-ids';
import { ApiTokenType } from '../../../../lib/types/models/api-token';
import { IUser, SYSTEM_USER } from '../../../../lib/types';
const importData = require('../../../examples/import.json');
let app: IUnleashTest;
let db: ITestDb;
const userId = -9999;
beforeAll(async () => {
db = await dbInit('state_api_serial', getLogger);
@ -173,6 +174,8 @@ test('Can roundtrip. I.e. export and then import', async () => {
await app.services.environmentService.addEnvironmentToProject(
environment,
projectId,
SYSTEM_USER.username,
SYSTEM_USER.id,
);
await app.services.featureToggleServiceV2.createFeatureToggle(
projectId,
@ -182,6 +185,7 @@ test('Can roundtrip. I.e. export and then import', async () => {
description: 'Feature for export',
},
userName,
userId,
);
await app.services.featureToggleServiceV2.createStrategy(
{
@ -193,6 +197,7 @@ test('Can roundtrip. I.e. export and then import', async () => {
},
{ projectId, featureName, environment },
userName,
{ id: userId } as IUser,
);
const data = await app.services.stateService.export({});
await app.services.stateService.import({
@ -200,6 +205,7 @@ test('Can roundtrip. I.e. export and then import', async () => {
dropBeforeImport: true,
keepExisting: false,
userName: 'export-tester',
userId: -9999,
});
});
@ -221,6 +227,8 @@ test('Roundtrip with tags works', async () => {
await app.services.environmentService.addEnvironmentToProject(
environment,
projectId,
SYSTEM_USER.username,
SYSTEM_USER.id,
);
await app.services.featureToggleServiceV2.createFeatureToggle(
projectId,
@ -230,6 +238,7 @@ test('Roundtrip with tags works', async () => {
description: 'Feature for export',
},
userName,
userId,
);
await app.services.featureToggleServiceV2.createStrategy(
{
@ -250,11 +259,13 @@ test('Roundtrip with tags works', async () => {
featureName,
{ type: 'simple', value: 'export-test' },
userName,
-9999,
);
await app.services.featureTagService.addTag(
featureName,
{ type: 'simple', value: 'export-test-2' },
userName,
-9999,
);
const data = await app.services.stateService.export({});
await app.services.stateService.import({
@ -262,6 +273,7 @@ test('Roundtrip with tags works', async () => {
dropBeforeImport: true,
keepExisting: false,
userName: 'export-tester',
userId: -9999,
});
const f = await app.services.featureTagService.listTags(featureName);
@ -292,15 +304,20 @@ test('Roundtrip with strategies in multiple environments works', async () => {
description: 'Feature for export',
},
userName,
userId,
);
await app.services.environmentService.addEnvironmentToProject(
environment,
projectId,
SYSTEM_USER.username,
SYSTEM_USER.id,
);
await app.services.environmentService.addEnvironmentToProject(
DEFAULT_ENV,
projectId,
SYSTEM_USER.username,
SYSTEM_USER.id,
);
await app.services.featureToggleServiceV2.createStrategy(
{
@ -330,6 +347,7 @@ test('Roundtrip with strategies in multiple environments works', async () => {
dropBeforeImport: true,
keepExisting: false,
userName: 'export-tester',
userId: -9999,
});
const f = await app.services.featureToggleServiceV2.getFeature({
featureName,
@ -387,6 +405,8 @@ test(`should not delete api_tokens on import when drop-flag is set`, async () =>
await app.services.environmentService.addEnvironmentToProject(
environment,
projectId,
SYSTEM_USER.username,
SYSTEM_USER.id,
);
await app.services.featureToggleServiceV2.createFeatureToggle(
projectId,
@ -396,6 +416,7 @@ test(`should not delete api_tokens on import when drop-flag is set`, async () =>
description: 'Feature for export',
},
userName,
userId,
);
await app.services.apiTokenService.createApiTokenWithProjects({
tokenName: apiTokenName,
@ -410,6 +431,7 @@ test(`should not delete api_tokens on import when drop-flag is set`, async () =>
dropBeforeImport: true,
keepExisting: false,
userName: userName,
userId: -9999,
});
const apiTokens = await app.services.apiTokenService.getAllTokens();

View File

@ -6,10 +6,11 @@ import dbInit, { ITestDb } from '../../helpers/database-init';
import getLogger from '../../../fixtures/no-logger';
import { DEFAULT_ENV } from '../../../../lib/util/constants';
import User from '../../../../lib/types/user';
import { SYSTEM_USER } from '../../../../lib/types';
let app: IUnleashTest;
let db: ITestDb;
const testUser = { name: 'test' } as User;
const testUser = { name: 'test', id: -9999 } as User;
beforeAll(async () => {
db = await dbInit('feature_api_client', getLogger);
@ -32,6 +33,7 @@ beforeAll(async () => {
impressionData: true,
},
'test',
testUser.id,
);
await app.services.featureToggleServiceV2.createFeatureToggle(
'default',
@ -40,6 +42,7 @@ beforeAll(async () => {
description: 'soon to be the #1 feature',
},
'test',
testUser.id,
);
await app.services.featureToggleServiceV2.createFeatureToggle(
@ -49,6 +52,7 @@ beforeAll(async () => {
description: 'terrible feature',
},
'test',
testUser.id,
);
await app.services.featureToggleServiceV2.createFeatureToggle(
'default',
@ -57,12 +61,14 @@ beforeAll(async () => {
description: 'the #1 feature',
},
'test',
testUser.id,
);
// depend on enabled feature with variant
await app.services.dependentFeaturesService.unprotectedUpsertFeatureDependency(
{ child: 'featureY', projectId: 'default' },
{ feature: 'featureX', variants: ['featureXVariant'] },
'test',
testUser.id,
);
await app.services.featureToggleServiceV2.archiveToggle(
@ -77,6 +83,7 @@ beforeAll(async () => {
description: 'soon to be the #1 feature',
},
'test',
testUser.id,
);
await app.services.featureToggleServiceV2.archiveToggle(
@ -90,6 +97,7 @@ beforeAll(async () => {
description: 'terrible feature',
},
'test',
testUser.id,
);
await app.services.featureToggleServiceV2.archiveToggle(
'featureArchivedZ',
@ -102,6 +110,7 @@ beforeAll(async () => {
description: 'A feature toggle with variants',
},
'test',
testUser.id,
);
await app.services.featureToggleServiceV2.saveVariants(
'feature.with.variants',
@ -121,6 +130,7 @@ beforeAll(async () => {
},
],
'ivar',
testUser.id,
);
});
@ -243,6 +253,8 @@ test('Can get strategies for specific environment', async () => {
await app.services.environmentService.addEnvironmentToProject(
'testing',
'default',
SYSTEM_USER.username,
SYSTEM_USER.id,
);
await app.request

View File

@ -1,17 +1,18 @@
import {
IUnleashTest,
setupApp,
setupAppWithCustomConfig,
} from '../../helpers/test-helper';
import dbInit, { ITestDb } from '../../helpers/database-init';
import getLogger from '../../../fixtures/no-logger';
import { DEFAULT_ENV } from '../../../../lib/util/constants';
import { IUser } from '../../../../lib/types';
let app: IUnleashTest;
let db: ITestDb;
const featureName = 'feature.default.1';
const username = 'test';
const userId = -9999;
const projectId = 'default';
beforeAll(async () => {
@ -25,12 +26,14 @@ beforeAll(async () => {
description: 'the #1 feature',
},
username,
userId,
);
await app.services.featureToggleServiceV2.createStrategy(
{ name: 'default', constraints: [], parameters: {} },
{ projectId, featureName, environment: DEFAULT_ENV },
username,
{ id: userId } as IUser,
);
});

View File

@ -9,7 +9,7 @@ import User from '../../../../lib/types/user';
let app: IUnleashTest;
let db: ITestDb;
const testUser = { name: 'test' } as User;
const testUser = { name: 'test', id: -9999 } as User;
beforeAll(async () => {
db = await dbInit('feature_304_api_client', getLogger);
@ -29,6 +29,7 @@ beforeAll(async () => {
impressionData: true,
},
'test',
testUser.id,
);
await app.services.featureToggleServiceV2.createFeatureToggle(
'default',
@ -37,6 +38,7 @@ beforeAll(async () => {
description: 'soon to be the #1 feature',
},
'test',
testUser.id,
);
await app.services.featureToggleServiceV2.createFeatureToggle(
'default',
@ -45,6 +47,7 @@ beforeAll(async () => {
description: 'terrible feature',
},
'test',
testUser.id,
);
await app.services.featureToggleServiceV2.createFeatureToggle(
'default',
@ -53,6 +56,7 @@ beforeAll(async () => {
description: 'the #1 feature',
},
'test',
testUser.id,
);
await app.services.featureToggleServiceV2.archiveToggle(
@ -67,6 +71,7 @@ beforeAll(async () => {
description: 'soon to be the #1 feature',
},
'test',
testUser.id,
);
await app.services.featureToggleServiceV2.archiveToggle(
@ -80,6 +85,7 @@ beforeAll(async () => {
description: 'terrible feature',
},
'test',
testUser.id,
);
await app.services.featureToggleServiceV2.archiveToggle(
'featureArchivedZ',
@ -92,6 +98,7 @@ beforeAll(async () => {
description: 'A feature toggle with variants',
},
'test',
testUser.id,
);
await app.services.featureToggleServiceV2.saveVariants(
'feature.with.variants',
@ -111,6 +118,7 @@ beforeAll(async () => {
},
],
'ivar',
testUser.id,
);
});
@ -143,6 +151,7 @@ test('returns 200 when content updates and hash does not match anymore', async (
description: 'the #1 feature',
},
'test',
testUser.id,
);
await app.services.configurationRevisionService.updateMaxRevisionId();

View File

@ -4,6 +4,7 @@ import getLogger from '../../../fixtures/no-logger';
import { ApiTokenService } from '../../../../lib/services/api-token-service';
import { ApiTokenType } from '../../../../lib/types/models/api-token';
import { DEFAULT_ENV } from '../../../../lib/util/constants';
import { SYSTEM_USER } from '../../../../lib/types';
let app: IUnleashTest;
let db: ITestDb;
@ -14,6 +15,7 @@ const environment = 'testing';
const project = 'default';
const project2 = 'some';
const tokenName = 'test';
const tokenUserId = -9999;
const feature1 = 'f1.token.access';
const feature2 = 'f2.token.access';
const feature3 = 'f3.p2.token.access';
@ -38,8 +40,18 @@ beforeAll(async () => {
mode: 'open' as const,
});
await environmentService.addEnvironmentToProject(environment, project);
await environmentService.addEnvironmentToProject(environment, project2);
await environmentService.addEnvironmentToProject(
environment,
project,
SYSTEM_USER.username,
SYSTEM_USER.id,
);
await environmentService.addEnvironmentToProject(
environment,
project2,
SYSTEM_USER.username,
SYSTEM_USER.id,
);
await featureToggleServiceV2.createFeatureToggle(
project,
@ -48,6 +60,7 @@ beforeAll(async () => {
description: 'the #1 feature',
},
tokenName,
tokenUserId,
);
await featureToggleServiceV2.createStrategy(
@ -76,6 +89,7 @@ beforeAll(async () => {
name: feature2,
},
tokenName,
tokenUserId,
);
await featureToggleServiceV2.createStrategy(
{
@ -94,6 +108,7 @@ beforeAll(async () => {
name: feature3,
},
tokenName,
tokenUserId,
);
await featureToggleServiceV2.createStrategy(
{

View File

@ -8,7 +8,7 @@ let app: IUnleashTest;
let db: ITestDb;
let defaultToken;
const TEST_USER_ID = -9999;
beforeAll(async () => {
db = await dbInit('metrics_two_api_client', getLogger);
app = await setupAppWithAuth(db.stores, {}, db.rawDatabase);
@ -104,11 +104,13 @@ test('should set lastSeen for toggles with metrics both for toggle and toggle en
'default',
{ name: 't1' },
'tester',
TEST_USER_ID,
);
await app.services.featureToggleServiceV2.createFeatureToggle(
'default',
{ name: 't2' },
'tester',
TEST_USER_ID,
);
const token = await app.services.apiTokenService.createApiToken({

View File

@ -12,13 +12,14 @@ import {
FEATURE_UPDATED,
IConstraint,
IStrategyConfig,
SYSTEM_USER,
} from '../../../../lib/types';
import { ProxyRepository } from '../../../../lib/proxy';
import { Logger } from '../../../../lib/logger';
let app: IUnleashTest;
let db: ITestDb;
const TEST_USER_ID = -9999;
beforeAll(async () => {
db = await dbInit('proxy', getLogger);
app = await setupAppWithAuth(
@ -78,6 +79,7 @@ const createFeatureToggle = async ({
project,
{ name },
'userName',
TEST_USER_ID,
true,
);
const createdStrategies = await Promise.all(
@ -688,10 +690,14 @@ test('should filter features by environment', async () => {
await app.services.environmentService.addEnvironmentToProject(
environmentA,
'default',
SYSTEM_USER.username,
SYSTEM_USER.id,
);
await app.services.environmentService.addEnvironmentToProject(
environmentB,
'default',
SYSTEM_USER.username,
SYSTEM_USER.id,
);
const frontendTokenEnvironmentDefault = await createApiToken(
ApiTokenType.FRONTEND,

View File

@ -3,6 +3,7 @@ import getLogger from '../../fixtures/no-logger';
import {
AccessService,
IRoleUpdate,
PermissionRef,
} from '../../../lib/services/access-service';
@ -34,6 +35,7 @@ let adminRole;
let readRole;
let userIndex = 0;
const TEST_USER_ID = -9999;
const createUser = async (role?: number) => {
const name = `User ${userIndex}`;
const email = `user-${userIndex}@getunleash.io`;
@ -73,6 +75,7 @@ const createRole = async (rolePermissions: PermissionRef[]) => {
name: `Role ${roleIndex}`,
description: `Role ${roleIndex++} description`,
permissions: rolePermissions,
createdByUserId: TEST_USER_ID,
});
};
@ -737,7 +740,7 @@ test('Should be denied access to delete a role that is in use', async () => {
await projectService.addUser(project.id, customRole.id, projectMember.id);
try {
await accessService.deleteRole(customRole.id);
await accessService.deleteRole(customRole.id, 'testuser', TEST_USER_ID);
} catch (e) {
expect(e.toString()).toBe(
'RoleInUseError: Role is in use by users(1) or groups(0). You cannot delete a role that is in use without first removing the role from the users and groups.',
@ -822,7 +825,8 @@ test('Should not be allowed to edit a root role', async () => {
expect.assertions(1);
const editRole = await accessService.getRoleByName(RoleName.EDITOR);
const roleUpdate = {
const roleUpdate: IRoleUpdate = {
createdByUserId: TEST_USER_ID,
id: editRole.id,
name: 'NoLongerTheEditor',
description: '',
@ -843,7 +847,7 @@ test('Should not be allowed to delete a root role', async () => {
const editRole = await accessService.getRoleByName(RoleName.EDITOR);
try {
await accessService.deleteRole(editRole.id);
await accessService.deleteRole(editRole.id, 'testuser', TEST_USER_ID);
} catch (e) {
expect(e.toString()).toBe(
'InvalidOperationError: You cannot change built in roles.',
@ -855,7 +859,8 @@ test('Should not be allowed to edit a project role', async () => {
expect.assertions(1);
const ownerRole = await accessService.getRoleByName(RoleName.OWNER);
const roleUpdate = {
const roleUpdate: IRoleUpdate = {
createdByUserId: TEST_USER_ID,
id: ownerRole.id,
name: 'NoLongerTheEditor',
description: '',
@ -876,7 +881,7 @@ test('Should not be allowed to delete a project role', async () => {
const ownerRole = await accessService.getRoleByName(RoleName.OWNER);
try {
await accessService.deleteRole(ownerRole.id);
await accessService.deleteRole(ownerRole.id, 'testuser', TEST_USER_ID);
} catch (e) {
expect(e.toString()).toBe(
'InvalidOperationError: You cannot change built in roles.',

View File

@ -14,6 +14,7 @@ const addonProvider = { simple: new SimpleAddon() };
let db;
let stores: IUnleashStores;
let addonService: AddonService;
const TEST_USER_ID = -9999;
beforeAll(async () => {
const config = createTestConfig({
@ -77,9 +78,9 @@ test('should only return active addons', async () => {
description: '',
};
await addonService.createAddon(config, 'me@mail.com');
await addonService.createAddon(config2, 'me@mail.com');
await addonService.createAddon(config3, 'me@mail.com');
await addonService.createAddon(config, 'me@mail.com', TEST_USER_ID);
await addonService.createAddon(config2, 'me@mail.com', TEST_USER_ID);
await addonService.createAddon(config3, 'me@mail.com', TEST_USER_ID);
jest.advanceTimersByTime(61_000);

View File

@ -117,7 +117,7 @@ test('should update expiry of token', async () => {
'tester',
);
await apiTokenService.updateExpiry(token.secret, newTime, 'tester');
await apiTokenService.updateExpiry(token.secret, newTime, 'tester', -9999);
const [updatedToken] = await apiTokenService.getAllTokens();

View File

@ -186,6 +186,7 @@ test('adding a root role to a group with a project role should not fail', async
description: 'root_group',
},
'test',
-9999,
);
await stores.accessStore.addGroupToRole(group.id, 1, 'test', 'default');
@ -200,6 +201,7 @@ test('adding a root role to a group with a project role should not fail', async
createdBy: 'test',
},
'test',
-9999,
);
expect(updatedGroup).toMatchObject({
@ -256,6 +258,7 @@ test('adding a nonexistent role to a group should fail', async () => {
createdBy: 'test',
},
'test',
-9999,
);
}).rejects.toThrow(
'Request validation failed: your request body or params contain invalid data: Incorrect role id 100',

View File

@ -17,7 +17,12 @@ import {
createFeatureToggleService,
createProjectService,
} from '../../../lib/features';
import { IGroup, IUnleashStores } from 'lib/types';
import {
IGroup,
IUnleashStores,
SYSTEM_USER,
SYSTEM_USER_ID,
} from '../../../lib/types';
import { User } from 'lib/server-impl';
let stores: IUnleashStores;
@ -30,6 +35,7 @@ let environmentService: EnvironmentService;
let featureToggleService: FeatureToggleService;
let user: User; // many methods in this test use User instead of IUser
let group: IGroup;
const TEST_USER_ID = -9999;
const isProjectUser = async (
userId: number,
@ -487,6 +493,7 @@ test('should remove user from the project', async () => {
memberRole.id,
projectMember1.id,
'test',
TEST_USER_ID,
);
const { users } = await projectService.getAccessToProject(project.id);
@ -511,6 +518,7 @@ test('should not change project if feature toggle project does not match current
project.id,
toggle,
user.email,
TEST_USER_ID,
);
try {
@ -542,6 +550,7 @@ test('should return 404 if no project is found with the project id', async () =>
project.id,
toggle,
user.email,
TEST_USER_ID,
);
try {
@ -585,6 +594,7 @@ test('should fail if user is not authorized', async () => {
project.id,
toggle,
user.email,
TEST_USER_ID,
);
try {
@ -621,6 +631,7 @@ test('should change project when checks pass', async () => {
projectA.id,
toggle,
user.email,
TEST_USER_ID,
);
await projectService.changeProject(
projectB.id,
@ -655,6 +666,7 @@ test('changing project should emit event even if user does not have a username s
projectA.id,
toggle,
user.email,
TEST_USER_ID,
);
const eventsBeforeChange = await stores.eventStore.getEvents();
await projectService.changeProject(
@ -689,11 +701,14 @@ test('should require equal project environments to move features', async () => {
projectA.id,
toggle,
user.email,
TEST_USER_ID,
);
await stores.environmentStore.create(environment);
await environmentService.addEnvironmentToProject(
environment.name,
projectB.id,
'test',
TEST_USER_ID,
);
await expect(() =>
@ -810,6 +825,7 @@ test('should add a user to the project with a custom role', async () => {
id: 8, // DELETE_FEATURE
},
],
createdByUserId: SYSTEM_USER_ID,
});
await projectService.addUser(
@ -860,6 +876,7 @@ test('should delete role entries when deleting project', async () => {
id: 8, // DELETE_FEATURE
},
],
createdByUserId: SYSTEM_USER_ID,
});
await projectService.addUser(project.id, customRole.id, user1.id, 'test');
@ -900,6 +917,7 @@ test('should change a users role in the project', async () => {
id: 8, // DELETE_FEATURE
},
],
createdByUserId: SYSTEM_USER_ID,
});
const member = await stores.roleStore.getRoleByName(RoleName.MEMBER);
@ -915,6 +933,7 @@ test('should change a users role in the project', async () => {
member.id,
projectUser.id,
'test',
TEST_USER_ID,
);
await projectService.addUser(
project.id,
@ -962,6 +981,7 @@ test('should update role for user on project', async () => {
ownerRole.id,
projectMember1.id,
'test',
TEST_USER_ID,
);
const { users } = await projectService.getAccessToProject(project.id);
@ -1006,6 +1026,7 @@ test('should able to assign role without existing members', async () => {
testRole.id,
projectMember1.id,
'test',
TEST_USER_ID,
);
const { users } = await projectService.getAccessToProject(project.id);
@ -1036,13 +1057,19 @@ describe('ensure project has at least one owner', () => {
ownerRole.id,
user.id,
'test',
TEST_USER_ID,
);
}).rejects.toThrowError(
new Error('A project must have at least one owner'),
);
await expect(async () => {
await projectService.removeUserAccess(project.id, user.id, 'test');
await projectService.removeUserAccess(
project.id,
user.id,
'test',
TEST_USER_ID,
);
}).rejects.toThrowError(
new Error('A project must have at least one owner'),
);
@ -1073,6 +1100,7 @@ describe('ensure project has at least one owner', () => {
[],
[memberUser.id],
'test',
TEST_USER_ID,
);
const usersBefore = await projectService.getProjectUsers(project.id);
@ -1080,6 +1108,7 @@ describe('ensure project has at least one owner', () => {
project.id,
memberUser.id,
'test',
TEST_USER_ID,
);
const usersAfter = await projectService.getProjectUsers(project.id);
expect(usersBefore).toHaveLength(2);
@ -1118,6 +1147,7 @@ describe('ensure project has at least one owner', () => {
memberRole.id,
user.id,
'test',
TEST_USER_ID,
);
}).rejects.toThrowError(
new Error('A project must have at least one owner'),
@ -1129,6 +1159,7 @@ describe('ensure project has at least one owner', () => {
user.id,
[memberRole.id],
'test',
TEST_USER_ID,
);
}).rejects.toThrowError(
new Error('A project must have at least one owner'),
@ -1153,6 +1184,7 @@ describe('ensure project has at least one owner', () => {
ownerRole.id,
group.id,
'test',
TEST_USER_ID,
);
// this should be fine, leaving the group as the only owner
@ -1162,6 +1194,7 @@ describe('ensure project has at least one owner', () => {
ownerRole.id,
user.id,
'test',
TEST_USER_ID,
);
return {
@ -1182,6 +1215,7 @@ describe('ensure project has at least one owner', () => {
ownerRole.id,
group.id,
'test',
TEST_USER_ID,
);
}).rejects.toThrowError(
new Error('A project must have at least one owner'),
@ -1192,6 +1226,7 @@ describe('ensure project has at least one owner', () => {
project.id,
group.id,
'test',
TEST_USER_ID,
);
}).rejects.toThrowError(
new Error('A project must have at least one owner'),
@ -1212,6 +1247,7 @@ describe('ensure project has at least one owner', () => {
memberRole.id,
group.id,
'test',
TEST_USER_ID,
);
}).rejects.toThrowError(
new Error('A project must have at least one owner'),
@ -1223,6 +1259,7 @@ describe('ensure project has at least one owner', () => {
group.id,
[memberRole.id],
'test',
TEST_USER_ID,
);
}).rejects.toThrowError(
new Error('A project must have at least one owner'),
@ -1258,6 +1295,7 @@ test('Should allow bulk update of group permissions', async () => {
id: 2, // CREATE_FEATURE
},
],
createdByUserId: SYSTEM_USER_ID,
});
await projectService.addAccess(
@ -1266,6 +1304,7 @@ test('Should allow bulk update of group permissions', async () => {
[group1.id],
[user1.id],
'some-admin-user',
TEST_USER_ID,
);
});
@ -1285,6 +1324,7 @@ test('Should bulk update of only users', async () => {
id: 2, // CREATE_FEATURE
},
],
createdByUserId: SYSTEM_USER_ID,
});
await projectService.addAccess(
@ -1293,6 +1333,7 @@ test('Should bulk update of only users', async () => {
[],
[user1.id],
'some-admin-user',
TEST_USER_ID,
);
});
@ -1320,6 +1361,7 @@ test('Should allow bulk update of only groups', async () => {
id: 2, // CREATE_FEATURE
},
],
createdByUserId: SYSTEM_USER_ID,
});
await projectService.addAccess(
@ -1328,6 +1370,7 @@ test('Should allow bulk update of only groups', async () => {
[group1.id],
[],
'some-admin-user',
TEST_USER_ID,
);
});
@ -1369,6 +1412,7 @@ test('Should allow permutations of roles, groups and users when adding a new acc
id: 2, // CREATE_FEATURE
},
],
createdByUserId: SYSTEM_USER_ID,
});
const role2 = await accessService.createRole({
@ -1379,6 +1423,7 @@ test('Should allow permutations of roles, groups and users when adding a new acc
id: 7, // UPDATE_FEATURE
},
],
createdByUserId: SYSTEM_USER_ID,
});
await projectService.addAccess(
@ -1387,6 +1432,7 @@ test('Should allow permutations of roles, groups and users when adding a new acc
[group1.id, group2.id],
[user1.id, user2.id],
'some-admin-user',
TEST_USER_ID,
);
const { users, groups } = await projectService.getAccessToProject(
@ -1487,6 +1533,7 @@ test('should calculate average time to production', async () => {
project.id,
toggle,
user.email,
TEST_USER_ID,
);
}),
);
@ -1500,6 +1547,7 @@ test('should calculate average time to production', async () => {
featureName: toggle.name,
environment: 'default',
createdBy: 'Fredrik',
createdByUserId: TEST_USER_ID,
}),
);
}),
@ -1534,6 +1582,7 @@ test('should calculate average time to production ignoring some items', async ()
featureName,
environment: 'default',
createdBy: 'Fredrik',
createdByUserId: TEST_USER_ID,
tags: [],
});
@ -1542,7 +1591,12 @@ test('should calculate average time to production ignoring some items', async ()
name: 'customEnv',
type: 'development',
});
await environmentService.addEnvironmentToProject('customEnv', project.id);
await environmentService.addEnvironmentToProject(
'customEnv',
project.id,
SYSTEM_USER.username,
SYSTEM_USER.id,
);
// actual toggle we take for calculations
const toggle = { name: 'main-toggle' };
@ -1550,6 +1604,7 @@ test('should calculate average time to production ignoring some items', async ()
project.id,
toggle,
user.email,
TEST_USER_ID,
);
await updateFeature(toggle.name, {
created_at: subDays(new Date(), 20),
@ -1569,6 +1624,7 @@ test('should calculate average time to production ignoring some items', async ()
project.id,
devToggle,
user.email,
TEST_USER_ID,
);
await eventService.storeEvent(
new FeatureEnvironmentEvent({
@ -1583,6 +1639,7 @@ test('should calculate average time to production ignoring some items', async ()
'default',
otherProjectToggle,
user.email,
TEST_USER_ID,
);
await eventService.storeEvent(
new FeatureEnvironmentEvent(makeEvent(otherProjectToggle.name)),
@ -1594,6 +1651,7 @@ test('should calculate average time to production ignoring some items', async ()
project.id,
nonReleaseToggle,
user.email,
TEST_USER_ID,
);
await eventService.storeEvent(
new FeatureEnvironmentEvent(makeEvent(nonReleaseToggle.name)),
@ -1605,6 +1663,7 @@ test('should calculate average time to production ignoring some items', async ()
project.id,
previouslyDeleteToggle,
user.email,
TEST_USER_ID,
);
await eventService.storeEvent(
new FeatureEnvironmentEvent(makeEvent(previouslyDeleteToggle.name)),
@ -1641,6 +1700,7 @@ test('should get correct amount of features created in current and past window',
project.id,
toggle,
user.email,
TEST_USER_ID,
);
}),
);
@ -1678,6 +1738,7 @@ test('should get correct amount of features archived in current and past window'
project.id,
toggle,
user.email,
TEST_USER_ID,
);
}),
);
@ -1770,6 +1831,7 @@ test('should return average time to production per toggle', async () => {
project.id,
toggle,
user.email,
TEST_USER_ID,
);
}),
);
@ -1783,6 +1845,7 @@ test('should return average time to production per toggle', async () => {
featureName: toggle.name,
environment: 'default',
createdBy: 'Fredrik',
createdByUserId: TEST_USER_ID,
}),
);
}),
@ -1838,6 +1901,7 @@ test('should return average time to production per toggle for a specific project
project1.id,
toggle,
user.email,
TEST_USER_ID,
);
}),
);
@ -1848,6 +1912,7 @@ test('should return average time to production per toggle for a specific project
project2.id,
toggle,
user.email,
TEST_USER_ID,
);
}),
);
@ -1861,6 +1926,7 @@ test('should return average time to production per toggle for a specific project
featureName: toggle.name,
environment: 'default',
createdBy: 'Fredrik',
createdByUserId: TEST_USER_ID,
}),
);
}),
@ -1875,6 +1941,7 @@ test('should return average time to production per toggle for a specific project
featureName: toggle.name,
environment: 'default',
createdBy: 'Fredrik',
createdByUserId: TEST_USER_ID,
}),
);
}),
@ -1925,6 +1992,7 @@ test('should return average time to production per toggle and include archived t
project1.id,
toggle,
user.email,
TEST_USER_ID,
);
}),
);
@ -1938,6 +2006,7 @@ test('should return average time to production per toggle and include archived t
featureName: toggle.name,
environment: 'default',
createdBy: 'Fredrik',
createdByUserId: TEST_USER_ID,
}),
);
}),

View File

@ -13,6 +13,7 @@ import { property } from 'fast-check';
let stores: IUnleashStores;
let db;
let service: SettingService;
const TEST_USER_ID = -9999;
beforeAll(async () => {
const config = createTestConfig();
@ -30,7 +31,13 @@ afterAll(async () => {
test('Can create new setting', async () => {
const someData = { some: 'blob' };
await service.insert('some-setting', someData, 'test-user', false);
await service.insert(
'some-setting',
someData,
'test-user',
TEST_USER_ID,
false,
);
const actual = await service.get('some-setting');
expect(actual).toStrictEqual(someData);
@ -44,8 +51,8 @@ test('Can create new setting', async () => {
test('Can delete setting', async () => {
const someData = { some: 'blob' };
await service.insert('some-setting', someData, 'test-user');
await service.delete('some-setting', 'test-user');
await service.insert('some-setting', someData, 'test-user', TEST_USER_ID);
await service.delete('some-setting', 'test-user', TEST_USER_ID);
const actual = await service.get('some-setting');
expect(actual).toBeUndefined();
@ -59,9 +66,14 @@ test('Can delete setting', async () => {
test('Sentitive SSO settings are redacted in event log', async () => {
const someData = { password: 'mySecretPassword' };
const property = 'unleash.enterprise.auth.oidc';
await service.insert(property, someData, 'a-user-in-places');
await service.insert(property, someData, 'a-user-in-places', TEST_USER_ID);
await service.insert(property, { password: 'changed' }, 'a-user-in-places');
await service.insert(
property,
{ password: 'changed' },
'a-user-in-places',
TEST_USER_ID,
);
const actual = await service.get(property);
const { eventStore } = stores;
@ -69,17 +81,24 @@ test('Sentitive SSO settings are redacted in event log', async () => {
type: SETTING_UPDATED,
});
expect(updatedEvents[0].preData).toEqual({ hideEventDetails: true });
await service.delete(property, 'test-user');
await service.delete(property, 'test-user', TEST_USER_ID);
});
test('Can update setting', async () => {
const { eventStore } = stores;
const someData = { some: 'blob' };
await service.insert('updated-setting', someData, 'test-user', false);
await service.insert(
'updated-setting',
someData,
'test-user',
TEST_USER_ID,
false,
);
await service.insert(
'updated-setting',
{ ...someData, test: 'fun' },
'test-user',
TEST_USER_ID,
false,
);
const updatedEvents = await eventStore.searchEvents({

View File

@ -131,7 +131,7 @@ test('Exporting featureEnvironmentVariants should work', async () => {
expect(
exportedData.featureEnvironments.find(
(fE) => fE.featureName === 'Some-feature',
).variants,
)!.variants,
).toHaveLength(3);
});
@ -140,6 +140,7 @@ test('Should import variants from old format and convert to new format (per envi
data: oldFormat,
keepExisting: false,
dropBeforeImport: true,
userId: -9999,
});
const featureEnvironments = await stores.featureEnvironmentStore.getAll();
expect(featureEnvironments).toHaveLength(6); // There are 3 environments enabled and 2 features
@ -154,12 +155,14 @@ test('Should import variants in new format (per environment)', async () => {
data: oldFormat,
keepExisting: false,
dropBeforeImport: true,
userId: -9999,
});
const exportedJson = await stateService.export({});
await stateService.import({
data: exportedJson,
keepExisting: false,
dropBeforeImport: true,
userId: -9999,
});
const featureEnvironments = await stores.featureEnvironmentStore.getAll();
expect(featureEnvironments).toHaveLength(6); // 3 environments, 2 features === 6 rows
@ -187,6 +190,7 @@ test('Importing states with deprecated strategies should keep their deprecated s
userName: 'strategy-importer',
dropBeforeImport: true,
keepExisting: false,
userId: -9999,
});
const deprecatedStrategy =
await stores.strategyStore.get('deprecatedstrat');
@ -199,6 +203,7 @@ test('Exporting a deprecated strategy and then importing it should keep correct
keepExisting: false,
dropBeforeImport: true,
userName: 'strategy importer',
userId: -9999,
});
const rolloutRandom = await stores.strategyStore.get(
'gradualRolloutRandom',

View File

@ -211,6 +211,7 @@ test('should not login user if simple auth is disabled', async () => {
simpleAuthSettingsKey,
{ disabled: true },
randomId(),
-9999,
true,
);

View File

@ -15,6 +15,7 @@ import { IUnleashStores } from '../../../lib/types';
let db;
let stores: IUnleashStores;
let eventStore: IEventStore;
const TEST_USER_ID = -9999;
beforeAll(async () => {
db = await dbInit('event_store_serial', getLogger);
@ -35,6 +36,7 @@ test('Should include id and createdAt when saving', async () => {
const event1 = {
type: APPLICATION_CREATED,
createdBy: '127.0.0.1',
createdByUserId: TEST_USER_ID,
data: {
clientIp: '127.0.0.1',
appName: 'test1',
@ -57,6 +59,7 @@ test('Should include empty tags array for new event', async () => {
const event = {
type: FEATURE_CREATED,
createdBy: 'me@mail.com',
createdByUserId: TEST_USER_ID,
data: {
name: 'someName',
enabled: true,
@ -83,6 +86,7 @@ test('Should be able to store multiple events at once', async () => {
jest.useFakeTimers();
const event1 = {
type: APPLICATION_CREATED,
createdByUserId: TEST_USER_ID,
createdBy: '127.0.0.1',
data: {
clientIp: '127.0.0.1',
@ -91,6 +95,7 @@ test('Should be able to store multiple events at once', async () => {
};
const event2 = {
type: APPLICATION_CREATED,
createdByUserId: TEST_USER_ID,
createdBy: '127.0.0.1',
data: {
clientIp: '127.0.0.1',
@ -99,6 +104,7 @@ test('Should be able to store multiple events at once', async () => {
};
const event3 = {
type: APPLICATION_CREATED,
createdByUserId: TEST_USER_ID,
createdBy: '127.0.0.1',
data: {
clientIp: '127.0.0.1',
@ -122,6 +128,7 @@ test('Should get all stored events', async () => {
const event = {
type: FEATURE_CREATED,
createdBy: 'me@mail.com',
createdByUserId: TEST_USER_ID,
data: {
name: 'someName',
enabled: true,
@ -139,6 +146,7 @@ test('Should get all stored events', async () => {
test('Should delete stored event', async () => {
const event = {
type: FEATURE_CREATED,
createdByUserId: TEST_USER_ID,
createdBy: 'me@mail.com',
data: {
name: 'someName',
@ -163,6 +171,7 @@ test('Should get stored event by id', async () => {
const event = {
type: FEATURE_CREATED,
createdBy: 'me@mail.com',
createdByUserId: TEST_USER_ID,
data: {
name: 'someName',
enabled: true,
@ -197,6 +206,8 @@ test('Should get all events of type', async () => {
project: data.project,
featureName: data.name,
createdBy: 'test-user',
createdByUserId: TEST_USER_ID,
data,
})
: new FeatureDeletedEvent({
@ -204,6 +215,8 @@ test('Should get all events of type', async () => {
preData: data,
featureName: data.name,
createdBy: 'test-user',
createdByUserId: TEST_USER_ID,
tags: [],
});
return eventStore.store(event);