From 95b50acdd8b7031ecb13564dd368dc8b2a4d92d0 Mon Sep 17 00:00:00 2001 From: sighphyre Date: Fri, 7 Jan 2022 11:10:48 +0200 Subject: [PATCH] feat: Schema validation for roles --- src/lib/schema/role-schema.test.ts | 83 +++++++++++++++++++ src/lib/schema/role-schema.ts | 22 +++++ src/lib/services/access-service.ts | 14 ++-- src/lib/util/constants.ts | 2 + .../20211202120808-add-custom-roles.js | 2 +- 5 files changed, 116 insertions(+), 7 deletions(-) create mode 100644 src/lib/schema/role-schema.test.ts create mode 100644 src/lib/schema/role-schema.ts diff --git a/src/lib/schema/role-schema.test.ts b/src/lib/schema/role-schema.test.ts new file mode 100644 index 0000000000..05fb65b6d0 --- /dev/null +++ b/src/lib/schema/role-schema.test.ts @@ -0,0 +1,83 @@ +import { roleSchema } from './role-schema'; + +test('role schema rejects a role without a name', async () => { + expect.assertions(1); + const role = { + permissions: [], + }; + + try { + await roleSchema.validateAsync(role); + } catch (error) { + expect(error.details[0].message).toBe('"name" is required'); + } +}); + +test('role schema allows a role with an empty description', async () => { + const role = { + name: 'Brønsted', + description: '', + }; + + const value = await roleSchema.validateAsync(role); + expect(value.description).toEqual(''); +}); + +test('role schema rejects a role with a broken permission list', async () => { + expect.assertions(1); + const role = { + name: 'Mendeleev', + permissions: [ + { + aPropertyThatIsAproposToNothing: true, + }, + ], + }; + + try { + await roleSchema.validateAsync(role); + } catch (error) { + expect(error.details[0].message).toBe( + '"permissions[0].id" is required', + ); + } +}); + +test('role schema allows a role with an empty permission list', async () => { + const role = { + name: 'Avogadro', + permissions: [], + }; + + const value = await roleSchema.validateAsync(role); + expect(value.permissions).toEqual([]); +}); + +test('role schema allows a role with a null list', async () => { + const role = { + name: 'Curie', + permissions: null, + }; + + const value = await roleSchema.validateAsync(role); + expect(value.permissions).toEqual(null); +}); + +test('role schema allows an undefined with a null list', async () => { + const role = { + name: 'Fischer', + }; + + const value = await roleSchema.validateAsync(role); + expect(value.permissions).toEqual(undefined); +}); + +test('role schema strips roleType if present', async () => { + const role = { + name: 'Grignard', + roleType: 'Organic Chemistry', + }; + + const value = await roleSchema.validateAsync(role); + expect(value.roleType).toEqual(undefined); +}); diff --git a/src/lib/schema/role-schema.ts b/src/lib/schema/role-schema.ts new file mode 100644 index 0000000000..8959c0302b --- /dev/null +++ b/src/lib/schema/role-schema.ts @@ -0,0 +1,22 @@ +import joi from 'joi'; + +export const permissionRoleSchema = joi + .object() + .keys({ + id: joi.number().required(), + enivronment: joi.string().allow(null).optional(), + }) + .options({ stripUnknown: true, allowUnknown: false, abortEarly: false }); + +export const roleSchema = joi + .object() + .keys({ + name: joi.string().required(), + description: joi.string().optional().allow('').allow(null).default(''), + permissions: joi + .array() + .allow(null) + .optional() + .items(permissionRoleSchema), + }) + .options({ stripUnknown: true, allowUnknown: false, abortEarly: false }); diff --git a/src/lib/services/access-service.ts b/src/lib/services/access-service.ts index c7049bffac..6117431607 100644 --- a/src/lib/services/access-service.ts +++ b/src/lib/services/access-service.ts @@ -23,6 +23,8 @@ 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'; +import { roleSchema } from '../schema/role-schema'; +import { CUSTOM_ROLE_TYPE } from '../util/constants'; export const ALL_PROJECTS = '*'; export const ALL_ENVS = '*'; @@ -386,12 +388,11 @@ export class AccessService { } async createRole(role: IRoleCreation): Promise { - await this.validateRole(role); const baseRole = { - name: role.name, - description: role.description, - roleType: 'custom', + ...(await this.validateRole(role)), + roleType: CUSTOM_ROLE_TYPE, }; + const rolePermissions = role.permissions; const newRole = await this.roleStore.create(baseRole); if (rolePermissions) { @@ -451,8 +452,9 @@ export class AccessService { async validateRole( role: IRoleCreation, existingId?: number, - ): Promise { + ): Promise { + const cleanedRole = await roleSchema.validateAsync(role); await this.validateRoleIsUnique(role.name, existingId); - //Handle schema validation here... + return cleanedRole; } } diff --git a/src/lib/util/constants.ts b/src/lib/util/constants.ts index ea7fffc2c3..45b0ab23b8 100644 --- a/src/lib/util/constants.ts +++ b/src/lib/util/constants.ts @@ -3,3 +3,5 @@ export const DEFAULT_ENV = 'default'; export const ROOT_PERMISSION_TYPE = 'root'; export const ENVIRONMENT_PERMISSION_TYPE = 'environment'; export const PROJECT_PERMISSION_TYPE = 'project'; + +export const CUSTOM_ROLE_TYPE = 'custom'; diff --git a/src/migrations/20211202120808-add-custom-roles.js b/src/migrations/20211202120808-add-custom-roles.js index 487137f332..af78c66f53 100644 --- a/src/migrations/20211202120808-add-custom-roles.js +++ b/src/migrations/20211202120808-add-custom-roles.js @@ -167,7 +167,7 @@ exports.up = function (db, cb) { p.id as permission_id, '*' environment FROM permissions p - WHERE p.permission = 'ADMIN' + WHERE p.permission = 'ADMIN'; `, cb, );