From cb02ae9c92d4353a9db16c1d2f6a374c931f42d3 Mon Sep 17 00:00:00 2001 From: sighphyre Date: Mon, 13 Dec 2021 11:14:26 +0200 Subject: [PATCH] feat: Add api endpoints for roles and permissions list --- src/lib/db/access-store.ts | 101 +++++++++++++++--- src/lib/db/role-store.ts | 15 +-- src/lib/services/access-service.ts | 38 ++++--- src/lib/services/project-service.ts | 2 +- src/lib/services/role-service.ts | 4 + src/lib/services/user-service.ts | 7 +- src/lib/types/model.ts | 13 ++- src/lib/types/stores/access-store.ts | 14 ++- src/lib/types/stores/role-store.ts | 1 + .../20211202120808-add-custom-roles.js | 51 +++++++-- .../e2e/services/access-service.e2e.test.ts | 35 +----- src/test/fixtures/access-service-mock.ts | 8 +- src/test/fixtures/fake-access-store.ts | 12 +++ src/test/fixtures/fake-role-store.ts | 1 + 14 files changed, 221 insertions(+), 81 deletions(-) diff --git a/src/lib/db/access-store.ts b/src/lib/db/access-store.ts index 4b8cf41bb8..2e00a02975 100644 --- a/src/lib/db/access-store.ts +++ b/src/lib/db/access-store.ts @@ -9,13 +9,26 @@ import { IUserPermission, IUserRole, } from '../types/stores/access-store'; +import { + IAvailablePermissions, + IEnvironmentPermission, + IPermission, +} from 'lib/types/model'; const T = { ROLE_USER: 'role_user', - ROLES: 'role_project', + ROLES: 'roles', ROLE_PERMISSION: 'role_permission', + PERMISSIONS: 'permissions', }; +interface IPermissionRow { + id: number; + permission: string; + display_name: string; + environment: string; +} + export class AccessStore implements IAccessStore { private logger: Logger; @@ -33,6 +46,20 @@ export class AccessStore implements IAccessStore { }); } + async setupPermissionsForEnvironment( + environmentName: string, + permissions: string[], + ): Promise { + const rows = permissions.map((permission) => { + return { + permission: permission, + display_name: '', + environment: environmentName, + }; + }); + await this.db.batchInsert(T.PERMISSIONS, rows); + } + async delete(key: number): Promise { await this.db(T.ROLES).where({ id: key }).del(); } @@ -64,12 +91,56 @@ export class AccessStore implements IAccessStore { return Promise.resolve([]); } + async getAvailablePermissions(): Promise { + const rows = await this.db + .select(['id', 'permission', 'environment', 'display_name']) + .from(T.PERMISSIONS); + + let projectPermissions: IPermission[] = []; + let rawEnvironments = new Map(); + + for (let permission of rows) { + if (!permission.environment) { + projectPermissions.push(this.mapPermission(permission)); + } else { + if (!rawEnvironments.get(permission.environment)) { + rawEnvironments.set(permission.environment, []); + } + rawEnvironments.get(permission.environment).push(permission); + } + } + let allEnvironmentPermissions: Array = + Array.from(rawEnvironments).map( + ([environmentName, environmentPermissions]) => { + return { + environmentName: environmentName, + permissions: environmentPermissions.map( + this.mapPermission, + ), + }; + }, + ); + return { + project: projectPermissions, + environments: allEnvironmentPermissions, + }; + } + + mapPermission(permission: IPermissionRow): IPermission { + return { + id: permission.id, + name: permission.permission, + displayName: permission.display_name, + }; + } + async getPermissionsForUser(userId: number): Promise { const stopTimer = this.timer('getPermissionsForUser'); const rows = await this.db .select('project', 'permission', 'environment') .from(`${T.ROLE_PERMISSION} AS rp`) - .leftJoin(`${T.ROLE_USER} AS ur`, 'ur.role_id', 'rp.role_id') + .join(`${T.ROLE_USER} AS ur`, 'ur.role_id', 'rp.role_id') + .join(`${T.PERMISSIONS} AS p`, 'p.id', 'rp.permission_id') .where('ur.user_id', '=', userId); stopTimer(); return rows; @@ -109,7 +180,7 @@ export class AccessStore implements IAccessStore { async getRootRoles(): Promise { return this.db - .select(['id', 'name', 'type', 'project', 'description']) + .select(['id', 'name', 'type', 'description']) .from(T.ROLES) .andWhere('type', 'root'); } @@ -138,10 +209,15 @@ export class AccessStore implements IAccessStore { return rows.map((r) => r.user_id); } - async addUserToRole(userId: number, roleId: number): Promise { + async addUserToRole( + userId: number, + roleId: number, + projecId: string, + ): Promise { return this.db(T.ROLE_USER).insert({ user_id: userId, role_id: roleId, + project: projecId, }); } @@ -171,7 +247,6 @@ export class AccessStore implements IAccessStore { async createRole( name: string, type: string, - project?: string, description?: string, ): Promise { const [id] = await this.db(T.ROLES) @@ -179,7 +254,6 @@ export class AccessStore implements IAccessStore { name, description, type, - project, }) .returning('id'); return { @@ -187,22 +261,25 @@ export class AccessStore implements IAccessStore { name, description, type, - project, }; } async addPermissionsToRole( role_id: number, permissions: string[], - projectId?: string, environment?: string, ): Promise { - const rows = permissions.map((permission) => ({ + const result = await this.db.raw( + `SELECT id FROM ${T.PERMISSIONS} where environment = ? and permission = ANY(?)`, + [environment, permissions], + ); + const ids = result.rows.map((x) => x.id); + + const rows = ids.map((permission_id) => ({ role_id, - project: projectId, - permission, - environment, + permission_id, })); + return this.db.batchInsert(T.ROLE_PERMISSION, rows); } diff --git a/src/lib/db/role-store.ts b/src/lib/db/role-store.ts index 524796199d..d9e9b52a76 100644 --- a/src/lib/db/role-store.ts +++ b/src/lib/db/role-store.ts @@ -38,7 +38,13 @@ export default class RoleStore { } async create(role: ICustomRoleInsert): Promise { - const row = await this.db(TABLE).insert(role).returning('*'); + const row = await this.db(TABLE) + .insert({ + name: role.name, + description: role.description, + type: role.roleType, + }) + .returning('*'); return this.mapRow(row[0]); } @@ -47,12 +53,7 @@ export default class RoleStore { } async get(id: number): Promise { - const rows = await this.db - .select(COLUMNS) - .from(TABLE) - .where({ id }) - .orderBy('name', 'asc'); - + const rows = await this.db.select(COLUMNS).from(TABLE).where({ id }); return this.mapRow(rows[0]); } diff --git a/src/lib/services/access-service.ts b/src/lib/services/access-service.ts index dd12ed2d2c..57711d7af5 100644 --- a/src/lib/services/access-service.ts +++ b/src/lib/services/access-service.ts @@ -10,10 +10,10 @@ import { IUserStore } from '../types/stores/user-store'; import { Logger } from '../logger'; import { IUnleashStores } from '../types/stores'; import { + IAvailablePermissions, IPermission, IRoleData, IUserWithRole, - PermissionType, RoleName, RoleType, } from '../types/model'; @@ -63,12 +63,12 @@ export class AccessService { this.store = accessStore; this.userStore = userStore; this.logger = getLogger('/services/access-service.ts'); - this.permissions = Object.values(permissions).map((p) => ({ - name: p, - type: isProjectPermission(p) - ? PermissionType.project - : PermissionType.root, - })); + // this.permissions = Object.values(permissions).map((p) => ({ + // name: p, + // type: isProjectPermission(p) + // ? PermissionType.project + // : PermissionType.root, + // })); } /** @@ -90,6 +90,8 @@ export class AccessService { try { const userP = await this.getPermissionsForUser(user); + console.log('My checks are', permission, projectId, environment); + console.log('My permissions for user are', userP); return userP .filter( @@ -126,12 +128,16 @@ export class AccessService { return this.store.getPermissionsForUser(user.id); } - getPermissions(): IPermission[] { - return this.permissions; + async getPermissions(): Promise { + return this.store.getAvailablePermissions(); } - async addUserToRole(userId: number, roleId: number): Promise { - return this.store.addUserToRole(userId, roleId); + async addUserToRole( + userId: number, + roleId: number, + projectId: string, + ): Promise { + return this.store.addUserToRole(userId, roleId, projectId); } async setUserRootRole( @@ -139,14 +145,17 @@ export class AccessService { role: number | RoleName, ): Promise { const newRootRole = await this.resolveRootRole(role); - if (newRootRole) { try { await this.store.removeRolesOfTypeForUser( userId, RoleType.ROOT, ); - await this.store.addUserToRole(userId, newRootRole.id); + await this.store.addUserToRole( + userId, + newRootRole.id, + ALL_PROJECTS, + ); } catch (error) { throw new Error( `Could not add role=${newRootRole.name} to userId=${userId}`, @@ -251,7 +260,6 @@ export class AccessService { const ownerRole = await this.store.createRole( RoleName.OWNER, RoleType.PROJECT, - projectId, PROJECT_DESCRIPTION.OWNER, ); await this.store.addPermissionsToRole( @@ -265,7 +273,7 @@ export class AccessService { this.logger.info( `Making ${owner.id} admin of ${projectId} via roleId=${ownerRole.id}`, ); - await this.store.addUserToRole(owner.id, ownerRole.id); + await this.store.addUserToRole(owner.id, ownerRole.id, projectId); } const memberRole = await this.store.createRole( RoleName.MEMBER, diff --git a/src/lib/services/project-service.ts b/src/lib/services/project-service.ts index d69320a302..c08485ff0f 100644 --- a/src/lib/services/project-service.ts +++ b/src/lib/services/project-service.ts @@ -300,7 +300,7 @@ export default class ProjectService { throw new Error(`User already has access to project=${projectId}`); } - await this.accessService.addUserToRole(userId, role.id); + await this.accessService.addUserToRole(userId, role.id, projectId); } // TODO: should be an event too diff --git a/src/lib/services/role-service.ts b/src/lib/services/role-service.ts index 0687bbb6fc..e1e0b9f6e9 100644 --- a/src/lib/services/role-service.ts +++ b/src/lib/services/role-service.ts @@ -21,6 +21,10 @@ export default class RoleService { return this.store.getAll(); } + async get(id: number): Promise { + return this.store.get(id); + } + async create(role: ICustomRoleInsert): Promise { return this.store.create(role); } diff --git a/src/lib/services/user-service.ts b/src/lib/services/user-service.ts index 129e16830a..5c5cdb3508 100644 --- a/src/lib/services/user-service.ts +++ b/src/lib/services/user-service.ts @@ -133,14 +133,19 @@ class UserService { const user = await this.store.insert({ username: 'admin', }); + this.logger.info(`Did the thing"`); const passwordHash = await bcrypt.hash(pwd, saltRounds); + this.logger.info(`Bcrypted that beast"`); await this.store.setPasswordHash(user.id, passwordHash); - + this.logger.info(`Set the hash"`); await this.accessService.setUserRootRole( user.id, RoleName.ADMIN, ); } catch (e) { + console.log(`My error ${e}`); + console.log(e); + this.logger.error(e); this.logger.error('Unable to create default user "admin"'); } } diff --git a/src/lib/types/model.ts b/src/lib/types/model.ts index 3f4222de68..08625d462f 100644 --- a/src/lib/types/model.ts +++ b/src/lib/types/model.ts @@ -215,9 +215,20 @@ export interface IRoleData { permissions: IUserPermission[]; } +export interface IAvailablePermissions { + project: IPermission[]; + environments: IEnvironmentPermission[]; +} + export interface IPermission { + id: number; name: string; - type: PermissionType; + displayName: string; +} + +export interface IEnvironmentPermission { + environmentName: string; + permissions: IPermission[]; } export enum PermissionType { diff --git a/src/lib/types/stores/access-store.ts b/src/lib/types/stores/access-store.ts index 13ab9af9bc..9afa732122 100644 --- a/src/lib/types/stores/access-store.ts +++ b/src/lib/types/stores/access-store.ts @@ -1,3 +1,4 @@ +import { IAvailablePermissions } from '../model'; import { Store } from './store'; export interface IUserPermission { @@ -11,7 +12,6 @@ export interface IRole { name: string; description?: string; type: string; - project?: string; } export interface IUserRole { @@ -19,6 +19,7 @@ export interface IUserRole { userId: number; } export interface IAccessStore extends Store { + getAvailablePermissions(): Promise; getPermissionsForUser(userId: number): Promise; getPermissionsForRole(roleId: number): Promise; getRoles(): Promise; @@ -27,7 +28,15 @@ export interface IAccessStore extends Store { removeRolesForProject(projectId: string): Promise; getRolesForUserId(userId: number): Promise; getUserIdsForRole(roleId: number): Promise; - addUserToRole(userId: number, roleId: number): Promise; + setupPermissionsForEnvironment( + environmentName: string, + permissions: string[], + ): Promise; + addUserToRole( + userId: number, + roleId: number, + projectId: string, + ): Promise; removeUserFromRole(userId: number, roleId: number): Promise; removeRolesOfTypeForUser(userId: number, roleType: string): Promise; createRole( @@ -39,7 +48,6 @@ export interface IAccessStore extends Store { addPermissionsToRole( role_id: number, permissions: string[], - projectId?: string, environment?: string, ): Promise; removePermissionFromRole( diff --git a/src/lib/types/stores/role-store.ts b/src/lib/types/stores/role-store.ts index 2ab720e928..9db994dd54 100644 --- a/src/lib/types/stores/role-store.ts +++ b/src/lib/types/stores/role-store.ts @@ -4,6 +4,7 @@ import { Store } from './store'; export interface ICustomRoleInsert { name: string; description: string; + roleType: string; } export interface IRoleStore extends Store { diff --git a/src/migrations/20211202120808-add-custom-roles.js b/src/migrations/20211202120808-add-custom-roles.js index 2b144c8e3b..5254f0c7b7 100644 --- a/src/migrations/20211202120808-add-custom-roles.js +++ b/src/migrations/20211202120808-add-custom-roles.js @@ -1,13 +1,48 @@ exports.up = function (db, cb) { db.runSql( ` - ALTER TABLE roles RENAME TO role_project; - CREATE TABLE roles ( - id integer PRIMARY KEY NOT NULL, - name varchar(255), - description text - ); - `, + CREATE TABLE IF NOT EXISTS permissions + ( id SERIAL PRIMARY KEY, + permission VARCHAR(255) NOT NULL, + environment VARCHAR(255), + display_name TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT now() + ); + + INSERT INTO permissions (permission, environment, display_name) (SELECT DISTINCT permission, environment, '' from role_permission); + + ALTER TABLE role_user ADD COLUMN + project VARCHAR(255); + + UPDATE role_user + SET project = roles.project + FROM roles + WHERE role_user.role_id = roles.id; + + ALTER TABLE roles DROP COLUMN project; + + ALTER TABLE roles + ADD COLUMN + updated_at TIMESTAMP WITH TIME ZONE; + + ALTER TABLE role_permission + ADD COLUMN + permission_id INTEGER; + + UPDATE role_permission + SET permission_id = permissions.id + FROM permissions + WHERE + (role_permission.environment = permissions.environment + OR (role_permission.environment IS NULL AND permissions.environment IS NULL)) + AND + role_permission.permission = permissions.permission; + + ALTER TABLE role_permission + DROP COLUMN project, + DROP COLUMN permission, + DROP COLUMN environment + `, cb, ); }; @@ -15,8 +50,6 @@ exports.up = function (db, cb) { exports.down = function (db, cb) { db.runSql( ` - DROP TABLE roles; - ALTER TABLE role_project RENAME TO roles; `, cb, ); diff --git a/src/test/e2e/services/access-service.e2e.test.ts b/src/test/e2e/services/access-service.e2e.test.ts index 41b854b348..e71f049b3f 100644 --- a/src/test/e2e/services/access-service.e2e.test.ts +++ b/src/test/e2e/services/access-service.e2e.test.ts @@ -24,7 +24,7 @@ let readRole; const createUserEditorAccess = async (name, email) => { const { userStore } = stores; const user = await userStore.insert({ name, email }); - await accessService.addUserToRole(user.id, editorRole.id); + await accessService.addUserToRole(user.id, editorRole.id, ALL_PROJECTS); return user; }; @@ -34,7 +34,7 @@ const createSuperUser = async () => { name: 'Alice Admin', email: 'admin@getunleash.io', }); - await accessService.addUserToRole(user.id, adminRole.id); + await accessService.addUserToRole(user.id, adminRole.id, ALL_PROJECTS); return user; }; @@ -384,27 +384,6 @@ test('should return role with permissions and users', async () => { expect(roleWithPermission.users.length > 2).toBe(true); }); -test('should return list of permissions', async () => { - const p = await accessService.getPermissions(); - - const findPerm = (perm) => p.find((_) => _.name === perm); - - const { - DELETE_FEATURE, - UPDATE_FEATURE, - CREATE_FEATURE, - UPDATE_PROJECT, - CREATE_PROJECT, - } = permissions; - - expect(p.length > 2).toBe(true); - expect(findPerm(CREATE_PROJECT).type).toBe('root'); - expect(findPerm(UPDATE_PROJECT).type).toBe('project'); - expect(findPerm(CREATE_FEATURE).type).toBe('project'); - expect(findPerm(UPDATE_FEATURE).type).toBe('project'); - expect(findPerm(DELETE_FEATURE).type).toBe('project'); -}); - test('should set root role for user', async () => { const { userStore } = stores; const user = await userStore.insert({ @@ -468,20 +447,16 @@ test('should support permission with "ALL" environment requirement', async () => const customRole = await accessStore.createRole( 'Power user', 'custom', - 'default', 'Grants access to modify all environments', ); const { CREATE_FEATURE_STRATEGY } = permissions; - await accessStore.addPermissionsToRole( customRole.id, [CREATE_FEATURE_STRATEGY], - 'default', - ALL_PROJECTS, + 'production', ); - - await accessStore.addUserToRole(user.id, customRole.id); + await accessStore.addUserToRole(user.id, customRole.id, ALL_PROJECTS); const hasAccess = await accessService.hasPermission( user, @@ -498,7 +473,7 @@ test('should support permission with "ALL" environment requirement', async () => 'default', 'development', ); - expect(hasNotAccess).toBe(true); + expect(hasNotAccess).toBe(false); }); test('Should have access to create a strategy in an environment', async () => { diff --git a/src/test/fixtures/access-service-mock.ts b/src/test/fixtures/access-service-mock.ts index c8c5089bbd..073c71acaa 100644 --- a/src/test/fixtures/access-service-mock.ts +++ b/src/test/fixtures/access-service-mock.ts @@ -4,7 +4,11 @@ import { AccessService } from '../../lib/services/access-service'; import User from '../../lib/types/user'; import noLoggerProvider from './no-logger'; import { IRole } from '../../lib/types/stores/access-store'; -import { IPermission, IRoleData, IUserWithRole } from '../../lib/types/model'; +import { + IAvailablePermissions, + IRoleData, + IUserWithRole, +} from '../../lib/types/model'; class AccessServiceMock extends AccessService { constructor() { @@ -22,7 +26,7 @@ class AccessServiceMock extends AccessService { throw new Error('Method not implemented.'); } - getPermissions(): IPermission[] { + getPermissions(): Promise { throw new Error('Method not implemented.'); } diff --git a/src/test/fixtures/fake-access-store.ts b/src/test/fixtures/fake-access-store.ts index 0e637d03dd..b2dba70f87 100644 --- a/src/test/fixtures/fake-access-store.ts +++ b/src/test/fixtures/fake-access-store.ts @@ -6,12 +6,24 @@ import { IUserPermission, IUserRole, } from '../../lib/types/stores/access-store'; +import { IAvailablePermissions, IPermission } from 'lib/types/model'; class AccessStoreMock implements IAccessStore { + setupPermissionsForEnvironment( + environmentName: string, + permissions: string[], + ): Promise { + throw new Error('Method not implemented.'); + } + userPermissions: IUserPermission[] = []; roles: IRole[] = []; + getAvailablePermissions(): Promise { + throw new Error('Method not implemented.'); + } + getPermissionsForUser(userId: Number): Promise { return Promise.resolve([]); } diff --git a/src/test/fixtures/fake-role-store.ts b/src/test/fixtures/fake-role-store.ts index ec64d3a829..a22c27ea43 100644 --- a/src/test/fixtures/fake-role-store.ts +++ b/src/test/fixtures/fake-role-store.ts @@ -15,6 +15,7 @@ export default class FakeRoleStore implements IRoleStore { createdAt: new Date(), }); } + async getAll(): Promise { return Promise.resolve([ {