1
0
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`;


![image](https://user-images.githubusercontent.com/14320932/236216227-bf6e5dff-f509-48a4-ba7c-9f37e23e87c0.png)
This commit is contained in:
Nuno Góis 2023-05-09 10:13:38 +01:00 committed by GitHub
parent 871c9da83c
commit b013d4286c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 93 additions and 21 deletions

View File

@ -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;
} }

View File

@ -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,
);
});

View File

@ -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(

View File

@ -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;

View File

@ -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);
}
}