From c2a29b49b8f5452e4457792b34b958de841744e3 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Fri, 21 Jun 2024 12:51:37 +0200 Subject: [PATCH] fix: turn off showing usernames and emails in the project cards when the flag is turned on (#7421) This PR: - adds a flag to anonymize user emails in the new project cards - performs the anonymization using the existing `anonymise` function that we have. It does not anonymize the system user, nor does it anonymize groups. It does, however, leave the gravatar url unchanged, as that is already hashed (but we may want to hide that too). This PR also does not affect the user's name or username. Considering the target is the demo instance where the vast majority of users don't have this (and if they do, they've chosen to set it themselves), this seems an appropriate mitigation. With the flag turned off: ![image](https://github.com/Unleash/unleash/assets/17786332/10a84562-c025-4e5c-b642-f949595b4e7e) With the flag on: ![image](https://github.com/Unleash/unleash/assets/17786332/6fc35203-e2fa-4208-9650-0a87d3898996) --- .../__snapshots__/create-config.test.ts.snap | 1 + .../project/project-owners-read-model.test.ts | 21 +++++++++++++++++++ .../project/project-owners-read-model.ts | 21 ++++++++++++++----- .../project/project-owners-read-model.type.ts | 1 + src/lib/features/project/project-service.ts | 8 ++++++- src/lib/types/experimental.ts | 7 ++++++- 6 files changed, 52 insertions(+), 7 deletions(-) diff --git a/src/lib/__snapshots__/create-config.test.ts.snap b/src/lib/__snapshots__/create-config.test.ts.snap index 9a4ee477ac..177ab87032 100644 --- a/src/lib/__snapshots__/create-config.test.ts.snap +++ b/src/lib/__snapshots__/create-config.test.ts.snap @@ -77,6 +77,7 @@ exports[`should create default config 1`] = ` "experiments": { "adminTokenKillSwitch": false, "anonymiseEventLog": false, + "anonymizeProjectOwners": false, "automatedActions": false, "caseInsensitiveInOperators": false, "celebrateUnleash": false, diff --git a/src/lib/features/project/project-owners-read-model.test.ts b/src/lib/features/project/project-owners-read-model.test.ts index 81503e1fdd..d4ec6ce752 100644 --- a/src/lib/features/project/project-owners-read-model.test.ts +++ b/src/lib/features/project/project-owners-read-model.test.ts @@ -360,4 +360,25 @@ describe('integration tests', () => { { name: projectIdB, owners: [{ ownerType: 'user' }] }, ]); }); + + test('anonymizes emails when asked to', async () => { + const projectId = randomId(); + await db.stores.projectStore.create({ id: projectId, name: projectId }); + + await db.stores.accessStore.addUserToRole( + owner.id, + ownerRoleId, + projectId, + ); + + const owners = await readModel.getAllProjectOwners(true); + expect(owners).toMatchObject({ + [projectId]: [ + { + name: 'Owner Name', + email: expect.stringMatching(/@unleash.run$/), + }, + ], + }); + }); }); diff --git a/src/lib/features/project/project-owners-read-model.ts b/src/lib/features/project/project-owners-read-model.ts index 8937b42af9..0c983f9982 100644 --- a/src/lib/features/project/project-owners-read-model.ts +++ b/src/lib/features/project/project-owners-read-model.ts @@ -1,6 +1,6 @@ import type { Db } from '../../db/db'; import { RoleName, type IProjectWithCount } from '../../types'; -import { generateImageUrl } from '../../util'; +import { anonymise, generateImageUrl } from '../../util'; import type { GroupProjectOwner, IProjectOwnersReadModel, @@ -35,6 +35,7 @@ export class ProjectOwnersReadModel implements IProjectOwnersReadModel { private async getAllProjectUsersByRole( roleId: number, + anonymizeProjectOwners: boolean = false, ): Promise> { const usersResult = await this.db .select( @@ -53,13 +54,17 @@ export class ProjectOwnersReadModel implements IProjectOwnersReadModel { .join(`${T.USERS} as user`, 'ru.user_id', 'user.id'); const usersDict: Record = {}; + const processSensitiveData = anonymizeProjectOwners + ? anonymise + : (x: string) => x; + usersResult.forEach((user) => { const project = user.project as string; const data: UserProjectOwner = { ownerType: 'user', name: user?.name || user?.username, - email: user?.email, + email: processSensitiveData(user?.email), imageUrl: generateImageUrl(user), }; @@ -104,11 +109,16 @@ export class ProjectOwnersReadModel implements IProjectOwnersReadModel { return groupsDict; } - async getAllProjectOwners(): Promise { + async getAllProjectOwners( + anonymizeProjectOwners: boolean = false, + ): Promise { const ownerRole = await this.db(T.ROLES) .where({ name: RoleName.OWNER }) .first(); - const usersDict = await this.getAllProjectUsersByRole(ownerRole.id); + const usersDict = await this.getAllProjectUsersByRole( + ownerRole.id, + anonymizeProjectOwners, + ); const groupsDict = await this.getAllProjectGroupsByRole(ownerRole.id); const dict: Record< @@ -129,8 +139,9 @@ export class ProjectOwnersReadModel implements IProjectOwnersReadModel { async addOwners( projects: IProjectWithCount[], + anonymizeProjectOwners: boolean = false, ): Promise { - const owners = await this.getAllProjectOwners(); + const owners = await this.getAllProjectOwners(anonymizeProjectOwners); return ProjectOwnersReadModel.addOwnerData(projects, owners); } diff --git a/src/lib/features/project/project-owners-read-model.type.ts b/src/lib/features/project/project-owners-read-model.type.ts index 0f1a71929f..c01436fda2 100644 --- a/src/lib/features/project/project-owners-read-model.type.ts +++ b/src/lib/features/project/project-owners-read-model.type.ts @@ -24,5 +24,6 @@ export type IProjectWithCountAndOwners = IProjectWithCount & { export interface IProjectOwnersReadModel { addOwners( projects: IProjectWithCount[], + anonymizeProjectOwners?: boolean, ): Promise; } diff --git a/src/lib/features/project/project-service.ts b/src/lib/features/project/project-service.ts index cca524d571..d1233afc73 100644 --- a/src/lib/features/project/project-service.ts +++ b/src/lib/features/project/project-service.ts @@ -233,7 +233,13 @@ export default class ProjectService { async addOwnersToProjects( projects: IProjectWithCount[], ): Promise { - return this.projectOwnersReadModel.addOwners(projects); + const anonymizeProjectOwners = this.flagResolver.isEnabled( + 'anonymizeProjectOwners', + ); + return this.projectOwnersReadModel.addOwners( + projects, + anonymizeProjectOwners, + ); } async getProject(id: string): Promise { diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts index d5a2a97117..bd577be748 100644 --- a/src/lib/types/experimental.ts +++ b/src/lib/types/experimental.ts @@ -62,7 +62,8 @@ export type IFlagKey = | 'enableLegacyVariants' | 'navigationSidebar' | 'commandBarUI' - | 'flagCreator'; + | 'flagCreator' + | 'anonymizeProjectOwners'; export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>; @@ -299,6 +300,10 @@ const flags: IFlags = { process.env.UNLEASH_EXPERIMENTAL_FLAG_CREATOR, false, ), + anonymizeProjectOwners: parseEnvVarBoolean( + process.env.UNLEASH_EXPERIMENTAL_ANONYMIZE_PROJECT_OWNERS, + false, + ), }; export const defaultExperimentalOptions: IExperimentalOptions = {