From 2ec427190e111f67a6925ee9c0a974091059c461 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20Conradi=20=C3=98sthus?= Date: Fri, 12 Nov 2021 13:12:11 +0100 Subject: [PATCH] wip: environment for permissions --- src/lib/db/access-store.ts | 6 ++- src/lib/services/access-service.ts | 10 +++- src/lib/types/permissions.ts | 12 ++++- src/lib/types/stores/access-store.ts | 2 + ...d-environment-column-to-role-permission.js | 12 +++++ .../e2e/services/access-service.e2e.test.ts | 53 +++++++++++++++++-- 6 files changed, 88 insertions(+), 7 deletions(-) create mode 100644 src/migrations/20211111194757-add-environment-column-to-role-permission.js diff --git a/src/lib/db/access-store.ts b/src/lib/db/access-store.ts index cee7c16fb9..681f076375 100644 --- a/src/lib/db/access-store.ts +++ b/src/lib/db/access-store.ts @@ -67,7 +67,7 @@ export class AccessStore implements IAccessStore { async getPermissionsForUser(userId: number): Promise { const stopTimer = this.timer('getPermissionsForUser'); const rows = await this.db - .select('project', 'permission') + .select('project', 'permission', 'environment') .from(`${T.ROLE_PERMISSION} AS rp`) .leftJoin(`${T.ROLE_USER} AS ur`, 'ur.role_id', 'rp.role_id') .where('ur.user_id', '=', userId); @@ -78,7 +78,7 @@ export class AccessStore implements IAccessStore { async getPermissionsForRole(roleId: number): Promise { const stopTimer = this.timer('getPermissionsForRole'); const rows = await this.db - .select('project', 'permission') + .select('project', 'permission', 'environment') .from(`${T.ROLE_PERMISSION}`) .where('role_id', '=', roleId); stopTimer(); @@ -195,11 +195,13 @@ export class AccessStore implements IAccessStore { role_id: number, permissions: string[], projectId?: string, + environment?: string, ): Promise { const rows = permissions.map((permission) => ({ role_id, project: projectId, permission, + environment, })); return this.db.batchInsert(T.ROLE_PERMISSION, rows); } diff --git a/src/lib/services/access-service.ts b/src/lib/services/access-service.ts index e5011a63e4..dd12ed2d2c 100644 --- a/src/lib/services/access-service.ts +++ b/src/lib/services/access-service.ts @@ -19,6 +19,7 @@ import { } from '../types/model'; export const ALL_PROJECTS = '*'; +export const ALL_ENVS = '*'; const PROJECT_DESCRIPTION = { OWNER: 'Users with this role have full control over the project, and can add and manage other users within the project context, manage feature toggles within the project, and control advanced project features like archiving and deleting the project.', @@ -81,9 +82,10 @@ export class AccessService { user: User, permission: string, projectId?: string, + environment?: string, ): Promise { this.logger.info( - `Checking permission=${permission}, userId=${user.id} projectId=${projectId}`, + `Checking permission=${permission}, userId=${user.id}, projectId=${projectId}, environment=${environment}`, ); try { @@ -96,6 +98,12 @@ export class AccessService { p.project === projectId || p.project === ALL_PROJECTS, ) + .filter( + (p) => + !p.environment || + p.environment === environment || + p.environment === ALL_ENVS, + ) .some( (p) => p.permission === permission || p.permission === ADMIN, diff --git a/src/lib/types/permissions.ts b/src/lib/types/permissions.ts index 0e0efe92f7..5cbb96725e 100644 --- a/src/lib/types/permissions.ts +++ b/src/lib/types/permissions.ts @@ -1,4 +1,3 @@ -// Special export const ADMIN = 'ADMIN'; export const CLIENT = 'CLIENT'; export const NONE = 'NONE'; @@ -6,21 +5,32 @@ export const NONE = 'NONE'; export const CREATE_FEATURE = 'CREATE_FEATURE'; export const UPDATE_FEATURE = 'UPDATE_FEATURE'; export const DELETE_FEATURE = 'DELETE_FEATURE'; + +export const CREATE_FEATURE_STRATEGY = 'CREATE_FEATURE_STRATEGY'; +export const UPDATE_FEATURE_STRATEGY = 'UPDATE_FEATURE_STRATEGY'; +export const DELETE_FEATURE_STRATEGY = 'DELETE_FEATURE_STRATEGY'; +export const UPDATE_FEATURE_ENVIRONMENT = 'UPDATE_FEATURE_ENVIRONMENT'; + export const CREATE_STRATEGY = 'CREATE_STRATEGY'; export const UPDATE_STRATEGY = 'UPDATE_STRATEGY'; export const DELETE_STRATEGY = 'DELETE_STRATEGY'; + export const UPDATE_APPLICATION = 'UPDATE_APPLICATION'; export const CREATE_CONTEXT_FIELD = 'CREATE_CONTEXT_FIELD'; export const UPDATE_CONTEXT_FIELD = 'UPDATE_CONTEXT_FIELD'; export const DELETE_CONTEXT_FIELD = 'DELETE_CONTEXT_FIELD'; + export const CREATE_PROJECT = 'CREATE_PROJECT'; export const UPDATE_PROJECT = 'UPDATE_PROJECT'; export const DELETE_PROJECT = 'DELETE_PROJECT'; + export const CREATE_ADDON = 'CREATE_ADDON'; export const UPDATE_ADDON = 'UPDATE_ADDON'; export const DELETE_ADDON = 'DELETE_ADDON'; + export const READ_ROLE = 'READ_ROLE'; export const UPDATE_ROLE = 'UPDATE_ROLE'; + export const UPDATE_API_TOKEN = 'UPDATE_API_TOKEN'; export const CREATE_API_TOKEN = 'CREATE_API_TOKEN'; export const DELETE_API_TOKEN = 'DELETE_API_TOKEN'; diff --git a/src/lib/types/stores/access-store.ts b/src/lib/types/stores/access-store.ts index 9adf5c1a2a..13ab9af9bc 100644 --- a/src/lib/types/stores/access-store.ts +++ b/src/lib/types/stores/access-store.ts @@ -2,6 +2,7 @@ import { Store } from './store'; export interface IUserPermission { project?: string; + environment?: string; permission: string; } @@ -39,6 +40,7 @@ export interface IAccessStore extends Store { role_id: number, permissions: string[], projectId?: string, + environment?: string, ): Promise; removePermissionFromRole( roleId: number, diff --git a/src/migrations/20211111194757-add-environment-column-to-role-permission.js b/src/migrations/20211111194757-add-environment-column-to-role-permission.js new file mode 100644 index 0000000000..ed61cea6ee --- /dev/null +++ b/src/migrations/20211111194757-add-environment-column-to-role-permission.js @@ -0,0 +1,12 @@ +'use strict'; + +exports.up = function (db, cb) { + db.runSql( + 'ALTER TABLE role_permission ADD COLUMN environment varchar(255);', + cb, + ); +}; + +exports.down = function (db, cb) { + db.runSql('ALTER TABLE role_permission DROP COLUMN environment', cb); +}; diff --git a/src/test/e2e/services/access-service.e2e.test.ts b/src/test/e2e/services/access-service.e2e.test.ts index be473647a9..dcc2220bb2 100644 --- a/src/test/e2e/services/access-service.e2e.test.ts +++ b/src/test/e2e/services/access-service.e2e.test.ts @@ -1,4 +1,4 @@ -import dbInit from '../helpers/database-init'; +import dbInit, { ITestDb } from '../helpers/database-init'; import getLogger from '../../fixtures/no-logger'; // eslint-disable-next-line import/no-unresolved @@ -9,9 +9,10 @@ import { import * as permissions from '../../../lib/types/permissions'; import { RoleName } from '../../../lib/types/model'; +import { IUnleashStores } from '../../../lib/types'; -let db; -let stores; +let db: ITestDb; +let stores: IUnleashStores; let accessService; let editorUser; @@ -453,3 +454,49 @@ test('should not crash if user does not have permission', async () => { expect(hasAccess).toBe(false); }); + +test('should support permission with "ALL" environment requirement', async () => { + const { userStore, accessStore } = stores; + + const user = await userStore.insert({ + name: 'Some User', + email: 'randomEnv1@getunleash.io', + }); + + await accessService.setUserRootRole(user.id, readRole.id); + + 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, + ); + + await accessStore.addUserToRole(user.id, customRole.id); + + 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(true); +});