1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-06-04 01:18:20 +02:00

feat: expose project members (#3310)

This commit is contained in:
Mateusz Kwasniewski 2023-03-14 16:27:57 +01:00 committed by GitHub
parent b32197b0b5
commit 7753082660
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 95 additions and 22 deletions

View File

@ -2,6 +2,7 @@ import NameExistsError from '../error/name-exists-error';
import getLogger from '../../test/fixtures/no-logger'; import getLogger from '../../test/fixtures/no-logger';
import createStores from '../../test/fixtures/store'; import createStores from '../../test/fixtures/store';
import { AccessService, IRoleValidation } from './access-service'; import { AccessService, IRoleValidation } from './access-service';
import { GroupService } from './group-service';
function getSetup(withNameInUse: boolean) { function getSetup(withNameInUse: boolean) {
const stores = createStores(); const stores = createStores();
@ -18,7 +19,7 @@ function getSetup(withNameInUse: boolean) {
{ {
getLogger, getLogger,
}, },
undefined, // GroupService {} as GroupService,
), ),
stores, stores,
}; };

View File

@ -31,6 +31,7 @@ import InvalidOperationError from '../error/invalid-operation-error';
import BadDataError from '../error/bad-data-error'; import BadDataError from '../error/bad-data-error';
import { IGroupModelWithProjectRole } from '../types/group'; import { IGroupModelWithProjectRole } from '../types/group';
import { GroupService } from './group-service'; import { GroupService } from './group-service';
import { uniqueByKey } from '../util/unique';
const { ADMIN } = permissions; const { ADMIN } = permissions;
@ -381,10 +382,10 @@ export class AccessService {
const userIdList = userRoleList.map((u) => u.userId); const userIdList = userRoleList.map((u) => u.userId);
const users = await this.accountStore.getAllWithId(userIdList); const users = await this.accountStore.getAllWithId(userIdList);
return users.map((user) => { return users.map((user) => {
const role = userRoleList.find((r) => r.userId == user.id); const role = userRoleList.find((r) => r.userId == user.id)!;
return { return {
...user, ...user,
addedAt: role.addedAt, addedAt: role.addedAt!,
}; };
}); });
} }
@ -409,6 +410,31 @@ export class AccessService {
return [roles, users.flat(), groups]; return [roles, users.flat(), groups];
} }
async getProjectMembers(
projectId: string,
): Promise<Array<Pick<IUser, 'id' | 'email' | 'username'>>> {
const [, users, groups] = await this.getProjectRoleAccess(projectId);
const actualUsers = users.map((user) => ({
id: user.id,
email: user.email,
username: user.username,
}));
const actualGroupUsers = groups
.flatMap((group) => group.users)
.map((user) => user.user)
.map((user) => ({
id: user.id,
email: user.email,
username: user.username,
}));
return uniqueByKey([...actualUsers, ...actualGroupUsers], 'id');
}
async isProjectMember(userId: number, projectId: string): Promise<boolean> {
const users = await this.getProjectMembers(projectId);
return Boolean(users.find((user) => user.id === userId));
}
async createDefaultProjectRoles( async createDefaultProjectRoles(
owner: IUser, owner: IUser,
projectId: string, projectId: string,
@ -444,9 +470,11 @@ export class AccessService {
return this.roleStore.getRootRoles(); return this.roleStore.getRootRoles();
} }
public async resolveRootRole(rootRole: number | RoleName): Promise<IRole> { public async resolveRootRole(
rootRole: number | RoleName,
): Promise<IRole | undefined> {
const rootRoles = await this.getRootRoles(); const rootRoles = await this.getRootRoles();
let role: IRole; let role: IRole | undefined;
if (typeof rootRole === 'number') { if (typeof rootRole === 'number') {
role = rootRoles.find((r) => r.id === rootRole); role = rootRoles.find((r) => r.id === rootRole);
} else { } else {
@ -455,7 +483,7 @@ export class AccessService {
return role; return role;
} }
async getRootRole(roleName: RoleName): Promise<IRole> { async getRootRole(roleName: RoleName): Promise<IRole | undefined> {
const roles = await this.roleStore.getRootRoles(); const roles = await this.roleStore.getRootRoles();
return roles.find((r) => r.name === roleName); return roles.find((r) => r.name === roleName);
} }

View File

@ -27,6 +27,6 @@ export interface IRoleStore extends Store<ICustomRole, number> {
getProjectRoles(): Promise<IRole[]>; getProjectRoles(): Promise<IRole[]>;
getRootRoles(): Promise<IRole[]>; getRootRoles(): Promise<IRole[]>;
getRootRoleForAllUsers(): Promise<IUserRole[]>; getRootRoleForAllUsers(): Promise<IUserRole[]>;
nameInUse(name: string, existingId: number): Promise<boolean>; nameInUse(name: string, existingId?: number): Promise<boolean>;
count(): Promise<number>; count(): Promise<number>;
} }

View File

@ -0,0 +1,19 @@
import { uniqueByKey } from './unique';
test('should filter unique objects by key', () => {
expect(
uniqueByKey(
[
{ name: 'name1', value: 'val1' },
{ name: 'name1', value: 'val1' },
{ name: 'name1', value: 'val2' },
{ name: 'name1', value: 'val4' },
{ name: 'name2', value: 'val5' },
],
'name',
),
).toStrictEqual([
{ name: 'name1', value: 'val4' },
{ name: 'name2', value: 'val5' },
]);
});

View File

@ -1,2 +1,7 @@
export const unique = <T extends string | number>(items: T[]): T[] => export const unique = <T extends string | number>(items: T[]): T[] =>
Array.from(new Set(items)); Array.from(new Set(items));
export const uniqueByKey = <T extends Record<string, unknown>>(
items: T[],
key: keyof T,
): T[] => [...new Map(items.map((item) => [item[key], item])).values()];

View File

@ -6,7 +6,7 @@ import { AccessService } from '../../../lib/services/access-service';
import * as permissions from '../../../lib/types/permissions'; import * as permissions from '../../../lib/types/permissions';
import { RoleName } from '../../../lib/types/model'; import { RoleName } from '../../../lib/types/model';
import { IUnleashStores } from '../../../lib/types'; import { IUnleashStores, IUser } from '../../../lib/types';
import FeatureToggleService from '../../../lib/services/feature-toggle-service'; import FeatureToggleService from '../../../lib/services/feature-toggle-service';
import ProjectService from '../../../lib/services/project-service'; import ProjectService from '../../../lib/services/project-service';
import { createTestConfig } from '../../config/test-config'; import { createTestConfig } from '../../config/test-config';
@ -18,7 +18,7 @@ import { FavoritesService } from '../../../lib/services';
let db: ITestDb; let db: ITestDb;
let stores: IUnleashStores; let stores: IUnleashStores;
let accessService; let accessService: AccessService;
let groupService; let groupService;
let featureToggleService; let featureToggleService;
let favoritesService; let favoritesService;
@ -43,6 +43,16 @@ const createUserViewerAccess = async (name, email) => {
return user; return user;
}; };
const isProjectMember = async (
user: Pick<IUser, 'id' | 'permissions' | 'isAPI'>,
projectName: string,
condition: boolean,
) => {
expect(await accessService.isProjectMember(user.id, projectName)).toBe(
condition,
);
};
const hasCommonProjectAccess = async (user, projectName, condition) => { const hasCommonProjectAccess = async (user, projectName, condition) => {
const defaultEnv = 'default'; const defaultEnv = 'default';
const developmentEnv = 'development'; const developmentEnv = 'development';
@ -385,6 +395,7 @@ test('should create default roles to project', async () => {
test('should require name when create default roles to project', async () => { test('should require name when create default roles to project', async () => {
await expect(async () => { await expect(async () => {
// @ts-ignore
await accessService.createDefaultProjectRoles(editorUser); await accessService.createDefaultProjectRoles(editorUser);
}).rejects.toThrow(new Error('ProjectId cannot be empty')); }).rejects.toThrow(new Error('ProjectId cannot be empty'));
}); });
@ -404,6 +415,13 @@ test('should grant user access to project', async () => {
// // Should be able to update feature toggles inside the project // // Should be able to update feature toggles inside the project
await hasCommonProjectAccess(sUser, project, true); await hasCommonProjectAccess(sUser, project, true);
await isProjectMember(sUser, project, true);
await isProjectMember(user, project, true);
// should list project members
expect(await accessService.getProjectMembers(project)).toStrictEqual([
{ email: user.email, id: user.id, username: user.username },
{ email: sUser.email, id: sUser.id, username: sUser.username },
]);
// Should not be able to admin the project itself. // Should not be able to admin the project itself.
expect( expect(
@ -701,14 +719,14 @@ test('Should be denied access to delete a role that is in use', async () => {
{ {
id: 2, id: 2,
name: 'CREATE_FEATURE', name: 'CREATE_FEATURE',
environment: null, environment: undefined,
displayName: 'Create Feature Toggles', displayName: 'Create Feature Toggles',
type: 'project', type: 'project',
}, },
{ {
id: 8, id: 8,
name: 'DELETE_FEATURE', name: 'DELETE_FEATURE',
environment: null, environment: undefined,
displayName: 'Delete Feature Toggles', displayName: 'Delete Feature Toggles',
type: 'project', type: 'project',
}, },
@ -886,7 +904,7 @@ test('Should be allowed move feature toggle to project when given access through
}); });
await groupStore.addUsersToGroup( await groupStore.addUsersToGroup(
groupWithProjectAccess.id, groupWithProjectAccess.id!,
[{ user: viewerUser }], [{ user: viewerUser }],
'Admin', 'Admin',
); );
@ -894,15 +912,17 @@ test('Should be allowed move feature toggle to project when given access through
const projectRole = await accessService.getRoleByName(RoleName.MEMBER); const projectRole = await accessService.getRoleByName(RoleName.MEMBER);
await hasCommonProjectAccess(viewerUser, project.id, false); await hasCommonProjectAccess(viewerUser, project.id, false);
await isProjectMember(viewerUser, project.id, false);
await accessService.addGroupToRole( await accessService.addGroupToRole(
groupWithProjectAccess.id, groupWithProjectAccess.id!,
projectRole.id, projectRole.id,
'SomeAdminUser', 'SomeAdminUser',
project.id, project.id,
); );
await hasCommonProjectAccess(viewerUser, project.id, true); await hasCommonProjectAccess(viewerUser, project.id, true);
await isProjectMember(viewerUser, project.id, true);
}); });
test('Should not lose user role access when given permissions from a group', async () => { test('Should not lose user role access when given permissions from a group', async () => {
@ -923,7 +943,7 @@ test('Should not lose user role access when given permissions from a group', asy
}); });
await groupStore.addUsersToGroup( await groupStore.addUsersToGroup(
groupWithNoAccess.id, groupWithNoAccess.id!,
[{ user: user }], [{ user: user }],
'Admin', 'Admin',
); );
@ -931,7 +951,7 @@ test('Should not lose user role access when given permissions from a group', asy
const viewerRole = await accessService.getRoleByName(RoleName.VIEWER); const viewerRole = await accessService.getRoleByName(RoleName.VIEWER);
await accessService.addGroupToRole( await accessService.addGroupToRole(
groupWithNoAccess.id, groupWithNoAccess.id!,
viewerRole.id, viewerRole.id,
'SomeAdminUser', 'SomeAdminUser',
project.id, project.id,
@ -972,13 +992,13 @@ test('Should allow user to take multiple group roles and have expected permissio
}); });
await groupStore.addUsersToGroup( await groupStore.addUsersToGroup(
groupWithCreateAccess.id, groupWithCreateAccess.id!,
[{ user: viewerUser }], [{ user: viewerUser }],
'Admin', 'Admin',
); );
await groupStore.addUsersToGroup( await groupStore.addUsersToGroup(
groupWithDeleteAccess.id, groupWithDeleteAccess.id!,
[{ user: viewerUser }], [{ user: viewerUser }],
'Admin', 'Admin',
); );
@ -990,7 +1010,7 @@ test('Should allow user to take multiple group roles and have expected permissio
{ {
id: 2, id: 2,
name: 'CREATE_FEATURE', name: 'CREATE_FEATURE',
environment: null, environment: undefined,
displayName: 'Create Feature Toggles', displayName: 'Create Feature Toggles',
type: 'project', type: 'project',
}, },
@ -1004,7 +1024,7 @@ test('Should allow user to take multiple group roles and have expected permissio
{ {
id: 8, id: 8,
name: 'DELETE_FEATURE', name: 'DELETE_FEATURE',
environment: null, environment: undefined,
displayName: 'Delete Feature Toggles', displayName: 'Delete Feature Toggles',
type: 'project', type: 'project',
}, },
@ -1012,14 +1032,14 @@ test('Should allow user to take multiple group roles and have expected permissio
}); });
await accessService.addGroupToRole( await accessService.addGroupToRole(
groupWithCreateAccess.id, groupWithCreateAccess.id!,
deleteFeatureRole.id, deleteFeatureRole.id,
'SomeAdminUser', 'SomeAdminUser',
projectForDelete.id, projectForDelete.id,
); );
await accessService.addGroupToRole( await accessService.addGroupToRole(
groupWithDeleteAccess.id, groupWithDeleteAccess.id!,
createFeatureRole.id, createFeatureRole.id,
'SomeAdminUser', 'SomeAdminUser',
projectForCreate.id, projectForCreate.id,

View File

@ -18,7 +18,7 @@ export default class FakeRoleStore implements IRoleStore {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }
nameInUse(name: string, existingId: number): Promise<boolean> { nameInUse(name: string, existingId?: number): Promise<boolean> {
throw new Error('Method not implemented.'); throw new Error('Method not implemented.');
} }