1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

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)
This commit is contained in:
Thomas Heartman 2024-06-21 12:51:37 +02:00 committed by GitHub
parent 7d95e8358a
commit c2a29b49b8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 52 additions and 7 deletions

View File

@ -77,6 +77,7 @@ exports[`should create default config 1`] = `
"experiments": {
"adminTokenKillSwitch": false,
"anonymiseEventLog": false,
"anonymizeProjectOwners": false,
"automatedActions": false,
"caseInsensitiveInOperators": false,
"celebrateUnleash": false,

View File

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

View File

@ -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<Record<string, UserProjectOwner[]>> {
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<string, UserProjectOwner[]> = {};
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<ProjectOwnersDictionary> {
async getAllProjectOwners(
anonymizeProjectOwners: boolean = false,
): Promise<ProjectOwnersDictionary> {
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<IProjectWithCountAndOwners[]> {
const owners = await this.getAllProjectOwners();
const owners = await this.getAllProjectOwners(anonymizeProjectOwners);
return ProjectOwnersReadModel.addOwnerData(projects, owners);
}

View File

@ -24,5 +24,6 @@ export type IProjectWithCountAndOwners = IProjectWithCount & {
export interface IProjectOwnersReadModel {
addOwners(
projects: IProjectWithCount[],
anonymizeProjectOwners?: boolean,
): Promise<IProjectWithCountAndOwners[]>;
}

View File

@ -233,7 +233,13 @@ export default class ProjectService {
async addOwnersToProjects(
projects: IProjectWithCount[],
): Promise<IProjectWithCount[]> {
return this.projectOwnersReadModel.addOwners(projects);
const anonymizeProjectOwners = this.flagResolver.isEnabled(
'anonymizeProjectOwners',
);
return this.projectOwnersReadModel.addOwners(
projects,
anonymizeProjectOwners,
);
}
async getProject(id: string): Promise<IProject> {

View File

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