Skip to content

Commit

Permalink
fix: add method to change role for project memeber
Browse files Browse the repository at this point in the history
  • Loading branch information
ivarconr committed Mar 3, 2022
1 parent d566116 commit 8364c3b
Show file tree
Hide file tree
Showing 7 changed files with 180 additions and 7 deletions.
13 changes: 13 additions & 0 deletions src/lib/db/access-store.ts
Expand Up @@ -247,6 +247,19 @@ export class AccessStore implements IAccessStore {
.delete();
}

async updateUserProjectRole(
userId: number,
roleId: number,
projectId: string,
): Promise<void> {
return this.db(T.ROLE_USER)
.where({
user_id: userId,
project: projectId,
})
.update('role_id', roleId);
}

async removeRolesOfTypeForUser(
userId: number,
roleType: string,
Expand Down
8 changes: 8 additions & 0 deletions src/lib/services/access-service.ts
Expand Up @@ -220,6 +220,14 @@ export class AccessService {
return this.store.removeUserFromRole(userId, roleId, projectId);
}

async updateUserProjectRole(
userId: number,
roleId: number,
projectId: string,
): Promise<void> {
return this.store.updateUserProjectRole(userId, roleId, projectId);
}

//This actually only exists for testing purposes
async addPermissionToRole(
roleId: number,
Expand Down
69 changes: 62 additions & 7 deletions src/lib/services/project-service.ts
Expand Up @@ -8,6 +8,7 @@ import NotFoundError from '../error/notfound-error';
import {
ProjectUserAddedEvent,
ProjectUserRemovedEvent,
ProjectUserUpdateRoleEvent,
PROJECT_CREATED,
PROJECT_DELETED,
PROJECT_UPDATED,
Expand Down Expand Up @@ -309,31 +310,87 @@ export default class ProjectService {
userId: number,
createdBy?: string,
): Promise<void> {
const role = await this.findProjectRole(projectId, roleId);

await this.validateAtLeastOneOwner(projectId, role);

await this.accessService.removeUserFromRole(userId, role.id, projectId);

await this.eventStore.store(
new ProjectUserRemovedEvent({
project: projectId,
createdBy,
preData: { roleId, userId, roleName: role.name },
}),
);
}

async findProjectRole(
projectId: string,
roleId: number,
): Promise<IRoleDescriptor> {
const roles = await this.accessService.getRolesForProject(projectId);
const role = roles.find((r) => r.id === roleId);
if (!role) {
throw new NotFoundError(
`Couldn't find roleId=${roleId} on project=${projectId}`,
);
}
return role;
}

if (role.name === RoleName.OWNER) {
async validateAtLeastOneOwner(
projectId: string,
currentRole: IRoleDescriptor,
): Promise<void> {
if (currentRole.name === RoleName.OWNER) {
const users = await this.accessService.getProjectUsersForRole(
role.id,
currentRole.id,
projectId,
);
if (users.length < 2) {
throw new Error('A project must have at least one owner');
}
}
}

await this.accessService.removeUserFromRole(userId, role.id, projectId);
async changeRole(
projectId: string,
roleId: number,
userId: number,
createdBy: string,
): Promise<void> {
const role = await this.findProjectRole(projectId, roleId);

const usersWithRoles = await this.getUsersWithAccess(projectId);
const user = usersWithRoles.users.find((u) => u.id === userId);
const currentRole = usersWithRoles.roles.find(
(r) => r.id === user.roleId,
);

if (currentRole.id === roleId) {
// Nothing to do....
return;
}

await this.validateAtLeastOneOwner(projectId, currentRole);

await this.accessService.updateUserProjectRole(
userId,
roleId,
projectId,
);

await this.eventStore.store(
new ProjectUserRemovedEvent({
new ProjectUserUpdateRoleEvent({
project: projectId,
createdBy,
preData: { roleId, userId, roleName: role.name },
preData: {
userId,
roleId: currentRole.id,
roleName: currentRole.name,
},
data: { userId, roleId, roleName: role.name },
}),
);
}
Expand Down Expand Up @@ -366,5 +423,3 @@ export default class ProjectService {
};
}
}

module.exports = ProjectService;
22 changes: 22 additions & 0 deletions src/lib/types/events.ts
Expand Up @@ -41,6 +41,7 @@ export const PROJECT_DELETED = 'project-deleted';
export const PROJECT_IMPORT = 'project-import';
export const PROJECT_USER_ADDED = 'project-user-added';
export const PROJECT_USER_REMOVED = 'project-user-removed';
export const PROJECT_USER_ROLE_CHANGED = 'project-user-role-changed';
export const DROP_PROJECTS = 'drop-projects';
export const TAG_CREATED = 'tag-created';
export const TAG_DELETED = 'tag-deleted';
Expand Down Expand Up @@ -412,3 +413,24 @@ export class ProjectUserRemovedEvent extends BaseEvent {
this.preData = preData;
}
}

export class ProjectUserUpdateRoleEvent extends BaseEvent {
readonly project: string;

readonly data: any;

readonly preData: any;

constructor(p: {
project: string;
createdBy: string;
data: any;
preData: any;
}) {
super(PROJECT_USER_REMOVED, p.createdBy);
const { project, data, preData } = p;
this.project = project;
this.data = data;
this.preData = preData;
}
}
6 changes: 6 additions & 0 deletions src/lib/types/stores/access-store.ts
Expand Up @@ -19,6 +19,7 @@ export interface IRoleWithPermissions extends IRole {
}

export interface IRoleDescriptor {
id: number;
name: string;
description?: string;
type: string;
Expand Down Expand Up @@ -51,6 +52,11 @@ export interface IAccessStore extends Store<IRole, number> {
roleId: number,
projectId?: string,
): Promise<void>;
updateUserProjectRole(
userId: number,
roleId: number,
projectId: string,
): Promise<void>;
removeRolesOfTypeForUser(userId: number, roleType: string): Promise<void>;
addPermissionsToRole(
role_id: number,
Expand Down
61 changes: 61 additions & 0 deletions src/test/e2e/services/project-service.e2e.test.ts
Expand Up @@ -652,3 +652,64 @@ test('should change a users role in the project', async () => {
expect(customUser[0].id).toBe(projectUser.id);
expect(customUser[0].name).toBe(projectUser.name);
});

test('should update role for user on project', async () => {
const project = {
id: 'update-users',
name: 'New project',
description: 'Blah',
};
await projectService.createProject(project, user);

const projectMember1 = await stores.userStore.insert({
name: 'Some Member',
email: 'update99@getunleash.io',
});

const memberRole = await stores.roleStore.getRoleByName(RoleName.MEMBER);
const ownerRole = await stores.roleStore.getRoleByName(RoleName.OWNER);

await projectService.addUser(project.id, memberRole.id, projectMember1.id);
await projectService.changeRole(
project.id,
ownerRole.id,
projectMember1.id,
'test',
);

const { users } = await projectService.getUsersWithAccess(project.id, user);
const memberUsers = users.filter((u) => u.roleId === memberRole.id);
const ownerUsers = users.filter((u) => u.roleId === ownerRole.id);

expect(memberUsers).toHaveLength(0);
expect(ownerUsers).toHaveLength(2);
});

test('should not update role for user on project when she is the owner', async () => {
const project = {
id: 'update-users-not-allowed',
name: 'New project',
description: 'Blah',
};
await projectService.createProject(project, user);

const projectMember1 = await stores.userStore.insert({
name: 'Some Member',
email: 'update991@getunleash.io',
});

const memberRole = await stores.roleStore.getRoleByName(RoleName.MEMBER);

await projectService.addUser(project.id, memberRole.id, projectMember1.id);

await expect(async () => {
await projectService.changeRole(
project.id,
memberRole.id,
user.id,
'test',
);
}).rejects.toThrowError(
new Error('A project must have at least one owner'),
);
});
8 changes: 8 additions & 0 deletions src/test/fixtures/fake-access-store.ts
Expand Up @@ -9,6 +9,14 @@ import {
import { IAvailablePermissions, IPermission } from 'lib/types/model';

class AccessStoreMock implements IAccessStore {
updateUserProjectRole(
userId: number,
roleId: number,
projectId: string,
): Promise<void> {
throw new Error('Method not implemented.');
}

removeUserFromRole(
userId: number,
roleId: number,
Expand Down

0 comments on commit 8364c3b

Please sign in to comment.