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:
parent
b32197b0b5
commit
7753082660
@ -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,
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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>;
|
||||
}
|
||||
|
19
src/lib/util/unique.test.ts
Normal file
19
src/lib/util/unique.test.ts
Normal 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' },
|
||||
]);
|
||||
});
|
@ -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()];
|
||||
|
@ -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,
|
||||
|
2
src/test/fixtures/fake-role-store.ts
vendored
2
src/test/fixtures/fake-role-store.ts
vendored
@ -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.');
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user