mirror of
https://github.com/Unleash/unleash.git
synced 2025-03-04 00:18:40 +01:00
fix: log project access events (#3683)
https://linear.app/unleash/issue/2-992/register-event-when-granting-access-to-a-project - Correctly registers a new `project-access-added` event; - Adds an anonymization method that supports anonymizing objects, recursively searching for keys to anonymize; - Fixes the event label used by `ProjectUserUpdateRoleEvent` to be `PROJECT_USER_ROLE_CHANGED`; 
This commit is contained in:
parent
871c9da83c
commit
b013d4286c
@ -5,7 +5,7 @@ import EventService from '../../services/event-service';
|
|||||||
import { ADMIN, NONE } from '../../types/permissions';
|
import { ADMIN, NONE } from '../../types/permissions';
|
||||||
import { IEvent, IEventList } from '../../types/events';
|
import { IEvent, IEventList } from '../../types/events';
|
||||||
import Controller from '../controller';
|
import Controller from '../controller';
|
||||||
import { anonymise } from '../../util/anonymise';
|
import { anonymiseKeys } from '../../util/anonymise';
|
||||||
import { OpenApiService } from '../../services/openapi-service';
|
import { OpenApiService } from '../../services/openapi-service';
|
||||||
import { createResponseSchema } from '../../openapi/util/create-response-schema';
|
import { createResponseSchema } from '../../openapi/util/create-response-schema';
|
||||||
import { endpointDescriptions } from '../../openapi/endpoint-descriptions';
|
import { endpointDescriptions } from '../../openapi/endpoint-descriptions';
|
||||||
@ -23,6 +23,7 @@ import { createRequestSchema } from '../../openapi/util/create-request-schema';
|
|||||||
import { SearchEventsSchema } from '../../openapi/spec/search-events-schema';
|
import { SearchEventsSchema } from '../../openapi/spec/search-events-schema';
|
||||||
import { IFlagResolver } from '../../types/experimental';
|
import { IFlagResolver } from '../../types/experimental';
|
||||||
|
|
||||||
|
const ANON_KEYS = ['email', 'username', 'createdBy'];
|
||||||
const version = 1;
|
const version = 1;
|
||||||
export default class EventController extends Controller {
|
export default class EventController extends Controller {
|
||||||
private eventService: EventService;
|
private eventService: EventService;
|
||||||
@ -108,24 +109,7 @@ export default class EventController extends Controller {
|
|||||||
|
|
||||||
maybeAnonymiseEvents(events: IEvent[]): IEvent[] {
|
maybeAnonymiseEvents(events: IEvent[]): IEvent[] {
|
||||||
if (this.flagResolver.isEnabled('anonymiseEventLog')) {
|
if (this.flagResolver.isEnabled('anonymiseEventLog')) {
|
||||||
return events.map((e: IEvent) => ({
|
return anonymiseKeys(events, ANON_KEYS);
|
||||||
...e,
|
|
||||||
createdBy: anonymise(e.createdBy),
|
|
||||||
data:
|
|
||||||
e.data && 'email' in e.data
|
|
||||||
? {
|
|
||||||
...e.data,
|
|
||||||
email: anonymise(e.data.email),
|
|
||||||
}
|
|
||||||
: e.data,
|
|
||||||
preData:
|
|
||||||
e.preData && 'email' in e.preData
|
|
||||||
? {
|
|
||||||
...e.preData,
|
|
||||||
email: anonymise(e.preData.email),
|
|
||||||
}
|
|
||||||
: e.preData,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
return events;
|
return events;
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import createStores from '../../../test/fixtures/store';
|
|||||||
import getApp from '../../app';
|
import getApp from '../../app';
|
||||||
import {
|
import {
|
||||||
FeatureCreatedEvent,
|
FeatureCreatedEvent,
|
||||||
|
ProjectAccessAddedEvent,
|
||||||
ProjectUserAddedEvent,
|
ProjectUserAddedEvent,
|
||||||
ProjectUserRemovedEvent,
|
ProjectUserRemovedEvent,
|
||||||
} from '../../types/events';
|
} from '../../types/events';
|
||||||
@ -104,3 +105,33 @@ test('should also anonymise email fields in data and preData properties', async
|
|||||||
expect(body.events[0].data.email).not.toBe(email1);
|
expect(body.events[0].data.email).not.toBe(email1);
|
||||||
expect(body.events[1].preData.email).not.toBe(email2);
|
expect(body.events[1].preData.email).not.toBe(email2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should anonymise any PII fields, no matter the depth', async () => {
|
||||||
|
const testUsername = 'test-username';
|
||||||
|
|
||||||
|
const { request, base, eventStore } = await getSetup(true);
|
||||||
|
eventStore.store(
|
||||||
|
new ProjectAccessAddedEvent({
|
||||||
|
createdBy: 'some@email.com',
|
||||||
|
data: {
|
||||||
|
groups: [
|
||||||
|
{
|
||||||
|
name: 'test',
|
||||||
|
project: 'default',
|
||||||
|
users: [{ username: testUsername }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
project: 'default',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const { body } = await request
|
||||||
|
.get(`${base}/api/admin/events`)
|
||||||
|
.expect('Content-Type', /json/)
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(body.events.length).toBe(1);
|
||||||
|
expect(body.events[0].data.groups[0].users[0].username).not.toBe(
|
||||||
|
testUsername,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
@ -35,6 +35,7 @@ import {
|
|||||||
ProjectUserUpdateRoleEvent,
|
ProjectUserUpdateRoleEvent,
|
||||||
RoleName,
|
RoleName,
|
||||||
IFlagResolver,
|
IFlagResolver,
|
||||||
|
ProjectAccessAddedEvent,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import {
|
import {
|
||||||
IProjectQuery,
|
IProjectQuery,
|
||||||
@ -348,6 +349,7 @@ export default class ProjectService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deprecated: See addAccess instead.
|
||||||
async addUser(
|
async addUser(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
roleId: number,
|
roleId: number,
|
||||||
@ -492,13 +494,25 @@ export default class ProjectService {
|
|||||||
usersAndGroups: IProjectAccessModel,
|
usersAndGroups: IProjectAccessModel,
|
||||||
createdBy: string,
|
createdBy: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return this.accessService.addAccessToProject(
|
await this.accessService.addAccessToProject(
|
||||||
usersAndGroups.users,
|
usersAndGroups.users,
|
||||||
usersAndGroups.groups,
|
usersAndGroups.groups,
|
||||||
projectId,
|
projectId,
|
||||||
roleId,
|
roleId,
|
||||||
createdBy,
|
createdBy,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await this.eventStore.store(
|
||||||
|
new ProjectAccessAddedEvent({
|
||||||
|
project: projectId,
|
||||||
|
createdBy,
|
||||||
|
data: {
|
||||||
|
roleId,
|
||||||
|
groups: usersAndGroups.groups.map(({ id }) => id),
|
||||||
|
users: usersAndGroups.users.map(({ id }) => id),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async findProjectGroupRole(
|
async findProjectGroupRole(
|
||||||
|
@ -40,6 +40,7 @@ export const DROP_STRATEGIES = 'drop-strategies';
|
|||||||
export const CONTEXT_FIELD_CREATED = 'context-field-created';
|
export const CONTEXT_FIELD_CREATED = 'context-field-created';
|
||||||
export const CONTEXT_FIELD_UPDATED = 'context-field-updated';
|
export const CONTEXT_FIELD_UPDATED = 'context-field-updated';
|
||||||
export const CONTEXT_FIELD_DELETED = 'context-field-deleted';
|
export const CONTEXT_FIELD_DELETED = 'context-field-deleted';
|
||||||
|
export const PROJECT_ACCESS_ADDED = 'project-access-added';
|
||||||
export const PROJECT_CREATED = 'project-created';
|
export const PROJECT_CREATED = 'project-created';
|
||||||
export const PROJECT_UPDATED = 'project-updated';
|
export const PROJECT_UPDATED = 'project-updated';
|
||||||
export const PROJECT_DELETED = 'project-deleted';
|
export const PROJECT_DELETED = 'project-deleted';
|
||||||
@ -563,7 +564,7 @@ export class ProjectUserUpdateRoleEvent extends BaseEvent {
|
|||||||
data: any;
|
data: any;
|
||||||
preData: any;
|
preData: any;
|
||||||
}) {
|
}) {
|
||||||
super(PROJECT_USER_REMOVED, eventData.createdBy);
|
super(PROJECT_USER_ROLE_CHANGED, eventData.createdBy);
|
||||||
const { project, data, preData } = eventData;
|
const { project, data, preData } = eventData;
|
||||||
this.project = project;
|
this.project = project;
|
||||||
this.data = data;
|
this.data = data;
|
||||||
@ -637,6 +638,25 @@ export class ProjectGroupUpdateRoleEvent extends BaseEvent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class ProjectAccessAddedEvent extends BaseEvent {
|
||||||
|
readonly project: string;
|
||||||
|
|
||||||
|
readonly data: any;
|
||||||
|
|
||||||
|
readonly preData: any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization
|
||||||
|
*/
|
||||||
|
constructor(p: { project: string; createdBy: string | IUser; data: any }) {
|
||||||
|
super(PROJECT_ACCESS_ADDED, p.createdBy);
|
||||||
|
const { project, data } = p;
|
||||||
|
this.project = project;
|
||||||
|
this.data = data;
|
||||||
|
this.preData = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class SettingCreatedEvent extends BaseEvent {
|
export class SettingCreatedEvent extends BaseEvent {
|
||||||
readonly data: any;
|
readonly data: any;
|
||||||
|
|
||||||
|
@ -7,3 +7,26 @@ export function anonymise(s: string): string {
|
|||||||
.slice(0, 9);
|
.slice(0, 9);
|
||||||
return `${hash}@unleash.run`;
|
return `${hash}@unleash.run`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function anonymiseKeys<T>(object: T, keys: string[]): T {
|
||||||
|
if (typeof object !== 'object' || object === null) {
|
||||||
|
return object;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(object)) {
|
||||||
|
return object.map((item) => anonymiseKeys(item, keys)) as unknown as T;
|
||||||
|
} else {
|
||||||
|
return Object.keys(object).reduce((result, key) => {
|
||||||
|
if (
|
||||||
|
keys.includes(key) &&
|
||||||
|
result[key] !== undefined &&
|
||||||
|
result[key] !== null
|
||||||
|
) {
|
||||||
|
result[key] = anonymise(result[key]);
|
||||||
|
} else if (typeof result[key] === 'object') {
|
||||||
|
result[key] = anonymiseKeys(result[key], keys);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}, object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user