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:
parent
7d95e8358a
commit
c2a29b49b8
@ -77,6 +77,7 @@ exports[`should create default config 1`] = `
|
|||||||
"experiments": {
|
"experiments": {
|
||||||
"adminTokenKillSwitch": false,
|
"adminTokenKillSwitch": false,
|
||||||
"anonymiseEventLog": false,
|
"anonymiseEventLog": false,
|
||||||
|
"anonymizeProjectOwners": false,
|
||||||
"automatedActions": false,
|
"automatedActions": false,
|
||||||
"caseInsensitiveInOperators": false,
|
"caseInsensitiveInOperators": false,
|
||||||
"celebrateUnleash": false,
|
"celebrateUnleash": false,
|
||||||
|
@ -360,4 +360,25 @@ describe('integration tests', () => {
|
|||||||
{ name: projectIdB, owners: [{ ownerType: 'user' }] },
|
{ 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$/),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import type { Db } from '../../db/db';
|
import type { Db } from '../../db/db';
|
||||||
import { RoleName, type IProjectWithCount } from '../../types';
|
import { RoleName, type IProjectWithCount } from '../../types';
|
||||||
import { generateImageUrl } from '../../util';
|
import { anonymise, generateImageUrl } from '../../util';
|
||||||
import type {
|
import type {
|
||||||
GroupProjectOwner,
|
GroupProjectOwner,
|
||||||
IProjectOwnersReadModel,
|
IProjectOwnersReadModel,
|
||||||
@ -35,6 +35,7 @@ export class ProjectOwnersReadModel implements IProjectOwnersReadModel {
|
|||||||
|
|
||||||
private async getAllProjectUsersByRole(
|
private async getAllProjectUsersByRole(
|
||||||
roleId: number,
|
roleId: number,
|
||||||
|
anonymizeProjectOwners: boolean = false,
|
||||||
): Promise<Record<string, UserProjectOwner[]>> {
|
): Promise<Record<string, UserProjectOwner[]>> {
|
||||||
const usersResult = await this.db
|
const usersResult = await this.db
|
||||||
.select(
|
.select(
|
||||||
@ -53,13 +54,17 @@ export class ProjectOwnersReadModel implements IProjectOwnersReadModel {
|
|||||||
.join(`${T.USERS} as user`, 'ru.user_id', 'user.id');
|
.join(`${T.USERS} as user`, 'ru.user_id', 'user.id');
|
||||||
const usersDict: Record<string, UserProjectOwner[]> = {};
|
const usersDict: Record<string, UserProjectOwner[]> = {};
|
||||||
|
|
||||||
|
const processSensitiveData = anonymizeProjectOwners
|
||||||
|
? anonymise
|
||||||
|
: (x: string) => x;
|
||||||
|
|
||||||
usersResult.forEach((user) => {
|
usersResult.forEach((user) => {
|
||||||
const project = user.project as string;
|
const project = user.project as string;
|
||||||
|
|
||||||
const data: UserProjectOwner = {
|
const data: UserProjectOwner = {
|
||||||
ownerType: 'user',
|
ownerType: 'user',
|
||||||
name: user?.name || user?.username,
|
name: user?.name || user?.username,
|
||||||
email: user?.email,
|
email: processSensitiveData(user?.email),
|
||||||
imageUrl: generateImageUrl(user),
|
imageUrl: generateImageUrl(user),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -104,11 +109,16 @@ export class ProjectOwnersReadModel implements IProjectOwnersReadModel {
|
|||||||
return groupsDict;
|
return groupsDict;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllProjectOwners(): Promise<ProjectOwnersDictionary> {
|
async getAllProjectOwners(
|
||||||
|
anonymizeProjectOwners: boolean = false,
|
||||||
|
): Promise<ProjectOwnersDictionary> {
|
||||||
const ownerRole = await this.db(T.ROLES)
|
const ownerRole = await this.db(T.ROLES)
|
||||||
.where({ name: RoleName.OWNER })
|
.where({ name: RoleName.OWNER })
|
||||||
.first();
|
.first();
|
||||||
const usersDict = await this.getAllProjectUsersByRole(ownerRole.id);
|
const usersDict = await this.getAllProjectUsersByRole(
|
||||||
|
ownerRole.id,
|
||||||
|
anonymizeProjectOwners,
|
||||||
|
);
|
||||||
const groupsDict = await this.getAllProjectGroupsByRole(ownerRole.id);
|
const groupsDict = await this.getAllProjectGroupsByRole(ownerRole.id);
|
||||||
|
|
||||||
const dict: Record<
|
const dict: Record<
|
||||||
@ -129,8 +139,9 @@ export class ProjectOwnersReadModel implements IProjectOwnersReadModel {
|
|||||||
|
|
||||||
async addOwners(
|
async addOwners(
|
||||||
projects: IProjectWithCount[],
|
projects: IProjectWithCount[],
|
||||||
|
anonymizeProjectOwners: boolean = false,
|
||||||
): Promise<IProjectWithCountAndOwners[]> {
|
): Promise<IProjectWithCountAndOwners[]> {
|
||||||
const owners = await this.getAllProjectOwners();
|
const owners = await this.getAllProjectOwners(anonymizeProjectOwners);
|
||||||
|
|
||||||
return ProjectOwnersReadModel.addOwnerData(projects, owners);
|
return ProjectOwnersReadModel.addOwnerData(projects, owners);
|
||||||
}
|
}
|
||||||
|
@ -24,5 +24,6 @@ export type IProjectWithCountAndOwners = IProjectWithCount & {
|
|||||||
export interface IProjectOwnersReadModel {
|
export interface IProjectOwnersReadModel {
|
||||||
addOwners(
|
addOwners(
|
||||||
projects: IProjectWithCount[],
|
projects: IProjectWithCount[],
|
||||||
|
anonymizeProjectOwners?: boolean,
|
||||||
): Promise<IProjectWithCountAndOwners[]>;
|
): Promise<IProjectWithCountAndOwners[]>;
|
||||||
}
|
}
|
||||||
|
@ -233,7 +233,13 @@ export default class ProjectService {
|
|||||||
async addOwnersToProjects(
|
async addOwnersToProjects(
|
||||||
projects: IProjectWithCount[],
|
projects: IProjectWithCount[],
|
||||||
): Promise<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> {
|
async getProject(id: string): Promise<IProject> {
|
||||||
|
@ -62,7 +62,8 @@ export type IFlagKey =
|
|||||||
| 'enableLegacyVariants'
|
| 'enableLegacyVariants'
|
||||||
| 'navigationSidebar'
|
| 'navigationSidebar'
|
||||||
| 'commandBarUI'
|
| 'commandBarUI'
|
||||||
| 'flagCreator';
|
| 'flagCreator'
|
||||||
|
| 'anonymizeProjectOwners';
|
||||||
|
|
||||||
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
||||||
|
|
||||||
@ -299,6 +300,10 @@ const flags: IFlags = {
|
|||||||
process.env.UNLEASH_EXPERIMENTAL_FLAG_CREATOR,
|
process.env.UNLEASH_EXPERIMENTAL_FLAG_CREATOR,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
|
anonymizeProjectOwners: parseEnvVarBoolean(
|
||||||
|
process.env.UNLEASH_EXPERIMENTAL_ANONYMIZE_PROJECT_OWNERS,
|
||||||
|
false,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultExperimentalOptions: IExperimentalOptions = {
|
export const defaultExperimentalOptions: IExperimentalOptions = {
|
||||||
|
Loading…
Reference in New Issue
Block a user