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 = {