From 2eb0b6a11e2e3d3ffea13ff1904bb30a7966a99b Mon Sep 17 00:00:00 2001 From: sighphyre Date: Fri, 3 Dec 2021 10:18:26 +0200 Subject: [PATCH] chore: Implement scaffolding for new rbac --- src/lib/db/access-store.ts | 2 +- src/lib/db/index.ts | 2 + src/lib/db/project-store.ts | 2 +- src/lib/db/role-store.ts | 88 +++++++++++++++++++ src/lib/routes/admin-api/project/features.ts | 3 + src/lib/services/index.ts | 4 + src/lib/services/project-service.ts | 2 +- src/lib/services/role-service.ts | 31 +++++++ src/lib/types/model.ts | 7 ++ src/lib/types/services.ts | 2 + src/lib/types/stores.ts | 2 + src/lib/types/stores/role-store.ts | 13 +++ .../20211202120808-add-custom-roles.js | 23 +++++ .../e2e/services/access-service.e2e.test.ts | 52 +++++++++++ .../e2e/services/project-service.e2e.test.ts | 2 +- 15 files changed, 231 insertions(+), 4 deletions(-) create mode 100644 src/lib/db/role-store.ts create mode 100644 src/lib/services/role-service.ts create mode 100644 src/lib/types/stores/role-store.ts create mode 100644 src/migrations/20211202120808-add-custom-roles.js diff --git a/src/lib/db/access-store.ts b/src/lib/db/access-store.ts index 681f076375..4b8cf41bb8 100644 --- a/src/lib/db/access-store.ts +++ b/src/lib/db/access-store.ts @@ -12,7 +12,7 @@ import { const T = { ROLE_USER: 'role_user', - ROLES: 'roles', + ROLES: 'role_project', ROLE_PERMISSION: 'role_permission', }; diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts index 40d36623fb..04dab0866f 100644 --- a/src/lib/db/index.ts +++ b/src/lib/db/index.ts @@ -27,6 +27,7 @@ import FeatureTagStore from './feature-tag-store'; import { FeatureEnvironmentStore } from './feature-environment-store'; import { ClientMetricsStoreV2 } from './client-metrics-store-v2'; import UserSplashStore from './user-splash-store'; +import RoleStore from './role-store'; export const createStores = ( config: IUnleashConfig, @@ -77,6 +78,7 @@ export const createStores = ( getLogger, ), userSplashStore: new UserSplashStore(db, eventBus, getLogger), + roleStore: new RoleStore(db, eventBus, getLogger), }; }; diff --git a/src/lib/db/project-store.ts b/src/lib/db/project-store.ts index 4d19552c44..44b622a156 100644 --- a/src/lib/db/project-store.ts +++ b/src/lib/db/project-store.ts @@ -88,7 +88,7 @@ class ProjectStore implements IProjectStore { const row = await this.db(TABLE) .insert(this.fieldToRow(project)) .returning('*'); - return this.mapRow(row); + return this.mapRow(row[0]); } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types diff --git a/src/lib/db/role-store.ts b/src/lib/db/role-store.ts new file mode 100644 index 0000000000..524796199d --- /dev/null +++ b/src/lib/db/role-store.ts @@ -0,0 +1,88 @@ +import EventEmitter from 'events'; +import { Knex } from 'knex'; +import { Logger, LogProvider } from '../logger'; +import NotFoundError from '../error/notfound-error'; +import { ICustomRole } from 'lib/types/model'; +import { ICustomRoleInsert } from 'lib/types/stores/role-store'; + +const TABLE = 'roles'; +const COLUMNS = ['id', 'name', 'description', 'created_at']; + +interface IRoleRow { + id: number; + name: string; + description: string; + created_at: Date; +} + +export default class RoleStore { + private logger: Logger; + + private eventBus: EventEmitter; + + private db: Knex; + + constructor(db: Knex, eventBus: EventEmitter, getLogger: LogProvider) { + this.db = db; + this.eventBus = eventBus; + this.logger = getLogger('lib/db/role-store.ts'); + } + + async getAll(): Promise { + const rows = await this.db + .select(COLUMNS) + .from(TABLE) + .orderBy('name', 'asc'); + + return rows.map(this.mapRow); + } + + async create(role: ICustomRoleInsert): Promise { + const row = await this.db(TABLE).insert(role).returning('*'); + return this.mapRow(row[0]); + } + + async delete(id: number): Promise { + return this.db(TABLE).where({ id }).del(); + } + + async get(id: number): Promise { + const rows = await this.db + .select(COLUMNS) + .from(TABLE) + .where({ id }) + .orderBy('name', 'asc'); + + return this.mapRow(rows[0]); + } + + async exists(id: number): Promise { + const result = await this.db.raw( + `SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE id = ?) AS present`, + [id], + ); + const { present } = result.rows[0]; + return present; + } + + async deleteAll(): Promise { + return this.db(TABLE).del(); + } + + mapRow(row: IRoleRow): ICustomRole { + if (!row) { + throw new NotFoundError('No project found'); + } + + return { + id: row.id, + name: row.name, + description: row.description, + createdAt: row.created_at, + }; + } + + destroy(): void {} +} + +module.exports = RoleStore; diff --git a/src/lib/routes/admin-api/project/features.ts b/src/lib/routes/admin-api/project/features.ts index 9ab4765dde..07ce7fe914 100644 --- a/src/lib/routes/admin-api/project/features.ts +++ b/src/lib/routes/admin-api/project/features.ts @@ -9,7 +9,10 @@ import { CREATE_FEATURE, DELETE_FEATURE, CREATE_FEATURE_STRATEGY, + DELETE_FEATURE_STRATEGY, UPDATE_FEATURE, + UPDATE_FEATURE_ENVIRONMENT, + UPDATE_FEATURE_STRATEGY, } from '../../../types/permissions'; import { FeatureToggleDTO, diff --git a/src/lib/services/index.ts b/src/lib/services/index.ts index d2a80e3f5c..cb3c24055a 100644 --- a/src/lib/services/index.ts +++ b/src/lib/services/index.ts @@ -28,6 +28,7 @@ import EnvironmentService from './environment-service'; import FeatureTagService from './feature-tag-service'; import ProjectHealthService from './project-health-service'; import UserSplashService from './user-splash-service'; +import RoleService from './role-service'; export const createServices = ( stores: IUnleashStores, @@ -75,6 +76,8 @@ export const createServices = ( ); const userSplashService = new UserSplashService(stores, config); + const roleService = new RoleService(stores, config); + return { accessService, addonService, @@ -103,6 +106,7 @@ export const createServices = ( featureTagService, projectHealthService, userSplashService, + roleService, }; }; diff --git a/src/lib/services/project-service.ts b/src/lib/services/project-service.ts index 29cd4ff9e6..d69320a302 100644 --- a/src/lib/services/project-service.ts +++ b/src/lib/services/project-service.ts @@ -297,7 +297,7 @@ export default class ProjectService { const alreadyHasAccess = users.some((u) => u.id === userId); if (alreadyHasAccess) { - throw new Error(`User already have access to project=${projectId}`); + throw new Error(`User already has access to project=${projectId}`); } await this.accessService.addUserToRole(userId, role.id); diff --git a/src/lib/services/role-service.ts b/src/lib/services/role-service.ts new file mode 100644 index 0000000000..0687bbb6fc --- /dev/null +++ b/src/lib/services/role-service.ts @@ -0,0 +1,31 @@ +import { IUnleashConfig } from 'lib/server-impl'; +import { IUnleashStores } from 'lib/types'; +import { ICustomRole } from 'lib/types/model'; +import { ICustomRoleInsert, IRoleStore } from 'lib/types/stores/role-store'; +import { Logger } from '../logger'; + +export default class RoleService { + private logger: Logger; + + private store: IRoleStore; + + constructor( + { roleStore }: Pick, + { getLogger }: Pick, + ) { + this.logger = getLogger('lib/services/session-service.ts'); + this.store = roleStore; + } + + async getAll(): Promise { + return this.store.getAll(); + } + + async create(role: ICustomRoleInsert): Promise { + return this.store.create(role); + } + + async delete(id: number): Promise { + return this.store.delete(id); + } +} diff --git a/src/lib/types/model.ts b/src/lib/types/model.ts index b80ffd80ce..3f4222de68 100644 --- a/src/lib/types/model.ts +++ b/src/lib/types/model.ts @@ -313,6 +313,13 @@ export interface IProject { updatedAt?: Date; } +export interface ICustomRole { + id: number; + name: string; + description: string; + createdAt: Date; +} + export interface IProjectWithCount extends IProject { featureCount: number; memberCount: number; diff --git a/src/lib/types/services.ts b/src/lib/types/services.ts index 3144ef3822..483859ccf2 100644 --- a/src/lib/types/services.ts +++ b/src/lib/types/services.ts @@ -24,6 +24,7 @@ import FeatureTagService from '../services/feature-tag-service'; import ProjectHealthService from '../services/project-health-service'; import ClientMetricsServiceV2 from '../services/client-metrics/metrics-service-v2'; import UserSplashService from '../services/user-splash-service'; +import RoleService from 'lib/services/role-service'; export interface IUnleashServices { accessService: AccessService; @@ -53,4 +54,5 @@ export interface IUnleashServices { userService: UserService; versionService: VersionService; userSplashService: UserSplashService; + roleService: RoleService; } diff --git a/src/lib/types/stores.ts b/src/lib/types/stores.ts index d0d9778f26..ea9bac57c7 100644 --- a/src/lib/types/stores.ts +++ b/src/lib/types/stores.ts @@ -23,6 +23,7 @@ import { IEnvironmentStore } from './stores/environment-store'; import { IFeatureToggleClientStore } from './stores/feature-toggle-client-store'; import { IClientMetricsStoreV2 } from './stores/client-metrics-store-v2'; import { IUserSplashStore } from './stores/user-splash-store'; +import { IRoleStore } from './stores/role-store'; export interface IUnleashStores { accessStore: IAccessStore; @@ -50,4 +51,5 @@ export interface IUnleashStores { userFeedbackStore: IUserFeedbackStore; userStore: IUserStore; userSplashStore: IUserSplashStore; + roleStore: IRoleStore; } diff --git a/src/lib/types/stores/role-store.ts b/src/lib/types/stores/role-store.ts new file mode 100644 index 0000000000..2ab720e928 --- /dev/null +++ b/src/lib/types/stores/role-store.ts @@ -0,0 +1,13 @@ +import { ICustomRole } from '../model'; +import { Store } from './store'; + +export interface ICustomRoleInsert { + name: string; + description: string; +} + +export interface IRoleStore extends Store { + getAll(): Promise; + create(role: ICustomRoleInsert): Promise; + delete(id: number): Promise; +} diff --git a/src/migrations/20211202120808-add-custom-roles.js b/src/migrations/20211202120808-add-custom-roles.js new file mode 100644 index 0000000000..2b144c8e3b --- /dev/null +++ b/src/migrations/20211202120808-add-custom-roles.js @@ -0,0 +1,23 @@ +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 + ); + `, + 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 25ea4fae87..41b854b348 100644 --- a/src/test/e2e/services/access-service.e2e.test.ts +++ b/src/test/e2e/services/access-service.e2e.test.ts @@ -526,3 +526,55 @@ test('Should be denied access to create a strategy in an environment the user do ), ).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 be denied access to edit a strategy in an environment the user does not have access to', async () => { + const { UPDATE_FEATURE_STRATEGY } = permissions; + const user = editorUser; + expect( + await accessService.hasPermission( + user, + UPDATE_FEATURE_STRATEGY, + 'default', + 'noaccess', + ), + ).toBe(false); +}); + +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); +}); diff --git a/src/test/e2e/services/project-service.e2e.test.ts b/src/test/e2e/services/project-service.e2e.test.ts index 05e26f7f05..5f8f0e9fcc 100644 --- a/src/test/e2e/services/project-service.e2e.test.ts +++ b/src/test/e2e/services/project-service.e2e.test.ts @@ -354,7 +354,7 @@ test('add user should fail if user already have access', async () => { await expect(async () => projectService.addUser(project.id, memberRole.id, projectMember1.id), ).rejects.toThrow( - new Error('User already have access to project=add-users-twice'), + new Error('User already has access to project=add-users-twice'), ); });