diff --git a/src/lib/error/role-in-use-error.ts b/src/lib/error/role-in-use-error.ts new file mode 100644 index 0000000000..1de4c762b1 --- /dev/null +++ b/src/lib/error/role-in-use-error.ts @@ -0,0 +1,24 @@ +class RoleInUseError extends Error { + constructor(message: string) { + super(); + Error.captureStackTrace(this, this.constructor); + + this.name = this.constructor.name; + this.message = message; + } + + toJSON(): object { + return { + isJoi: true, + name: this.constructor.name, + details: [ + { + message: this.message, + }, + ], + }; + } +} + +export default RoleInUseError; +module.exports = RoleInUseError; diff --git a/src/lib/routes/util.ts b/src/lib/routes/util.ts index f8b5051328..df4ea5de37 100644 --- a/src/lib/routes/util.ts +++ b/src/lib/routes/util.ts @@ -58,6 +58,8 @@ export const handleErrors: ( return res.status(409).json(error).end(); case 'FeatureHasTagError': return res.status(409).json(error).end(); + case 'RoleInUseError': + return res.status(400).json(error).end(); default: logger.error('Server failed executing request', error); return res.status(500).end(); diff --git a/src/lib/services/access-service.ts b/src/lib/services/access-service.ts index f7dad3bc0e..4dd7ddbde7 100644 --- a/src/lib/services/access-service.ts +++ b/src/lib/services/access-service.ts @@ -22,6 +22,7 @@ import { import { IRoleStore } from 'lib/types/stores/role-store'; import NameExistsError from '../error/name-exists-error'; import { IEnvironmentStore } from 'lib/types/stores/environment-store'; +import RoleInUseError from '../error/role-in-use-error'; export const ALL_PROJECTS = '*'; export const ALL_ENVS = '*'; @@ -427,6 +428,14 @@ export class AccessService { } async deleteRole(id: number): Promise { + const roleUsers = await this.getUsersForRole(id); + + if (roleUsers.length > 0) { + throw new RoleInUseError( + 'Role is in use by more than one user. You cannot delete a role that is in use without first removing the role from the users.', + ); + } + return this.roleStore.delete(id); } diff --git a/src/test/e2e/services/access-service.e2e.test.ts b/src/test/e2e/services/access-service.e2e.test.ts index 2c1c426e8c..aade13bc5b 100644 --- a/src/test/e2e/services/access-service.e2e.test.ts +++ b/src/test/e2e/services/access-service.e2e.test.ts @@ -10,11 +10,15 @@ import { import * as permissions from '../../../lib/types/permissions'; import { RoleName } from '../../../lib/types/model'; import { IUnleashStores } from '../../../lib/types'; +import FeatureToggleService from '../../../lib/services/feature-toggle-service'; +import ProjectService from '../../../lib/services/project-service'; +import { createTestConfig } from '../../config/test-config'; let db: ITestDb; let stores: IUnleashStores; let accessService; - +let featureToggleService; +let projectService; let editorUser; let superUser; let editorRole; @@ -182,11 +186,23 @@ beforeAll(async () => { db = await dbInit('access_service_serial', getLogger); stores = db.stores; // projectStore = stores.projectStore; + const config = createTestConfig({ + getLogger, + // @ts-ignore + experimental: { environments: { enabled: true } }, + }); accessService = new AccessService(stores, { getLogger }); const roles = await accessService.getRootRoles(); editorRole = roles.find((r) => r.name === RoleName.EDITOR); adminRole = roles.find((r) => r.name === RoleName.ADMIN); readRole = roles.find((r) => r.name === RoleName.VIEWER); + featureToggleService = new FeatureToggleService(stores, config); + projectService = new ProjectService( + stores, + config, + accessService, + featureToggleService, + ); editorUser = await createUserEditorAccess('Bob Test', 'bob@getunleash.io'); superUser = await createSuperUser(); @@ -604,3 +620,50 @@ test('Should be denied access to delete a strategy in an environment the user do ), ).toBe(false); }); + +test('Should be denied access to delete a role that is in use', async () => { + const user = editorUser; + + const project = { + id: 'projectToUseRole', + name: 'New project', + description: 'Blah', + }; + await projectService.createProject(project, user.id); + + const projectMember = await stores.userStore.insert({ + name: 'CustomProjectMember', + email: 'custom@getunleash.io', + }); + + const customRole = await accessService.createRole({ + name: 'RoleInUse', + description: '', + permissions: [ + { + id: 2, + name: 'CREATE_FEATURE', + environment: null, + displayName: 'Create Feature Toggles', + type: 'project', + }, + { + id: 8, + name: 'DELETE_FEATURE', + environment: null, + displayName: 'Delete Feature Toggles', + type: 'project', + }, + ], + }); + + await projectService.addUser(project.id, customRole.id, projectMember.id); + + try { + await accessService.deleteRole(customRole.id); + } catch (e) { + expect(e.toString()).toBe( + 'RoleInUseError: Role is in use by more than one user. You cannot delete a role that is in use without first removing the role from the users.', + ); + } +});