Skip to content

Commit

Permalink
feat: expose project members (#3310)
Browse files Browse the repository at this point in the history
  • Loading branch information
kwasniew committed Mar 14, 2023
1 parent b32197b commit 7753082
Show file tree
Hide file tree
Showing 7 changed files with 95 additions and 22 deletions.
3 changes: 2 additions & 1 deletion src/lib/services/access-service.test.ts
Expand Up @@ -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();
Expand All @@ -18,7 +19,7 @@ function getSetup(withNameInUse: boolean) {
{
getLogger,
},
undefined, // GroupService
{} as GroupService,
),
stores,
};
Expand Down
38 changes: 33 additions & 5 deletions src/lib/services/access-service.ts
Expand Up @@ -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;

Expand Down Expand Up @@ -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!,
};
});
}
Expand All @@ -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,
Expand Down Expand Up @@ -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 {
Expand All @@ -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);
}
Expand Down
2 changes: 1 addition & 1 deletion src/lib/types/stores/role-store.ts
Expand Up @@ -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 changes: 19 additions & 0 deletions src/lib/util/unique.test.ts
@@ -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' },
]);
});
5 changes: 5 additions & 0 deletions src/lib/util/unique.ts
@@ -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()];
48 changes: 34 additions & 14 deletions src/test/e2e/services/access-service.e2e.test.ts
Expand Up @@ -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';
Expand All @@ -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;
Expand All @@ -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';
Expand Down Expand Up @@ -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'));
});
Expand All @@ -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(
Expand Down Expand Up @@ -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',
},
Expand Down Expand Up @@ -886,23 +904,25 @@ test('Should be allowed move feature toggle to project when given access through
});

await groupStore.addUsersToGroup(
groupWithProjectAccess.id,
groupWithProjectAccess.id!,
[{ user: viewerUser }],
'Admin',
);

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 () => {
Expand All @@ -923,15 +943,15 @@ test('Should not lose user role access when given permissions from a group', asy
});

await groupStore.addUsersToGroup(
groupWithNoAccess.id,
groupWithNoAccess.id!,
[{ user: user }],
'Admin',
);

const viewerRole = await accessService.getRoleByName(RoleName.VIEWER);

await accessService.addGroupToRole(
groupWithNoAccess.id,
groupWithNoAccess.id!,
viewerRole.id,
'SomeAdminUser',
project.id,
Expand Down Expand Up @@ -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',
);
Expand All @@ -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',
},
Expand All @@ -1004,22 +1024,22 @@ 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',
},
],
});

await accessService.addGroupToRole(
groupWithCreateAccess.id,
groupWithCreateAccess.id!,
deleteFeatureRole.id,
'SomeAdminUser',
projectForDelete.id,
);

await accessService.addGroupToRole(
groupWithDeleteAccess.id,
groupWithDeleteAccess.id!,
createFeatureRole.id,
'SomeAdminUser',
projectForCreate.id,
Expand Down
2 changes: 1 addition & 1 deletion src/test/fixtures/fake-role-store.ts
Expand Up @@ -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.');
}

Expand Down

0 comments on commit 7753082

Please sign in to comment.