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 { IEvent, IEventList } from '../../types/events';
|
||||
import Controller from '../controller';
|
||||
import { anonymise } from '../../util/anonymise';
|
||||
import { anonymiseKeys } from '../../util/anonymise';
|
||||
import { OpenApiService } from '../../services/openapi-service';
|
||||
import { createResponseSchema } from '../../openapi/util/create-response-schema';
|
||||
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 { IFlagResolver } from '../../types/experimental';
|
||||
|
||||
const ANON_KEYS = ['email', 'username', 'createdBy'];
|
||||
const version = 1;
|
||||
export default class EventController extends Controller {
|
||||
private eventService: EventService;
|
||||
@ -108,24 +109,7 @@ export default class EventController extends Controller {
|
||||
|
||||
maybeAnonymiseEvents(events: IEvent[]): IEvent[] {
|
||||
if (this.flagResolver.isEnabled('anonymiseEventLog')) {
|
||||
return events.map((e: IEvent) => ({
|
||||
...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 anonymiseKeys(events, ANON_KEYS);
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import createStores from '../../../test/fixtures/store';
|
||||
import getApp from '../../app';
|
||||
import {
|
||||
FeatureCreatedEvent,
|
||||
ProjectAccessAddedEvent,
|
||||
ProjectUserAddedEvent,
|
||||
ProjectUserRemovedEvent,
|
||||
} 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[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,
|
||||
RoleName,
|
||||
IFlagResolver,
|
||||
ProjectAccessAddedEvent,
|
||||
} from '../types';
|
||||
import {
|
||||
IProjectQuery,
|
||||
@ -348,6 +349,7 @@ export default class ProjectService {
|
||||
};
|
||||
}
|
||||
|
||||
// Deprecated: See addAccess instead.
|
||||
async addUser(
|
||||
projectId: string,
|
||||
roleId: number,
|
||||
@ -492,13 +494,25 @@ export default class ProjectService {
|
||||
usersAndGroups: IProjectAccessModel,
|
||||
createdBy: string,
|
||||
): Promise<void> {
|
||||
return this.accessService.addAccessToProject(
|
||||
await this.accessService.addAccessToProject(
|
||||
usersAndGroups.users,
|
||||
usersAndGroups.groups,
|
||||
projectId,
|
||||
roleId,
|
||||
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(
|
||||
|
@ -40,6 +40,7 @@ export const DROP_STRATEGIES = 'drop-strategies';
|
||||
export const CONTEXT_FIELD_CREATED = 'context-field-created';
|
||||
export const CONTEXT_FIELD_UPDATED = 'context-field-updated';
|
||||
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_UPDATED = 'project-updated';
|
||||
export const PROJECT_DELETED = 'project-deleted';
|
||||
@ -563,7 +564,7 @@ export class ProjectUserUpdateRoleEvent extends BaseEvent {
|
||||
data: any;
|
||||
preData: any;
|
||||
}) {
|
||||
super(PROJECT_USER_REMOVED, eventData.createdBy);
|
||||
super(PROJECT_USER_ROLE_CHANGED, eventData.createdBy);
|
||||
const { project, data, preData } = eventData;
|
||||
this.project = project;
|
||||
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 {
|
||||
readonly data: any;
|
||||
|
||||
|
@ -7,3 +7,26 @@ export function anonymise(s: string): string {
|
||||
.slice(0, 9);
|
||||
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