From df7509e3819c4c141d88a0ac0ea0e19094b65c0a Mon Sep 17 00:00:00 2001 From: Benjamin Ludewig Date: Wed, 19 Dec 2018 10:36:56 +0100 Subject: [PATCH] feature: Add action specific user permissions --- lib/permissions.js | 45 +++++++++++++++ lib/permissions.test.js | 78 ++++++++++++++++++++++++++ lib/routes/admin-api/archive.js | 7 ++- lib/routes/admin-api/archive.test.js | 12 +++- lib/routes/admin-api/feature.js | 17 ++++-- lib/routes/admin-api/feature.test.js | 24 ++++++-- lib/routes/admin-api/index.js | 12 ++-- lib/routes/admin-api/metrics-schema.js | 22 ++++++++ lib/routes/admin-api/metrics.js | 37 ++++++++---- lib/routes/admin-api/metrics.test.js | 11 +++- lib/routes/admin-api/strategy.js | 15 +++-- lib/routes/admin-api/strategy.test.js | 45 +++++++++++---- lib/routes/controller.js | 24 ++++++-- lib/user.js | 3 +- test/fixtures/permissions.js | 17 ++++++ 15 files changed, 312 insertions(+), 57 deletions(-) create mode 100644 lib/permissions.js create mode 100644 lib/permissions.test.js create mode 100644 lib/routes/admin-api/metrics-schema.js create mode 100644 test/fixtures/permissions.js diff --git a/lib/permissions.js b/lib/permissions.js new file mode 100644 index 0000000000..a8fc39ad4b --- /dev/null +++ b/lib/permissions.js @@ -0,0 +1,45 @@ +'use strict'; + +const ADMIN = 'ADMIN'; +const REVIVE_FEATURE = 'REVIVE_FEATURE'; +const CREATE_FEATURE = 'CREATE_FEATURE'; +const UPDATE_FEATURE = 'UPDATE_FEATURE'; +const DELETE_FEATURE = 'DELETE_FEATURE'; +const CREATE_STRATEGY = 'CREATE_STRATEGY'; +const UPDATE_STRATEGY = 'UPDATE_STRATEGY'; +const DELETE_STRATEGY = 'DELETE_STRATEGY'; +const UPDATE_APPLICATION = 'UPDATE_APPLICATION'; + +function requirePerms(prms) { + return (req, res, next) => { + for (const permission of prms) { + if ( + req.user && + req.user.permissions && + (req.user.permissions.indexOf(ADMIN) !== -1 || + req.user.permissions.indexOf(permission) !== -1) + ) { + return next(); + } + } + return res + .status(403) + .json({ + message: 'Missing permissions to perform this action.', + }) + .end(); + }; +} + +module.exports = { + requirePerms, + ADMIN, + REVIVE_FEATURE, + CREATE_FEATURE, + UPDATE_FEATURE, + DELETE_FEATURE, + CREATE_STRATEGY, + UPDATE_STRATEGY, + DELETE_STRATEGY, + UPDATE_APPLICATION, +}; diff --git a/lib/permissions.test.js b/lib/permissions.test.js new file mode 100644 index 0000000000..8240a1ccb5 --- /dev/null +++ b/lib/permissions.test.js @@ -0,0 +1,78 @@ +'use strict'; + +const test = require('ava'); +const store = require('./../test/fixtures/store'); +const { requirePerms } = require('./permissions'); +const supertest = require('supertest'); +const getApp = require('./app'); + +const { EventEmitter } = require('events'); +const eventBus = new EventEmitter(); + +function getSetup(preRouterHook) { + const base = `/random${Math.round(Math.random() * 1000)}`; + const stores = store.createStores(); + const app = getApp({ + baseUriPath: base, + stores, + eventBus, + extendedPermissions: true, + preRouterHook(_app) { + preRouterHook(_app); + + _app.get( + `${base}/protectedResource`, + requirePerms(['READ']), + (req, res) => { + res.status(200) + .json({ message: 'OK' }) + .end(); + } + ); + }, + }); + + return { + base, + request: supertest(app), + }; +} + +test('should return 403 when missing permission', t => { + t.plan(0); + const { base, request } = getSetup(() => {}); + + return request.get(`${base}/protectedResource`).expect(403); +}); + +test('should allow access with correct permissions', t => { + const { base, request } = getSetup(app => { + app.use((req, res, next) => { + req.user = { email: 'some@email.com', permissions: ['READ'] }; + next(); + }); + }); + + return request + .get(`${base}/protectedResource`) + .expect(200) + .expect(res => { + t.is(res.body.message, 'OK'); + }); +}); + +test('should allow access with admin permissions', t => { + const { base, request } = getSetup(app => { + app.use((req, res, next) => { + req.user = { email: 'some@email.com', permissions: ['ADMIN'] }; + next(); + }); + }); + + return request + .get(`${base}/protectedResource`) + .expect(200) + .expect(res => { + t.is(res.body.message, 'OK'); + }); +}); diff --git a/lib/routes/admin-api/archive.js b/lib/routes/admin-api/archive.js index 8e535af252..4bab99547a 100644 --- a/lib/routes/admin-api/archive.js +++ b/lib/routes/admin-api/archive.js @@ -5,15 +5,16 @@ const Controller = require('../controller'); const logger = require('../../logger')('/admin-api/archive.js'); const { FEATURE_REVIVED } = require('../../event-type'); const extractUser = require('../../extract-user'); +const { REVIVE_FEATURE } = require('../../permissions'); class ArchiveController extends Controller { - constructor({ featureToggleStore, eventStore }) { - super(); + constructor(extendedPerms, { featureToggleStore, eventStore }) { + super(extendedPerms); this.featureToggleStore = featureToggleStore; this.eventStore = eventStore; this.get('/features', this.getArchivedFeatures); - this.post('/revive/:name', this.reviveFeatureToggle); + this.post('/revive/:name', this.reviveFeatureToggle, REVIVE_FEATURE); } async getArchivedFeatures(req, res) { diff --git a/lib/routes/admin-api/archive.test.js b/lib/routes/admin-api/archive.test.js index e022ff9dad..d625b83f43 100644 --- a/lib/routes/admin-api/archive.test.js +++ b/lib/routes/admin-api/archive.test.js @@ -2,8 +2,10 @@ const test = require('ava'); const store = require('./../../../test/fixtures/store'); +const permissions = require('../../../test/fixtures/permissions'); const supertest = require('supertest'); const getApp = require('../../app'); +const { REVIVE_FEATURE } = require('../../permissions'); const { EventEmitter } = require('events'); const eventBus = new EventEmitter(); @@ -11,14 +13,18 @@ const eventBus = new EventEmitter(); function getSetup() { const base = `/random${Math.round(Math.random() * 1000)}`; const stores = store.createStores(); + const perms = permissions(); const app = getApp({ baseUriPath: base, stores, eventBus, + extendedPermissions: true, + preRouterHook: perms.hook, }); return { base, + perms, archiveStore: stores.featureToggleStore, eventStore: stores.eventStore, request: supertest(app), @@ -60,7 +66,8 @@ test('should get archived toggles via admin', t => { test('should revive toggle', t => { t.plan(0); const name = 'name1'; - const { request, base, archiveStore } = getSetup(); + const { request, base, archiveStore, perms } = getSetup(); + perms.withPerms(REVIVE_FEATURE); archiveStore.addArchivedFeature({ name, strategies: [{ name: 'default' }], @@ -72,7 +79,8 @@ test('should revive toggle', t => { test('should create event when reviving toggle', async t => { t.plan(4); const name = 'name1'; - const { request, base, archiveStore, eventStore } = getSetup(); + const { request, base, archiveStore, eventStore, perms } = getSetup(); + perms.withPerms(REVIVE_FEATURE); archiveStore.addArchivedFeature({ name, strategies: [{ name: 'default' }], diff --git a/lib/routes/admin-api/feature.js b/lib/routes/admin-api/feature.js index 0eb4b3dc57..35cba1dcfd 100644 --- a/lib/routes/admin-api/feature.js +++ b/lib/routes/admin-api/feature.js @@ -11,22 +11,27 @@ const { const NameExistsError = require('../../error/name-exists-error'); const { handleErrors } = require('./util'); const extractUser = require('../../extract-user'); +const { + UPDATE_FEATURE, + DELETE_FEATURE, + CREATE_FEATURE, +} = require('../../permissions'); const { featureShema, nameSchema } = require('./feature-schema'); const version = 1; class FeatureController extends Controller { - constructor({ featureToggleStore, eventStore }) { - super(); + constructor(extendedPerms, { featureToggleStore, eventStore }) { + super(extendedPerms); this.featureToggleStore = featureToggleStore; this.eventStore = eventStore; this.get('/', this.getAllToggles); - this.post('/', this.createToggle); + this.post('/', this.createToggle, CREATE_FEATURE); this.get('/:featureName', this.getToggle); - this.put('/:featureName', this.updateToggle); - this.delete('/:featureName', this.deleteToggle); + this.put('/:featureName', this.updateToggle, UPDATE_FEATURE); + this.delete('/:featureName', this.deleteToggle, DELETE_FEATURE); this.post('/validate', this.validate); - this.post('/:featureName/toggle', this.toggle); + this.post('/:featureName/toggle', this.toggle, UPDATE_FEATURE); } async getAllToggles(req, res) { diff --git a/lib/routes/admin-api/feature.test.js b/lib/routes/admin-api/feature.test.js index 5f380260fb..6a7074c8ff 100644 --- a/lib/routes/admin-api/feature.test.js +++ b/lib/routes/admin-api/feature.test.js @@ -2,8 +2,10 @@ const test = require('ava'); const store = require('./../../../test/fixtures/store'); +const permissions = require('../../../test/fixtures/permissions'); const supertest = require('supertest'); const getApp = require('../../app'); +const { UPDATE_FEATURE, CREATE_FEATURE } = require('../../permissions'); const { EventEmitter } = require('events'); const eventBus = new EventEmitter(); @@ -11,14 +13,18 @@ const eventBus = new EventEmitter(); function getSetup() { const base = `/random${Math.round(Math.random() * 1000)}`; const stores = store.createStores(); + const perms = permissions(); const app = getApp({ baseUriPath: base, stores, eventBus, + extendedPermissions: true, + preRouterHook: perms.hook, }); return { base, + perms, featureToggleStore: stores.featureToggleStore, request: supertest(app), }; @@ -72,7 +78,8 @@ test('should add version numbers for /features', t => { test('should require at least one strategy when creating a feature toggle', t => { t.plan(0); - const { request, base } = getSetup(); + const { request, base, perms } = getSetup(); + perms.withPerms(CREATE_FEATURE); return request .post(`${base}/api/admin/features`) @@ -83,7 +90,8 @@ test('should require at least one strategy when creating a feature toggle', t => test('should be allowed to use new toggle name', t => { t.plan(0); - const { request, base } = getSetup(); + const { request, base, perms } = getSetup(); + perms.withPerms(CREATE_FEATURE); return request .post(`${base}/api/admin/features/validate`) @@ -136,7 +144,8 @@ test('should not be allowed to reuse archived toggle name', t => { test('should require at least one strategy when updating a feature toggle', t => { t.plan(0); - const { request, featureToggleStore, base } = getSetup(); + const { request, featureToggleStore, base, perms } = getSetup(); + perms.withPerms(UPDATE_FEATURE); featureToggleStore.addFeature({ name: 'ts', strategies: [{ name: 'default' }], @@ -151,7 +160,8 @@ test('should require at least one strategy when updating a feature toggle', t => test('valid feature names should pass validation', t => { t.plan(0); - const { request, base } = getSetup(); + const { request, base, perms } = getSetup(); + perms.withPerms(CREATE_FEATURE); const validNames = [ 'com.example', @@ -179,7 +189,8 @@ test('valid feature names should pass validation', t => { test('invalid feature names should not pass validation', t => { t.plan(0); - const { request, base } = getSetup(); + const { request, base, perms } = getSetup(); + perms.withPerms(CREATE_FEATURE); const invalidNames = [ 'some example', @@ -207,7 +218,8 @@ test('invalid feature names should not pass validation', t => { // Make sure current UI works. Should align on joi errors in future. test('invalid feature names should have error msg', t => { t.plan(1); - const { request, base } = getSetup(); + const { request, base, perms } = getSetup(); + perms.withPerms(CREATE_FEATURE); const name = 'ØÆ`'; diff --git a/lib/routes/admin-api/index.js b/lib/routes/admin-api/index.js index dc32c2021c..da7e497130 100644 --- a/lib/routes/admin-api/index.js +++ b/lib/routes/admin-api/index.js @@ -14,13 +14,17 @@ class AdminApi extends Controller { super(); const stores = config.stores; + const perms = config.extendedPermissions; this.app.get('/', this.index); - this.app.use('/features', new FeatureController(stores).router); - this.app.use('/archive', new ArchiveController(stores).router); - this.app.use('/strategies', new StrategyController(stores).router); + this.app.use('/features', new FeatureController(perms, stores).router); + this.app.use('/archive', new ArchiveController(perms, stores).router); + this.app.use( + '/strategies', + new StrategyController(perms, stores).router + ); this.app.use('/events', new EventController(stores).router); - this.app.use('/metrics', new MetricsController(stores).router); + this.app.use('/metrics', new MetricsController(perms, stores).router); this.app.use('/user', new UserController().router); } diff --git a/lib/routes/admin-api/metrics-schema.js b/lib/routes/admin-api/metrics-schema.js new file mode 100644 index 0000000000..d020716853 --- /dev/null +++ b/lib/routes/admin-api/metrics-schema.js @@ -0,0 +1,22 @@ +'use strict'; + +const joi = require('joi'); +const { nameType } = require('./util'); + +const applicationSchema = joi + .object() + .options({ stripUnknown: false }) + .keys({ + appName: nameType, + sdkVersion: joi.string().optional(), + strategies: joi + .array() + .required() + .items(joi.string(), joi.any().strip()), + description: joi.string().optional(), + url: joi.string().optional(), + color: joi.string().optional(), + icon: joi.string().optional(), + }); + +module.exports = applicationSchema; diff --git a/lib/routes/admin-api/metrics.js b/lib/routes/admin-api/metrics.js index 54d2622db4..4d4f51a997 100644 --- a/lib/routes/admin-api/metrics.js +++ b/lib/routes/admin-api/metrics.js @@ -1,18 +1,24 @@ 'use strict'; +const joi = require('joi'); const Controller = require('../controller'); const logger = require('../../logger')('/admin-api/metrics.js'); const ClientMetrics = require('../../client-metrics'); +const schema = require('./metrics-schema'); +const { UPDATE_APPLICATION } = require('../../permissions'); class MetricsController extends Controller { - constructor({ - clientMetricsStore, - clientInstanceStore, - clientApplicationsStore, - strategyStore, - featureToggleStore, - }) { - super(); + constructor( + extendedPerms, + { + clientMetricsStore, + clientInstanceStore, + clientApplicationsStore, + strategyStore, + featureToggleStore, + } + ) { + super(extendedPerms); this.metrics = new ClientMetrics(clientMetricsStore); this.clientInstanceStore = clientInstanceStore; this.clientApplicationsStore = clientApplicationsStore; @@ -23,7 +29,11 @@ class MetricsController extends Controller { this.get('/seen-apps', this.getSeenApps); this.get('/feature-toggles', this.getFeatureToggles); this.get('/feature-toggles/:name', this.getFeatureToggle); - this.post('/applications/:appName', this.createApplication); + this.post( + '/applications/:appName', + this.createApplication, + UPDATE_APPLICATION + ); this.get('/applications/', this.getApplications); this.get('/applications/:appName', this.getApplication); } @@ -67,14 +77,19 @@ class MetricsController extends Controller { }); } - // Todo: add joi-schema validation async createApplication(req, res) { const input = Object.assign({}, req.body, { appName: req.params.appName, }); + const { value: applicationData, error } = joi.validate(input, schema); + + if (error) { + logger.warn('Invalid application data posted', error); + return res.status(400).json(error); + } try { - await this.clientApplicationsStore.upsert(input); + await this.clientApplicationsStore.upsert(applicationData); res.status(202).end(); } catch (err) { logger.error(err); diff --git a/lib/routes/admin-api/metrics.test.js b/lib/routes/admin-api/metrics.test.js index 72bf5ce5cc..112464b4a2 100644 --- a/lib/routes/admin-api/metrics.test.js +++ b/lib/routes/admin-api/metrics.test.js @@ -2,23 +2,29 @@ const test = require('ava'); const store = require('./../../../test/fixtures/store'); +const permissions = require('../../../test/fixtures/permissions'); const supertest = require('supertest'); const getApp = require('../../app'); +const { UPDATE_APPLICATION } = require('../../permissions'); const { EventEmitter } = require('events'); const eventBus = new EventEmitter(); function getSetup() { const stores = store.createStores(); + const perms = permissions(); const app = getApp({ baseUriPath: '', stores, eventBus, + extendedPermissions: true, + preRouterHook: perms.hook, }); return { request: supertest(app), stores, + perms, }; } @@ -125,11 +131,12 @@ test('should return applications', t => { test('should store application', t => { t.plan(0); - const { request } = getSetup(); + const { request, perms } = getSetup(); const appName = '123!23'; + perms.withPerms(UPDATE_APPLICATION); return request .post(`/api/admin/metrics/applications/${appName}`) - .send({ appName }) + .send({ appName, strategies: ['default'] }) .expect(202); }); diff --git a/lib/routes/admin-api/strategy.js b/lib/routes/admin-api/strategy.js index a6e721e5d2..e0f73e7f7b 100644 --- a/lib/routes/admin-api/strategy.js +++ b/lib/routes/admin-api/strategy.js @@ -8,19 +8,24 @@ const NameExistsError = require('../../error/name-exists-error'); const extractUser = require('../../extract-user'); const strategySchema = require('./strategy-schema'); const { handleErrors } = require('./util'); +const { + DELETE_STRATEGY, + CREATE_STRATEGY, + UPDATE_STRATEGY, +} = require('../../permissions'); const version = 1; class StrategyController extends Controller { - constructor({ strategyStore, eventStore }) { - super(); + constructor(extendedPerms, { strategyStore, eventStore }) { + super(extendedPerms); this.strategyStore = strategyStore; this.eventStore = eventStore; this.get('/', this.getAllStratgies); this.get('/:name', this.getStrategy); - this.delete('/:name', this.removeStrategy); - this.post('/', this.createStrategy); - this.put('/:strategyName', this.updateStrategy); + this.delete('/:name', this.removeStrategy, DELETE_STRATEGY); + this.post('/', this.createStrategy, CREATE_STRATEGY); + this.put('/:strategyName', this.updateStrategy, UPDATE_STRATEGY); } async getAllStratgies(req, res) { diff --git a/lib/routes/admin-api/strategy.test.js b/lib/routes/admin-api/strategy.test.js index c23996d99a..c3cec26525 100644 --- a/lib/routes/admin-api/strategy.test.js +++ b/lib/routes/admin-api/strategy.test.js @@ -2,25 +2,35 @@ const test = require('ava'); const store = require('./../../../test/fixtures/store'); +const permissions = require('../../../test/fixtures/permissions'); const supertest = require('supertest'); const getApp = require('../../app'); +const { + DELETE_STRATEGY, + CREATE_STRATEGY, + UPDATE_STRATEGY, +} = require('../../permissions'); const { EventEmitter } = require('events'); const eventBus = new EventEmitter(); function getSetup() { const base = `/random${Math.round(Math.random() * 1000)}`; + const perms = permissions(); const stores = store.createStores(); const app = getApp({ baseUriPath: base, stores, eventBus, + extendedPermissions: true, + preRouterHook: perms.hook, }); return { base, strategyStore: stores.strategyStore, request: supertest(app), + perms, }; } @@ -39,7 +49,8 @@ test('add version numbers for /stategies', t => { test('require a name when creating a new stratey', t => { t.plan(1); - const { request, base } = getSetup(); + const { request, base, perms } = getSetup(); + perms.withPerms(CREATE_STRATEGY); return request .post(`${base}/api/admin/strategies`) @@ -52,7 +63,8 @@ test('require a name when creating a new stratey', t => { test('require parameters array when creating a new stratey', t => { t.plan(1); - const { request, base } = getSetup(); + const { request, base, perms } = getSetup(); + perms.withPerms(CREATE_STRATEGY); return request .post(`${base}/api/admin/strategies`) @@ -65,7 +77,8 @@ test('require parameters array when creating a new stratey', t => { test('create a new stratey with empty parameters', t => { t.plan(0); - const { request, base } = getSetup(); + const { request, base, perms } = getSetup(); + perms.withPerms(CREATE_STRATEGY); return request .post(`${base}/api/admin/strategies`) @@ -75,7 +88,8 @@ test('create a new stratey with empty parameters', t => { test('not be possible to override name', t => { t.plan(0); - const { request, base, strategyStore } = getSetup(); + const { request, base, strategyStore, perms } = getSetup(); + perms.withPerms(CREATE_STRATEGY); strategyStore.addStrategy({ name: 'Testing', parameters: [] }); return request @@ -87,7 +101,8 @@ test('not be possible to override name', t => { test('update strategy', t => { t.plan(0); const name = 'AnotherStrat'; - const { request, base, strategyStore } = getSetup(); + const { request, base, strategyStore, perms } = getSetup(); + perms.withPerms(UPDATE_STRATEGY); strategyStore.addStrategy({ name, parameters: [] }); return request @@ -96,10 +111,11 @@ test('update strategy', t => { .expect(200); }); -test('not update uknown strategy', t => { +test('not update unknown strategy', t => { t.plan(0); const name = 'UnknownStrat'; - const { request, base } = getSetup(); + const { request, base, perms } = getSetup(); + perms.withPerms(UPDATE_STRATEGY); return request .put(`${base}/api/admin/strategies/${name}`) @@ -110,7 +126,8 @@ test('not update uknown strategy', t => { test('validate format when updating strategy', t => { t.plan(0); const name = 'AnotherStrat'; - const { request, base, strategyStore } = getSetup(); + const { request, base, strategyStore, perms } = getSetup(); + perms.withPerms(UPDATE_STRATEGY); strategyStore.addStrategy({ name, parameters: [] }); return request @@ -122,7 +139,8 @@ test('validate format when updating strategy', t => { test('editable=false will stop delete request', t => { t.plan(0); const name = 'default'; - const { request, base } = getSetup(); + const { request, base, perms } = getSetup(); + perms.withPerms(DELETE_STRATEGY); return request.delete(`${base}/api/admin/strategies/${name}`).expect(500); }); @@ -130,7 +148,8 @@ test('editable=false will stop delete request', t => { test('editable=false will stop edit request', t => { t.plan(0); const name = 'default'; - const { request, base } = getSetup(); + const { request, base, perms } = getSetup(); + perms.withPerms(UPDATE_STRATEGY); return request .put(`${base}/api/admin/strategies/${name}`) @@ -141,7 +160,8 @@ test('editable=false will stop edit request', t => { test('editable=true will allow delete request', t => { t.plan(0); const name = 'deleteStrat'; - const { request, base, strategyStore } = getSetup(); + const { request, base, strategyStore, perms } = getSetup(); + perms.withPerms(DELETE_STRATEGY); strategyStore.addStrategy({ name, parameters: [] }); return request @@ -153,7 +173,8 @@ test('editable=true will allow delete request', t => { test('editable=true will allow edit request', t => { t.plan(0); const name = 'editStrat'; - const { request, base, strategyStore } = getSetup(); + const { request, base, strategyStore, perms } = getSetup(); + perms.withPerms(UPDATE_STRATEGY); strategyStore.addStrategy({ name, parameters: [] }); return request diff --git a/lib/routes/controller.js b/lib/routes/controller.js index 8ef578dc9c..44441f7044 100644 --- a/lib/routes/controller.js +++ b/lib/routes/controller.js @@ -1,28 +1,42 @@ 'use strict'; const { Router } = require('express'); +const { requirePerms } = require('./../permissions'); /** * Base class for Controllers to standardize binding to express Router. */ class Controller { - constructor() { + constructor(extendedPerms) { const router = Router(); this.app = router; + this.extendedPerms = extendedPerms; } - get(path, handler) { + get(path, handler, ...perms) { + if (this.extendedPerms && perms.length > 0) { + this.app.get(path, requirePerms(perms), handler.bind(this)); + } this.app.get(path, handler.bind(this)); } - post(path, handler) { + post(path, handler, ...perms) { + if (this.extendedPerms && perms.length > 0) { + this.app.post(path, requirePerms(perms), handler.bind(this)); + } this.app.post(path, handler.bind(this)); } - put(path, handler) { + put(path, handler, ...perms) { + if (this.extendedPerms && perms.length > 0) { + this.app.put(path, requirePerms(perms), handler.bind(this)); + } this.app.put(path, handler.bind(this)); } - delete(path, handler) { + delete(path, handler, ...perms) { + if (this.extendedPerms && perms.length > 0) { + this.app.delete(path, requirePerms(perms), handler.bind(this)); + } this.app.delete(path, handler.bind(this)); } diff --git a/lib/user.js b/lib/user.js index d070bfac22..ea0d24e983 100644 --- a/lib/user.js +++ b/lib/user.js @@ -4,7 +4,7 @@ const gravatar = require('gravatar'); const Joi = require('joi'); module.exports = class User { - constructor({ name, email, imageUrl } = {}) { + constructor({ name, email, permissions, imageUrl } = {}) { Joi.assert( email, Joi.string() @@ -14,6 +14,7 @@ module.exports = class User { ); this.email = email; this.name = name; + this.permissions = permissions; this.imageUrl = imageUrl || gravatar.url(email, { s: '42', d: 'retro' }); } diff --git a/test/fixtures/permissions.js b/test/fixtures/permissions.js new file mode 100644 index 0000000000..4f02db0e4c --- /dev/null +++ b/test/fixtures/permissions.js @@ -0,0 +1,17 @@ +'use strict'; + +module.exports = () => { + let _perms = []; + return { + hook(app) { + app.use((req, res, next) => { + if (req.user) req.user.permissions = _perms; + else req.user = { email: 'unknown', permissions: _perms }; + next(); + }); + }, + withPerms(...prms) { + _perms = prms; + }, + }; +};