1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-10-18 20:09:08 +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 createStores from '../../test/fixtures/store';
import { AccessService, IRoleValidation } from './access-service';
import { GroupService } from './group-service';
function getSetup(withNameInUse: boolean) {
const stores = createStores();
@ -18,7 +19,7 @@ function getSetup(withNameInUse: boolean) {
{
getLogger,
},
undefined, // GroupService
{} as GroupService,
),
stores,
};

View File

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

View File

@ -27,6 +27,6 @@ export interface IRoleStore extends Store<ICustomRole, number> {
getProjectRoles(): Promise<IRole[]>;
getRootRoles(): Promise<IRole[]>;
getRootRoleForAllUsers(): Promise<IUserRole[]>;
nameInUse(name: string, existingId: number): Promise<boolean>;
nameInUse(name: string, existingId?: number): Promise<boolean>;
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[] =>
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 { 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 ProjectService from '../../../lib/services/project-service';
import { createTestConfig } from '../../config/test-config';
@ -18,7 +18,7 @@ import { FavoritesService } from '../../../lib/services';
let db: ITestDb;
let stores: IUnleashStores;
let accessService;
let accessService: AccessService;
let groupService;
let featureToggleService;
let favoritesService;
@ -43,6 +43,16 @@ const createUserViewerAccess = async (name, email) => {
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 defaultEnv = 'default';
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 () => {
await expect(async () => {
// @ts-ignore
await accessService.createDefaultProjectRoles(editorUser);
}).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
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.
expect(
@ -701,14 +719,14 @@ test('Should be denied access to delete a role that is in use', async () => {
{
id: 2,
name: 'CREATE_FEATURE',
environment: null,
environment: undefined,
displayName: 'Create Feature Toggles',
type: 'project',
},
{
id: 8,
name: 'DELETE_FEATURE',
environment: null,
environment: undefined,
displayName: 'Delete Feature Toggles',
type: 'project',
},
@ -886,7 +904,7 @@ test('Should be allowed move feature toggle to project when given access through
});
await groupStore.addUsersToGroup(
groupWithProjectAccess.id,
groupWithProjectAccess.id!,
[{ user: viewerUser }],
'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);
await hasCommonProjectAccess(viewerUser, project.id, false);
await isProjectMember(viewerUser, project.id, false);
await accessService.addGroupToRole(
groupWithProjectAccess.id,
groupWithProjectAccess.id!,
projectRole.id,
'SomeAdminUser',
project.id,
);
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 () => {
@ -923,7 +943,7 @@ test('Should not lose user role access when given permissions from a group', asy
});
await groupStore.addUsersToGroup(
groupWithNoAccess.id,
groupWithNoAccess.id!,
[{ user: user }],
'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);
await accessService.addGroupToRole(
groupWithNoAccess.id,
groupWithNoAccess.id!,
viewerRole.id,
'SomeAdminUser',
project.id,
@ -972,13 +992,13 @@ test('Should allow user to take multiple group roles and have expected permissio
});
await groupStore.addUsersToGroup(
groupWithCreateAccess.id,
groupWithCreateAccess.id!,
[{ user: viewerUser }],
'Admin',
);
await groupStore.addUsersToGroup(
groupWithDeleteAccess.id,
groupWithDeleteAccess.id!,
[{ user: viewerUser }],
'Admin',
);
@ -990,7 +1010,7 @@ test('Should allow user to take multiple group roles and have expected permissio
{
id: 2,
name: 'CREATE_FEATURE',
environment: null,
environment: undefined,
displayName: 'Create Feature Toggles',
type: 'project',
},
@ -1004,7 +1024,7 @@ test('Should allow user to take multiple group roles and have expected permissio
{
id: 8,
name: 'DELETE_FEATURE',
environment: null,
environment: undefined,
displayName: 'Delete Feature Toggles',
type: 'project',
},
@ -1012,14 +1032,14 @@ test('Should allow user to take multiple group roles and have expected permissio
});
await accessService.addGroupToRole(
groupWithCreateAccess.id,
groupWithCreateAccess.id!,
deleteFeatureRole.id,
'SomeAdminUser',
projectForDelete.id,
);
await accessService.addGroupToRole(
groupWithDeleteAccess.id,
groupWithDeleteAccess.id!,
createFeatureRole.id,
'SomeAdminUser',
projectForCreate.id,

View File

@ -18,7 +18,7 @@ export default class FakeRoleStore implements IRoleStore {
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.');
}