mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-10 01:16:39 +02:00
feat: filter projectless events for normal users (#7914)
Now events that do not have project ( for example user creation, segment creation etc), will not be displayed to non root admins.
This commit is contained in:
parent
6c5ce52470
commit
df73c65073
3
src/lib/features/access/access-read-model-type.ts
Normal file
3
src/lib/features/access/access-read-model-type.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export interface IAccessReadModel {
|
||||||
|
isRootAdmin(userId: number): Promise<boolean>;
|
||||||
|
}
|
28
src/lib/features/access/access-read-model.ts
Normal file
28
src/lib/features/access/access-read-model.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import {
|
||||||
|
ADMIN_TOKEN_USER,
|
||||||
|
type IAccessStore,
|
||||||
|
type IUnleashStores,
|
||||||
|
SYSTEM_USER_ID,
|
||||||
|
} from '../../types';
|
||||||
|
import type { IAccessReadModel } from './access-read-model-type';
|
||||||
|
import * as permissions from '../../types/permissions';
|
||||||
|
|
||||||
|
const { ADMIN } = permissions;
|
||||||
|
|
||||||
|
export class AccessReadModel implements IAccessReadModel {
|
||||||
|
private store: IAccessStore;
|
||||||
|
|
||||||
|
constructor({ accessStore }: Pick<IUnleashStores, 'accessStore'>) {
|
||||||
|
this.store = accessStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
async isRootAdmin(userId: number): Promise<boolean> {
|
||||||
|
if (userId === SYSTEM_USER_ID || userId === ADMIN_TOKEN_USER.id) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const roles = await this.store.getRolesForUserId(userId);
|
||||||
|
return roles.some(
|
||||||
|
(role) => role.name.toLowerCase() === ADMIN.toLowerCase(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
24
src/lib/features/access/createAccessReadModel.ts
Normal file
24
src/lib/features/access/createAccessReadModel.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import type { Db, IUnleashConfig } from '../../server-impl';
|
||||||
|
import type { IAccessReadModel } from './access-read-model-type';
|
||||||
|
import { AccessReadModel } from './access-read-model';
|
||||||
|
import { AccessStore } from '../../db/access-store';
|
||||||
|
import FakeRoleStore from '../../../test/fixtures/fake-role-store';
|
||||||
|
import FakeAccessStore from '../../../test/fixtures/fake-access-store';
|
||||||
|
import type { IAccessStore } from '../../types';
|
||||||
|
|
||||||
|
export const createAccessReadModel = (
|
||||||
|
db: Db,
|
||||||
|
config: IUnleashConfig,
|
||||||
|
): IAccessReadModel => {
|
||||||
|
const { eventBus, getLogger } = config;
|
||||||
|
const accessStore = new AccessStore(db, eventBus, getLogger);
|
||||||
|
return new AccessReadModel({ accessStore });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createFakeAccessReadModel = (
|
||||||
|
accessStore?: IAccessStore,
|
||||||
|
): IAccessReadModel => {
|
||||||
|
const roleStore = new FakeRoleStore();
|
||||||
|
const finalAccessStore = accessStore ?? new FakeAccessStore(roleStore);
|
||||||
|
return new AccessReadModel({ accessStore: finalAccessStore });
|
||||||
|
};
|
@ -33,7 +33,6 @@ export const createAccessService = (
|
|||||||
{ getLogger },
|
{ getLogger },
|
||||||
eventService,
|
eventService,
|
||||||
);
|
);
|
||||||
|
|
||||||
return new AccessService(
|
return new AccessService(
|
||||||
{ accessStore, accountStore, roleStore, environmentStore },
|
{ accessStore, accountStore, roleStore, environmentStore },
|
||||||
{ getLogger },
|
{ getLogger },
|
||||||
|
@ -13,6 +13,10 @@ import {
|
|||||||
createFakePrivateProjectChecker,
|
createFakePrivateProjectChecker,
|
||||||
createPrivateProjectChecker,
|
createPrivateProjectChecker,
|
||||||
} from '../private-project/createPrivateProjectChecker';
|
} from '../private-project/createPrivateProjectChecker';
|
||||||
|
import {
|
||||||
|
createAccessReadModel,
|
||||||
|
createFakeAccessReadModel,
|
||||||
|
} from '../access/createAccessReadModel';
|
||||||
|
|
||||||
export const createEventsService: (
|
export const createEventsService: (
|
||||||
db: Db,
|
db: Db,
|
||||||
@ -25,10 +29,12 @@ export const createEventsService: (
|
|||||||
config.getLogger,
|
config.getLogger,
|
||||||
);
|
);
|
||||||
const privateProjectChecker = createPrivateProjectChecker(db, config);
|
const privateProjectChecker = createPrivateProjectChecker(db, config);
|
||||||
|
const accessReadModel = createAccessReadModel(db, config);
|
||||||
return new EventService(
|
return new EventService(
|
||||||
{ eventStore, featureTagStore },
|
{ eventStore, featureTagStore },
|
||||||
config,
|
config,
|
||||||
privateProjectChecker,
|
privateProjectChecker,
|
||||||
|
accessReadModel,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -43,9 +49,11 @@ export const createFakeEventsService: (
|
|||||||
const featureTagStore =
|
const featureTagStore =
|
||||||
stores?.featureTagStore || new FakeFeatureTagStore();
|
stores?.featureTagStore || new FakeFeatureTagStore();
|
||||||
const fakePrivateProjectChecker = createFakePrivateProjectChecker();
|
const fakePrivateProjectChecker = createFakePrivateProjectChecker();
|
||||||
|
const fakeAccessReadModel = createFakeAccessReadModel();
|
||||||
return new EventService(
|
return new EventService(
|
||||||
{ eventStore, featureTagStore },
|
{ eventStore, featureTagStore },
|
||||||
config,
|
config,
|
||||||
fakePrivateProjectChecker,
|
fakePrivateProjectChecker,
|
||||||
|
fakeAccessReadModel,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -5,7 +5,7 @@ import { EventEmitter } from 'stream';
|
|||||||
import { EVENTS_CREATED_BY_PROCESSED } from '../../metric-events';
|
import { EVENTS_CREATED_BY_PROCESSED } from '../../metric-events';
|
||||||
import type { IUnleashConfig } from '../../types';
|
import type { IUnleashConfig } from '../../types';
|
||||||
import { createTestConfig } from '../../../test/config/test-config';
|
import { createTestConfig } from '../../../test/config/test-config';
|
||||||
import EventService from './event-service';
|
import { createEventsService } from './createEventsService';
|
||||||
|
|
||||||
let db: ITestDb;
|
let db: ITestDb;
|
||||||
|
|
||||||
@ -126,14 +126,11 @@ test('emits events with details on amount of updated rows', async () => {
|
|||||||
const store = new EventStore(db.rawDatabase, getLogger);
|
const store = new EventStore(db.rawDatabase, getLogger);
|
||||||
|
|
||||||
const eventBus = new EventEmitter();
|
const eventBus = new EventEmitter();
|
||||||
const service = new EventService(
|
const config = createTestConfig();
|
||||||
{ eventStore: store, featureTagStore: db.stores.featureTagStore },
|
const service = createEventsService(db.rawDatabase, config);
|
||||||
{ getLogger, eventBus },
|
|
||||||
{} as any,
|
|
||||||
);
|
|
||||||
let triggered = false;
|
let triggered = false;
|
||||||
|
|
||||||
eventBus.on(EVENTS_CREATED_BY_PROCESSED, ({ updated }) => {
|
config.eventBus.on(EVENTS_CREATED_BY_PROCESSED, ({ updated }) => {
|
||||||
expect(updated).toBe(2);
|
expect(updated).toBe(2);
|
||||||
triggered = true;
|
triggered = true;
|
||||||
});
|
});
|
||||||
|
@ -16,6 +16,7 @@ import { parseSearchOperatorValue } from '../feature-search/search-utils';
|
|||||||
import { endOfDay, formatISO } from 'date-fns';
|
import { endOfDay, formatISO } from 'date-fns';
|
||||||
import type { IPrivateProjectChecker } from '../private-project/privateProjectCheckerType';
|
import type { IPrivateProjectChecker } from '../private-project/privateProjectCheckerType';
|
||||||
import type { ProjectAccess } from '../private-project/privateProjectStore';
|
import type { ProjectAccess } from '../private-project/privateProjectStore';
|
||||||
|
import type { IAccessReadModel } from '../access/access-read-model-type';
|
||||||
|
|
||||||
export default class EventService {
|
export default class EventService {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
@ -24,6 +25,8 @@ export default class EventService {
|
|||||||
|
|
||||||
private featureTagStore: IFeatureTagStore;
|
private featureTagStore: IFeatureTagStore;
|
||||||
|
|
||||||
|
private accessReadModel: IAccessReadModel;
|
||||||
|
|
||||||
private privateProjectChecker: IPrivateProjectChecker;
|
private privateProjectChecker: IPrivateProjectChecker;
|
||||||
|
|
||||||
private eventBus: EventEmitter;
|
private eventBus: EventEmitter;
|
||||||
@ -35,12 +38,14 @@ export default class EventService {
|
|||||||
}: Pick<IUnleashStores, 'eventStore' | 'featureTagStore'>,
|
}: Pick<IUnleashStores, 'eventStore' | 'featureTagStore'>,
|
||||||
{ getLogger, eventBus }: Pick<IUnleashConfig, 'getLogger' | 'eventBus'>,
|
{ getLogger, eventBus }: Pick<IUnleashConfig, 'getLogger' | 'eventBus'>,
|
||||||
privateProjectChecker: IPrivateProjectChecker,
|
privateProjectChecker: IPrivateProjectChecker,
|
||||||
|
accessReadModel: IAccessReadModel,
|
||||||
) {
|
) {
|
||||||
this.logger = getLogger('services/event-service.ts');
|
this.logger = getLogger('services/event-service.ts');
|
||||||
this.eventStore = eventStore;
|
this.eventStore = eventStore;
|
||||||
this.privateProjectChecker = privateProjectChecker;
|
this.privateProjectChecker = privateProjectChecker;
|
||||||
this.featureTagStore = featureTagStore;
|
this.featureTagStore = featureTagStore;
|
||||||
this.eventBus = eventBus;
|
this.eventBus = eventBus;
|
||||||
|
this.accessReadModel = accessReadModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEvents(): Promise<IEventList> {
|
async getEvents(): Promise<IEventList> {
|
||||||
@ -77,6 +82,8 @@ export default class EventService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const queryParams = this.convertToDbParams(search);
|
const queryParams = this.convertToDbParams(search);
|
||||||
|
const projectFilter = await this.getProjectFilterForNonAdmins(userId);
|
||||||
|
queryParams.push(...projectFilter);
|
||||||
|
|
||||||
const totalEvents = await this.eventStore.searchEventsCount(
|
const totalEvents = await this.eventStore.searchEventsCount(
|
||||||
{
|
{
|
||||||
@ -222,6 +229,14 @@ export default class EventService {
|
|||||||
async getEventCreators() {
|
async getEventCreators() {
|
||||||
return this.eventStore.getEventCreators();
|
return this.eventStore.getEventCreators();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getProjectFilterForNonAdmins(userId: number): Promise<IQueryParam[]> {
|
||||||
|
const isRootAdmin = await this.accessReadModel.isRootAdmin(userId);
|
||||||
|
if (!isRootAdmin) {
|
||||||
|
return [{ field: 'project', operator: 'IS_NOT', values: [null] }];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const filterAccessibleProjects = (
|
export const filterAccessibleProjects = (
|
||||||
|
@ -50,6 +50,7 @@ export const applyGenericQueryParams = (
|
|||||||
queryParams: IQueryParam[],
|
queryParams: IQueryParam[],
|
||||||
): void => {
|
): void => {
|
||||||
queryParams.forEach((param) => {
|
queryParams.forEach((param) => {
|
||||||
|
const isSingleParam = param.values.length === 1;
|
||||||
switch (param.operator) {
|
switch (param.operator) {
|
||||||
case 'IS':
|
case 'IS':
|
||||||
case 'IS_ANY_OF':
|
case 'IS_ANY_OF':
|
||||||
@ -57,7 +58,11 @@ export const applyGenericQueryParams = (
|
|||||||
break;
|
break;
|
||||||
case 'IS_NOT':
|
case 'IS_NOT':
|
||||||
case 'IS_NONE_OF':
|
case 'IS_NONE_OF':
|
||||||
|
if (isSingleParam) {
|
||||||
|
query.whereNot(param.field, param.values[0]);
|
||||||
|
} else {
|
||||||
query.whereNotIn(param.field, param.values);
|
query.whereNotIn(param.field, param.values);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'IS_BEFORE':
|
case 'IS_BEFORE':
|
||||||
query.where(param.field, '<', param.values[0]);
|
query.where(param.field, '<', param.values[0]);
|
||||||
|
@ -55,7 +55,7 @@ export type IQueryOperator =
|
|||||||
export interface IQueryParam {
|
export interface IQueryParam {
|
||||||
field: string;
|
field: string;
|
||||||
operator: IQueryOperator;
|
operator: IQueryOperator;
|
||||||
values: string[];
|
values: (string | null)[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IFeatureStrategiesStore
|
export interface IFeatureStrategiesStore
|
||||||
|
@ -154,7 +154,7 @@ export class SegmentService implements ISegmentService {
|
|||||||
await this.eventService.storeEvent(
|
await this.eventService.storeEvent(
|
||||||
new SegmentCreatedEvent({
|
new SegmentCreatedEvent({
|
||||||
data: segment,
|
data: segment,
|
||||||
project: segment.project || 'no-project',
|
project: segment.project,
|
||||||
auditUser,
|
auditUser,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
@ -23,13 +23,22 @@ import {
|
|||||||
} from '../../lib/types';
|
} from '../../lib/types';
|
||||||
import BadDataError from '../../lib/error/bad-data-error';
|
import BadDataError from '../../lib/error/bad-data-error';
|
||||||
import { createFakeEventsService } from '../../lib/features/events/createEventsService';
|
import { createFakeEventsService } from '../../lib/features/events/createEventsService';
|
||||||
|
import { createFakeAccessReadModel } from '../features/access/createAccessReadModel';
|
||||||
|
|
||||||
function getSetup() {
|
function getSetup() {
|
||||||
const config = createTestConfig({
|
const config = createTestConfig({
|
||||||
getLogger,
|
getLogger,
|
||||||
});
|
});
|
||||||
|
|
||||||
return createFakeAccessService(config);
|
const { accessService, eventStore, accessStore } =
|
||||||
|
createFakeAccessService(config);
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessService,
|
||||||
|
eventStore,
|
||||||
|
accessStore,
|
||||||
|
accessReadModel: createFakeAccessReadModel(accessStore),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
test('should fail when name exists', async () => {
|
test('should fail when name exists', async () => {
|
||||||
@ -287,28 +296,28 @@ describe('addAccessToProject', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should return true if user has admin role', async () => {
|
test('should return true if user has admin role', async () => {
|
||||||
const { accessService, accessStore } = getSetup();
|
const { accessReadModel, accessStore } = getSetup();
|
||||||
|
|
||||||
const userId = 1;
|
const userId = 1;
|
||||||
accessStore.getRolesForUserId = jest
|
accessStore.getRolesForUserId = jest
|
||||||
.fn()
|
.fn()
|
||||||
.mockResolvedValue([{ id: 1, name: 'ADMIN', type: 'custom' }]);
|
.mockResolvedValue([{ id: 1, name: 'ADMIN', type: 'custom' }]);
|
||||||
|
|
||||||
const result = await accessService.isRootAdmin(userId);
|
const result = await accessReadModel.isRootAdmin(userId);
|
||||||
|
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
expect(accessStore.getRolesForUserId).toHaveBeenCalledWith(userId);
|
expect(accessStore.getRolesForUserId).toHaveBeenCalledWith(userId);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return false if user does not have admin role', async () => {
|
test('should return false if user does not have admin role', async () => {
|
||||||
const { accessService, accessStore } = getSetup();
|
const { accessReadModel, accessStore } = getSetup();
|
||||||
|
|
||||||
const userId = 2;
|
const userId = 2;
|
||||||
accessStore.getRolesForUserId = jest
|
accessStore.getRolesForUserId = jest
|
||||||
.fn()
|
.fn()
|
||||||
.mockResolvedValue([{ id: 2, name: 'user', type: 'custom' }]);
|
.mockResolvedValue([{ id: 2, name: 'user', type: 'custom' }]);
|
||||||
|
|
||||||
const result = await accessService.isRootAdmin(userId);
|
const result = await accessReadModel.isRootAdmin(userId);
|
||||||
|
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
expect(accessStore.getRolesForUserId).toHaveBeenCalledWith(userId);
|
expect(accessStore.getRolesForUserId).toHaveBeenCalledWith(userId);
|
||||||
|
@ -41,13 +41,11 @@ import BadDataError from '../error/bad-data-error';
|
|||||||
import type { IGroup } from '../types/group';
|
import type { IGroup } from '../types/group';
|
||||||
import type { GroupService } from './group-service';
|
import type { GroupService } from './group-service';
|
||||||
import {
|
import {
|
||||||
ADMIN_TOKEN_USER,
|
|
||||||
type IUnleashConfig,
|
type IUnleashConfig,
|
||||||
type IUserAccessOverview,
|
type IUserAccessOverview,
|
||||||
RoleCreatedEvent,
|
RoleCreatedEvent,
|
||||||
RoleDeletedEvent,
|
RoleDeletedEvent,
|
||||||
RoleUpdatedEvent,
|
RoleUpdatedEvent,
|
||||||
SYSTEM_USER_ID,
|
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import type EventService from '../features/events/event-service';
|
import type EventService from '../features/events/event-service';
|
||||||
|
|
||||||
@ -889,14 +887,4 @@ export class AccessService {
|
|||||||
async getUserAccessOverview(): Promise<IUserAccessOverview[]> {
|
async getUserAccessOverview(): Promise<IUserAccessOverview[]> {
|
||||||
return this.store.getUserAccessOverview();
|
return this.store.getUserAccessOverview();
|
||||||
}
|
}
|
||||||
|
|
||||||
async isRootAdmin(userId: number): Promise<boolean> {
|
|
||||||
if (userId === SYSTEM_USER_ID || userId === ADMIN_TOKEN_USER.id) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const roles = await this.store.getRolesForUserId(userId);
|
|
||||||
return roles.some(
|
|
||||||
(role) => role.name.toLowerCase() === ADMIN.toLowerCase(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -44,6 +44,7 @@ test('Should only store events for potentially stale on', async () => {
|
|||||||
},
|
},
|
||||||
config,
|
config,
|
||||||
{},
|
{},
|
||||||
|
{},
|
||||||
);
|
);
|
||||||
|
|
||||||
const featureToggleService = new FeatureToggleService(
|
const featureToggleService = new FeatureToggleService(
|
||||||
|
@ -1930,12 +1930,12 @@ export class AddonConfigDeletedEvent extends BaseEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class SegmentCreatedEvent extends BaseEvent {
|
export class SegmentCreatedEvent extends BaseEvent {
|
||||||
readonly project: string;
|
readonly project: string | undefined;
|
||||||
readonly data: any;
|
readonly data: any;
|
||||||
|
|
||||||
constructor(eventData: {
|
constructor(eventData: {
|
||||||
auditUser: IAuditUser;
|
auditUser: IAuditUser;
|
||||||
project: string;
|
project: string | undefined;
|
||||||
data: any;
|
data: any;
|
||||||
}) {
|
}) {
|
||||||
super(SEGMENT_CREATED, eventData.auditUser);
|
super(SEGMENT_CREATED, eventData.auditUser);
|
||||||
|
@ -1,34 +1,97 @@
|
|||||||
import type { EventSearchQueryParameters } from '../../../../lib/openapi/spec/event-search-query-parameters';
|
import type { EventSearchQueryParameters } from '../../../../lib/openapi/spec/event-search-query-parameters';
|
||||||
import dbInit, { type ITestDb } from '../../helpers/database-init';
|
import dbInit, { type ITestDb } from '../../helpers/database-init';
|
||||||
|
|
||||||
import { FEATURE_CREATED, type IUnleashConfig } from '../../../../lib/types';
|
|
||||||
import type { EventService } from '../../../../lib/services';
|
|
||||||
import getLogger from '../../../fixtures/no-logger';
|
|
||||||
import {
|
import {
|
||||||
type IUnleashTest,
|
FEATURE_CREATED,
|
||||||
setupAppWithCustomConfig,
|
type IUnleashConfig,
|
||||||
} from '../../helpers/test-helper';
|
type IUnleashStores,
|
||||||
|
RoleName,
|
||||||
|
USER_CREATED,
|
||||||
|
} from '../../../../lib/types';
|
||||||
|
import type { AccessService, EventService } from '../../../../lib/services';
|
||||||
|
import getLogger from '../../../fixtures/no-logger';
|
||||||
|
import { type IUnleashTest, setupAppWithAuth } from '../../helpers/test-helper';
|
||||||
import { createEventsService } from '../../../../lib/features';
|
import { createEventsService } from '../../../../lib/features';
|
||||||
import { createTestConfig } from '../../../config/test-config';
|
import { createTestConfig } from '../../../config/test-config';
|
||||||
|
import type { IRole } from '../../../../lib/types/stores/access-store';
|
||||||
|
|
||||||
let app: IUnleashTest;
|
let app: IUnleashTest;
|
||||||
let db: ITestDb;
|
let db: ITestDb;
|
||||||
let eventService: EventService;
|
let eventService: EventService;
|
||||||
const TEST_USER_ID = -9999;
|
const TEST_USER_ID = -9999;
|
||||||
|
const regularUserName = 'import-user';
|
||||||
|
const adminUserName = 'admin-user';
|
||||||
|
|
||||||
const config: IUnleashConfig = createTestConfig();
|
const config: IUnleashConfig = createTestConfig();
|
||||||
|
let adminRole: IRole;
|
||||||
|
let stores: IUnleashStores;
|
||||||
|
let accessService: AccessService;
|
||||||
|
|
||||||
|
const loginRegularUser = () =>
|
||||||
|
app.request
|
||||||
|
.post(`/auth/demo/login`)
|
||||||
|
.send({
|
||||||
|
email: `${regularUserName}@getunleash.io`,
|
||||||
|
})
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const loginAdminUser = () =>
|
||||||
|
app.request
|
||||||
|
.post(`/auth/demo/login`)
|
||||||
|
.send({
|
||||||
|
email: `${adminUserName}@getunleash.io`,
|
||||||
|
})
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
const createUserEditorAccess = async (name, email) => {
|
||||||
|
const { userStore } = stores;
|
||||||
|
const user = await userStore.insert({
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
});
|
||||||
|
return user;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createUserAdminAccess = async (name, email) => {
|
||||||
|
const { userStore } = stores;
|
||||||
|
const user = await userStore.insert({
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
});
|
||||||
|
await accessService.addUserToRole(user.id, adminRole.id, 'default');
|
||||||
|
return user;
|
||||||
|
};
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
db = await dbInit('event_search', getLogger);
|
db = await dbInit('event_search', getLogger);
|
||||||
app = await setupAppWithCustomConfig(db.stores, {
|
stores = db.stores;
|
||||||
|
app = await setupAppWithAuth(
|
||||||
|
db.stores,
|
||||||
|
{
|
||||||
experimental: {
|
experimental: {
|
||||||
flags: {
|
flags: {
|
||||||
strictSchemaValidation: true,
|
strictSchemaValidation: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
|
db.rawDatabase,
|
||||||
|
);
|
||||||
|
|
||||||
eventService = createEventsService(db.rawDatabase, config);
|
eventService = createEventsService(db.rawDatabase, config);
|
||||||
|
|
||||||
|
accessService = app.services.accessService;
|
||||||
|
|
||||||
|
const roles = await accessService.getRootRoles();
|
||||||
|
adminRole = roles.find((role) => role.name === RoleName.ADMIN)!;
|
||||||
|
|
||||||
|
await createUserEditorAccess(
|
||||||
|
regularUserName,
|
||||||
|
`${regularUserName}@getunleash.io`,
|
||||||
|
);
|
||||||
|
await createUserAdminAccess(
|
||||||
|
adminUserName,
|
||||||
|
`${adminUserName}@getunleash.io`,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@ -37,6 +100,7 @@ afterAll(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
await loginAdminUser();
|
||||||
await db.stores.featureToggleStore.deleteAll();
|
await db.stores.featureToggleStore.deleteAll();
|
||||||
await db.stores.segmentStore.deleteAll();
|
await db.stores.segmentStore.deleteAll();
|
||||||
await db.stores.eventStore.deleteAll();
|
await db.stores.eventStore.deleteAll();
|
||||||
@ -182,6 +246,7 @@ test('should filter events by type', async () => {
|
|||||||
test('should filter events by created by', async () => {
|
test('should filter events by created by', async () => {
|
||||||
await eventService.storeEvent({
|
await eventService.storeEvent({
|
||||||
type: FEATURE_CREATED,
|
type: FEATURE_CREATED,
|
||||||
|
project: 'default',
|
||||||
createdBy: 'admin1@example.com',
|
createdBy: 'admin1@example.com',
|
||||||
createdByUserId: TEST_USER_ID + 1,
|
createdByUserId: TEST_USER_ID + 1,
|
||||||
ip: '127.0.0.1',
|
ip: '127.0.0.1',
|
||||||
@ -189,6 +254,7 @@ test('should filter events by created by', async () => {
|
|||||||
|
|
||||||
await eventService.storeEvent({
|
await eventService.storeEvent({
|
||||||
type: FEATURE_CREATED,
|
type: FEATURE_CREATED,
|
||||||
|
project: 'default',
|
||||||
createdBy: 'admin2@example.com',
|
createdBy: 'admin2@example.com',
|
||||||
createdByUserId: TEST_USER_ID,
|
createdByUserId: TEST_USER_ID,
|
||||||
ip: '127.0.0.1',
|
ip: '127.0.0.1',
|
||||||
@ -311,8 +377,7 @@ test('should filter events by feature using IS_ANY_OF', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await eventService.storeEvent({
|
await eventService.storeEvent({
|
||||||
type: FEATURE_CREATED,
|
type: USER_CREATED,
|
||||||
project: 'default',
|
|
||||||
featureName: 'my_feature_b',
|
featureName: 'my_feature_b',
|
||||||
createdBy: 'test-user',
|
createdBy: 'test-user',
|
||||||
createdByUserId: TEST_USER_ID,
|
createdByUserId: TEST_USER_ID,
|
||||||
@ -335,7 +400,7 @@ test('should filter events by feature using IS_ANY_OF', async () => {
|
|||||||
expect(body).toMatchObject({
|
expect(body).toMatchObject({
|
||||||
events: [
|
events: [
|
||||||
{
|
{
|
||||||
type: 'feature-created',
|
type: 'user-created',
|
||||||
featureName: 'my_feature_b',
|
featureName: 'my_feature_b',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -390,3 +455,63 @@ test('should filter events by project using IS_ANY_OF', async () => {
|
|||||||
total: 2,
|
total: 2,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should not show user creation events for non-admins', async () => {
|
||||||
|
await loginRegularUser();
|
||||||
|
await eventService.storeEvent({
|
||||||
|
type: USER_CREATED,
|
||||||
|
createdBy: 'test-user',
|
||||||
|
createdByUserId: TEST_USER_ID,
|
||||||
|
ip: '127.0.0.1',
|
||||||
|
});
|
||||||
|
|
||||||
|
await eventService.storeEvent({
|
||||||
|
type: FEATURE_CREATED,
|
||||||
|
project: 'default',
|
||||||
|
createdBy: 'test-user',
|
||||||
|
createdByUserId: TEST_USER_ID,
|
||||||
|
ip: '127.0.0.1',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { body } = await searchEvents({});
|
||||||
|
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
events: [
|
||||||
|
{
|
||||||
|
type: FEATURE_CREATED,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
total: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show user creation events for admins', async () => {
|
||||||
|
await eventService.storeEvent({
|
||||||
|
type: USER_CREATED,
|
||||||
|
createdBy: 'test-user',
|
||||||
|
createdByUserId: TEST_USER_ID,
|
||||||
|
ip: '127.0.0.1',
|
||||||
|
});
|
||||||
|
|
||||||
|
await eventService.storeEvent({
|
||||||
|
type: FEATURE_CREATED,
|
||||||
|
project: 'default',
|
||||||
|
createdBy: 'test-user',
|
||||||
|
createdByUserId: TEST_USER_ID,
|
||||||
|
ip: '127.0.0.1',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { body } = await searchEvents({});
|
||||||
|
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
events: [
|
||||||
|
{
|
||||||
|
type: FEATURE_CREATED,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: USER_CREATED,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
total: 2,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user