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:
parent
b32197b0b5
commit
7753082660
@ -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,
|
||||||
};
|
};
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
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[] =>
|
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()];
|
||||||
|
@ -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,
|
||||||
|
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.');
|
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.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user