import dbInit, { ITestDb } from '../helpers/database-init'; import getLogger from '../../fixtures/no-logger'; // eslint-disable-next-line import/no-unresolved import { AccessService, ALL_PROJECTS, } from '../../../lib/services/access-service'; 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; let adminRole; let readRole; const createUserEditorAccess = async (name, email) => { const { userStore } = stores; const user = await userStore.insert({ name, email }); await accessService.addUserToRole(user.id, editorRole.id, 'default'); return user; }; const createUserViewerAccess = async (name, email) => { const { userStore } = stores; const user = await userStore.insert({ name, email }); await accessService.addUserToRole(user.id, readRole.id, ALL_PROJECTS); return user; }; const hasCommonProjectAccess = async (user, projectName, condition) => { const defaultEnv = 'default'; const developmentEnv = 'development'; const productionEnv = 'production'; const { CREATE_FEATURE, UPDATE_FEATURE, DELETE_FEATURE, CREATE_FEATURE_STRATEGY, UPDATE_FEATURE_STRATEGY, DELETE_FEATURE_STRATEGY, UPDATE_FEATURE_ENVIRONMENT, UPDATE_FEATURE_VARIANTS, } = permissions; expect( await accessService.hasPermission(user, CREATE_FEATURE, projectName), ).toBe(condition); expect( await accessService.hasPermission(user, UPDATE_FEATURE, projectName), ).toBe(condition); expect( await accessService.hasPermission(user, DELETE_FEATURE, projectName), ).toBe(condition); expect( await accessService.hasPermission( user, UPDATE_FEATURE_VARIANTS, projectName, ), ).toBe(condition); expect( await accessService.hasPermission( user, CREATE_FEATURE_STRATEGY, projectName, defaultEnv, ), ).toBe(condition); expect( await accessService.hasPermission( user, UPDATE_FEATURE_STRATEGY, projectName, defaultEnv, ), ).toBe(condition); expect( await accessService.hasPermission( user, DELETE_FEATURE_STRATEGY, projectName, defaultEnv, ), ).toBe(condition); expect( await accessService.hasPermission( user, UPDATE_FEATURE_ENVIRONMENT, projectName, defaultEnv, ), ).toBe(condition); expect( await accessService.hasPermission( user, CREATE_FEATURE_STRATEGY, projectName, developmentEnv, ), ).toBe(condition); expect( await accessService.hasPermission( user, UPDATE_FEATURE_STRATEGY, projectName, developmentEnv, ), ).toBe(condition); expect( await accessService.hasPermission( user, DELETE_FEATURE_STRATEGY, projectName, developmentEnv, ), ).toBe(condition); expect( await accessService.hasPermission( user, UPDATE_FEATURE_ENVIRONMENT, projectName, developmentEnv, ), ).toBe(condition); expect( await accessService.hasPermission( user, CREATE_FEATURE_STRATEGY, projectName, productionEnv, ), ).toBe(condition); expect( await accessService.hasPermission( user, UPDATE_FEATURE_STRATEGY, projectName, productionEnv, ), ).toBe(condition); expect( await accessService.hasPermission( user, DELETE_FEATURE_STRATEGY, projectName, productionEnv, ), ).toBe(condition); expect( await accessService.hasPermission( user, UPDATE_FEATURE_ENVIRONMENT, projectName, productionEnv, ), ).toBe(condition); }; const hasFullProjectAccess = async (user, projectName, condition) => { const { DELETE_PROJECT, UPDATE_PROJECT, MOVE_FEATURE_TOGGLE } = permissions; expect( await accessService.hasPermission(user, DELETE_PROJECT, projectName), ).toBe(condition); expect( await accessService.hasPermission(user, UPDATE_PROJECT, projectName), ).toBe(condition); expect( await accessService.hasPermission( user, MOVE_FEATURE_TOGGLE, projectName, ), ); hasCommonProjectAccess(user, projectName, condition); }; const createSuperUser = async () => { const { userStore } = stores; const user = await userStore.insert({ name: 'Alice Admin', email: 'admin@getunleash.io', }); await accessService.addUserToRole(user.id, adminRole.id, ALL_PROJECTS); return user; }; 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(); }); afterAll(async () => { await db.destroy(); }); test('should have access to admin addons', async () => { const { CREATE_ADDON, UPDATE_ADDON, DELETE_ADDON } = permissions; const user = editorUser; expect(await accessService.hasPermission(user, CREATE_ADDON)).toBe(true); expect(await accessService.hasPermission(user, UPDATE_ADDON)).toBe(true); expect(await accessService.hasPermission(user, DELETE_ADDON)).toBe(true); }); test('should have access to admin strategies', async () => { const { CREATE_STRATEGY, UPDATE_STRATEGY, DELETE_STRATEGY } = permissions; const user = editorUser; expect(await accessService.hasPermission(user, CREATE_STRATEGY)).toBe(true); expect(await accessService.hasPermission(user, UPDATE_STRATEGY)).toBe(true); expect(await accessService.hasPermission(user, DELETE_STRATEGY)).toBe(true); }); test('should have access to admin contexts', async () => { const { CREATE_CONTEXT_FIELD, UPDATE_CONTEXT_FIELD, DELETE_CONTEXT_FIELD } = permissions; const user = editorUser; expect(await accessService.hasPermission(user, CREATE_CONTEXT_FIELD)).toBe( true, ); expect(await accessService.hasPermission(user, UPDATE_CONTEXT_FIELD)).toBe( true, ); expect(await accessService.hasPermission(user, DELETE_CONTEXT_FIELD)).toBe( true, ); }); test('should have access to create projects', async () => { const { CREATE_PROJECT } = permissions; const user = editorUser; expect(await accessService.hasPermission(user, CREATE_PROJECT)).toBe(true); }); test('should have access to update applications', async () => { const { UPDATE_APPLICATION } = permissions; const user = editorUser; expect(await accessService.hasPermission(user, UPDATE_APPLICATION)).toBe( true, ); }); test('should not have admin permission', async () => { const { ADMIN } = permissions; const user = editorUser; expect(await accessService.hasPermission(user, ADMIN)).toBe(false); }); test('should have project admin to default project as editor', async () => { const projectName = 'default'; const user = editorUser; hasFullProjectAccess(user, projectName, true); }); test('should not have project admin to other projects as editor', async () => { const projectName = 'unusedprojectname'; const user = editorUser; hasFullProjectAccess(user, projectName, false); }); test('cannot add CREATE_FEATURE without defining project', async () => { await expect(async () => { await accessService.addPermissionToRole( editorRole.id, permissions.CREATE_FEATURE, ); }).rejects.toThrow( new Error('ProjectId cannot be empty for permission=CREATE_FEATURE'), ); }); test('cannot remove CREATE_FEATURE without defining project', async () => { await expect(async () => { await accessService.removePermissionFromRole( editorRole.id, permissions.CREATE_FEATURE, ); }).rejects.toThrow( new Error('ProjectId cannot be empty for permission=CREATE_FEATURE'), ); }); test('should remove CREATE_FEATURE on default environment', async () => { const { CREATE_FEATURE } = permissions; const user = editorUser; const editRole = await accessService.getRoleByName(RoleName.EDITOR); await accessService.addPermissionToRole( editRole.id, permissions.CREATE_FEATURE, '*', ); await accessService.removePermissionFromRole( editRole.id, permissions.CREATE_FEATURE, '*', ); expect( await accessService.hasPermission(user, CREATE_FEATURE, 'some-project'), ).toBe(false); }); test('admin should be admin', async () => { const { DELETE_PROJECT, UPDATE_PROJECT, CREATE_FEATURE, UPDATE_FEATURE, DELETE_FEATURE, ADMIN, } = permissions; const user = superUser; expect( await accessService.hasPermission(user, DELETE_PROJECT, 'default'), ).toBe(true); expect( await accessService.hasPermission(user, UPDATE_PROJECT, 'default'), ).toBe(true); expect( await accessService.hasPermission(user, CREATE_FEATURE, 'default'), ).toBe(true); expect( await accessService.hasPermission(user, UPDATE_FEATURE, 'default'), ).toBe(true); expect( await accessService.hasPermission(user, DELETE_FEATURE, 'default'), ).toBe(true); expect(await accessService.hasPermission(user, ADMIN)).toBe(true); }); test('should create default roles to project', async () => { const project = 'some-project'; const user = editorUser; await accessService.createDefaultProjectRoles(user, project); hasFullProjectAccess(user, project, true); }); test('should require name when create default roles to project', async () => { await expect(async () => { await accessService.createDefaultProjectRoles(editorUser); }).rejects.toThrow(new Error('ProjectId cannot be empty')); }); test('should grant user access to project', async () => { const { DELETE_PROJECT, UPDATE_PROJECT } = permissions; const project = 'another-project'; const user = editorUser; const sUser = await createUserViewerAccess( 'Some Random', 'random@getunleash.io', ); await accessService.createDefaultProjectRoles(user, project); const projectRole = await accessService.getRoleByName(RoleName.MEMBER); await accessService.addUserToRole(sUser.id, projectRole.id, project); // // Should be able to update feature toggles inside the project hasCommonProjectAccess(sUser, project, true); // Should not be able to admin the project itself. expect( await accessService.hasPermission(sUser, UPDATE_PROJECT, project), ).toBe(false); expect( await accessService.hasPermission(sUser, DELETE_PROJECT, project), ).toBe(false); }); test('should not get access if not specifying project', async () => { const project = 'another-project-2'; const user = editorUser; const sUser = await createUserViewerAccess( 'Some Random', 'random22@getunleash.io', ); await accessService.createDefaultProjectRoles(user, project); const projectRole = await accessService.getRoleByName(RoleName.MEMBER); await accessService.addUserToRole(sUser.id, projectRole.id, project); // Should not be able to update feature toggles outside project hasCommonProjectAccess(sUser, undefined, false); }); test('should remove user from role', async () => { const { userStore } = stores; const user = await userStore.insert({ name: 'Some User', email: 'random123@getunleash.io', }); await accessService.addUserToRole(user.id, editorRole.id, 'default'); // check user has one role const userRoles = await accessService.getRolesForUser(user.id); expect(userRoles.length).toBe(1); expect(userRoles[0].name).toBe(RoleName.EDITOR); await accessService.removeUserFromRole(user.id, editorRole.id, 'default'); const userRolesAfterRemove = await accessService.getRolesForUser(user.id); expect(userRolesAfterRemove.length).toBe(0); }); test('should return role with users', async () => { const { userStore } = stores; const user = await userStore.insert({ name: 'Some User', email: 'random2223@getunleash.io', }); await accessService.addUserToRole(user.id, editorRole.id, 'default'); const roleWithUsers = await accessService.getRoleData(editorRole.id); expect(roleWithUsers.role.name).toBe(RoleName.EDITOR); expect(roleWithUsers.users.length >= 2).toBe(true); expect(roleWithUsers.users.find((u) => u.id === user.id)).toBeTruthy(); expect( roleWithUsers.users.find((u) => u.email === user.email), ).toBeTruthy(); }); test('should return role with permissions and users', async () => { const { userStore } = stores; const user = await userStore.insert({ name: 'Some User', email: 'random2244@getunleash.io', }); await accessService.addUserToRole(user.id, editorRole.id, 'default'); const roleWithPermission = await accessService.getRoleData(editorRole.id); expect(roleWithPermission.role.name).toBe(RoleName.EDITOR); expect(roleWithPermission.permissions.length > 2).toBe(true); expect( roleWithPermission.permissions.find( (p) => p.name === permissions.CREATE_PROJECT, ), ).toBeTruthy(); //This assert requires other tests to have run in this pack before length > 2 resolves to true // I've set this to be > 1, which allows us to run the test alone and should still satisfy the logic requirement expect(roleWithPermission.users.length > 1).toBe(true); }); test('should set root role for user', async () => { const { userStore } = stores; const user = await userStore.insert({ name: 'Some User', email: 'random2255@getunleash.io', }); await accessService.setUserRootRole(user.id, editorRole.id); const roles = await accessService.getRolesForUser(user.id); //To have duplicated roles like this may not may not be a hack. Needs some thought expect(roles[0].name).toBe(RoleName.EDITOR); expect(roles.length).toBe(1); }); test('should switch root role for user', async () => { const { userStore } = stores; const user = await userStore.insert({ name: 'Some User', email: 'random22Read@getunleash.io', }); await accessService.setUserRootRole(user.id, editorRole.id); await accessService.setUserRootRole(user.id, readRole.id); const roles = await accessService.getRolesForUser(user.id); expect(roles.length).toBe(1); expect(roles[0].name).toBe(RoleName.VIEWER); }); test('should not crash if user does not have permission', async () => { const { userStore } = stores; const user = await userStore.insert({ name: 'Some User', email: 'random55Read@getunleash.io', }); await accessService.setUserRootRole(user.id, readRole.id); const { UPDATE_CONTEXT_FIELD } = permissions; const hasAccess = await accessService.hasPermission( user, UPDATE_CONTEXT_FIELD, ); expect(hasAccess).toBe(false); }); test('should support permission with "ALL" environment requirement', async () => { const { userStore, roleStore, accessStore } = stores; const user = await userStore.insert({ name: 'Some User', email: 'randomEnv1@getunleash.io', }); await accessService.setUserRootRole(user.id, readRole.id); const customRole = await roleStore.create({ name: 'Power user', roleType: 'custom', description: 'Grants access to modify all environments', }); const { CREATE_FEATURE_STRATEGY } = permissions; await accessStore.addPermissionsToRole( customRole.id, [CREATE_FEATURE_STRATEGY], 'production', ); await accessStore.addUserToRole(user.id, customRole.id, ALL_PROJECTS); const hasAccess = await accessService.hasPermission( user, CREATE_FEATURE_STRATEGY, 'default', 'production', ); expect(hasAccess).toBe(true); const hasNotAccess = await accessService.hasPermission( user, CREATE_FEATURE_STRATEGY, 'default', 'development', ); expect(hasNotAccess).toBe(false); }); test('Should have access to create a strategy in an environment', async () => { const { CREATE_FEATURE_STRATEGY } = permissions; const user = editorUser; expect( await accessService.hasPermission( user, CREATE_FEATURE_STRATEGY, 'default', 'development', ), ).toBe(true); }); test('Should be denied access to create a strategy in an environment the user does not have access to', async () => { const { CREATE_FEATURE_STRATEGY } = permissions; const user = editorUser; expect( await accessService.hasPermission( user, CREATE_FEATURE_STRATEGY, 'default', 'noaccess', ), ).toBe(false); }); test('Should have access to edit a strategy in an environment', async () => { const { UPDATE_FEATURE_STRATEGY } = permissions; const user = editorUser; expect( await accessService.hasPermission( user, UPDATE_FEATURE_STRATEGY, 'default', 'development', ), ).toBe(true); }); test('Should have access to delete a strategy in an environment', async () => { const { DELETE_FEATURE_STRATEGY } = permissions; const user = editorUser; expect( await accessService.hasPermission( user, DELETE_FEATURE_STRATEGY, 'default', 'development', ), ).toBe(true); }); test('Should be denied access to delete a strategy in an environment the user does not have access to', async () => { const { DELETE_FEATURE_STRATEGY } = permissions; const user = editorUser; expect( await accessService.hasPermission( user, DELETE_FEATURE_STRATEGY, 'default', 'noaccess', ), ).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.', ); } }); test('Should be denied move feature toggle to project where the user does not have access', async () => { const user = editorUser; const editorUser2 = await createUserEditorAccess( 'seconduser', 'bob2@gmail.com', ); const projectOrigin = { id: 'projectOrigin', name: 'New project', description: 'Blah', }; const projectDest = { id: 'projectDest', name: 'New project', description: 'Blah', }; await projectService.createProject(projectOrigin, user.id); await projectService.createProject(projectDest, editorUser2.id); const featureToggle = { name: 'moveableToggle' }; await featureToggleService.createFeatureToggle( projectOrigin.id, featureToggle, user.username, ); try { await projectService.changeProject( projectDest.id, featureToggle.name, user, projectOrigin.id, ); } catch (e) { expect(e.toString()).toBe( 'NoAccessError: You need permission=MOVE_FEATURE_TOGGLE to perform this action', ); } }); test('Should be allowed move feature toggle to project when the user has access', async () => { const user = editorUser; const projectOrigin = { id: 'projectOrigin1', name: 'New project', description: 'Blah', }; const projectDest = { id: 'projectDest2', name: 'New project', description: 'Blah', }; await projectService.createProject(projectOrigin, user); await projectService.createProject(projectDest, user); const featureToggle = { name: 'moveableToggle2' }; await featureToggleService.createFeatureToggle( projectOrigin.id, featureToggle, user.username, ); await projectService.changeProject( projectDest.id, featureToggle.name, user, projectOrigin.id, ); }); test('Should not be allowed to edit a built in role', async () => { expect.assertions(1); const editRole = await accessService.getRoleByName(RoleName.EDITOR); const roleUpdate = { id: editRole.id, name: 'NoLongerTheEditor', description: 'Ha!', }; try { await accessService.updateRole(roleUpdate); } catch (e) { expect(e.toString()).toBe( 'InvalidOperationError: You can not change built in roles.', ); } });