mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-04 00:18:01 +01:00
feature: Add action specific user permissions
This commit is contained in:
parent
59ef1d356b
commit
1af921c535
45
lib/permissions.js
Normal file
45
lib/permissions.js
Normal file
@ -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,
|
||||||
|
};
|
78
lib/permissions.test.js
Normal file
78
lib/permissions.test.js
Normal file
@ -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');
|
||||||
|
});
|
||||||
|
});
|
@ -5,15 +5,16 @@ const Controller = require('../controller');
|
|||||||
const logger = require('../../logger')('/admin-api/archive.js');
|
const logger = require('../../logger')('/admin-api/archive.js');
|
||||||
const { FEATURE_REVIVED } = require('../../event-type');
|
const { FEATURE_REVIVED } = require('../../event-type');
|
||||||
const extractUser = require('../../extract-user');
|
const extractUser = require('../../extract-user');
|
||||||
|
const { REVIVE_FEATURE } = require('../../permissions');
|
||||||
|
|
||||||
class ArchiveController extends Controller {
|
class ArchiveController extends Controller {
|
||||||
constructor({ featureToggleStore, eventStore }) {
|
constructor(extendedPerms, { featureToggleStore, eventStore }) {
|
||||||
super();
|
super(extendedPerms);
|
||||||
this.featureToggleStore = featureToggleStore;
|
this.featureToggleStore = featureToggleStore;
|
||||||
this.eventStore = eventStore;
|
this.eventStore = eventStore;
|
||||||
|
|
||||||
this.get('/features', this.getArchivedFeatures);
|
this.get('/features', this.getArchivedFeatures);
|
||||||
this.post('/revive/:name', this.reviveFeatureToggle);
|
this.post('/revive/:name', this.reviveFeatureToggle, REVIVE_FEATURE);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getArchivedFeatures(req, res) {
|
async getArchivedFeatures(req, res) {
|
||||||
|
@ -2,8 +2,10 @@
|
|||||||
|
|
||||||
const test = require('ava');
|
const test = require('ava');
|
||||||
const store = require('./../../../test/fixtures/store');
|
const store = require('./../../../test/fixtures/store');
|
||||||
|
const permissions = require('../../../test/fixtures/permissions');
|
||||||
const supertest = require('supertest');
|
const supertest = require('supertest');
|
||||||
const getApp = require('../../app');
|
const getApp = require('../../app');
|
||||||
|
const { REVIVE_FEATURE } = require('../../permissions');
|
||||||
|
|
||||||
const { EventEmitter } = require('events');
|
const { EventEmitter } = require('events');
|
||||||
const eventBus = new EventEmitter();
|
const eventBus = new EventEmitter();
|
||||||
@ -11,14 +13,18 @@ const eventBus = new EventEmitter();
|
|||||||
function getSetup() {
|
function getSetup() {
|
||||||
const base = `/random${Math.round(Math.random() * 1000)}`;
|
const base = `/random${Math.round(Math.random() * 1000)}`;
|
||||||
const stores = store.createStores();
|
const stores = store.createStores();
|
||||||
|
const perms = permissions();
|
||||||
const app = getApp({
|
const app = getApp({
|
||||||
baseUriPath: base,
|
baseUriPath: base,
|
||||||
stores,
|
stores,
|
||||||
eventBus,
|
eventBus,
|
||||||
|
extendedPermissions: true,
|
||||||
|
preRouterHook: perms.hook,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
base,
|
base,
|
||||||
|
perms,
|
||||||
archiveStore: stores.featureToggleStore,
|
archiveStore: stores.featureToggleStore,
|
||||||
eventStore: stores.eventStore,
|
eventStore: stores.eventStore,
|
||||||
request: supertest(app),
|
request: supertest(app),
|
||||||
@ -60,7 +66,8 @@ test('should get archived toggles via admin', t => {
|
|||||||
test('should revive toggle', t => {
|
test('should revive toggle', t => {
|
||||||
t.plan(0);
|
t.plan(0);
|
||||||
const name = 'name1';
|
const name = 'name1';
|
||||||
const { request, base, archiveStore } = getSetup();
|
const { request, base, archiveStore, perms } = getSetup();
|
||||||
|
perms.withPerms(REVIVE_FEATURE);
|
||||||
archiveStore.addArchivedFeature({
|
archiveStore.addArchivedFeature({
|
||||||
name,
|
name,
|
||||||
strategies: [{ name: 'default' }],
|
strategies: [{ name: 'default' }],
|
||||||
@ -72,7 +79,8 @@ test('should revive toggle', t => {
|
|||||||
test('should create event when reviving toggle', async t => {
|
test('should create event when reviving toggle', async t => {
|
||||||
t.plan(4);
|
t.plan(4);
|
||||||
const name = 'name1';
|
const name = 'name1';
|
||||||
const { request, base, archiveStore, eventStore } = getSetup();
|
const { request, base, archiveStore, eventStore, perms } = getSetup();
|
||||||
|
perms.withPerms(REVIVE_FEATURE);
|
||||||
archiveStore.addArchivedFeature({
|
archiveStore.addArchivedFeature({
|
||||||
name,
|
name,
|
||||||
strategies: [{ name: 'default' }],
|
strategies: [{ name: 'default' }],
|
||||||
|
@ -11,22 +11,27 @@ const {
|
|||||||
const NameExistsError = require('../../error/name-exists-error');
|
const NameExistsError = require('../../error/name-exists-error');
|
||||||
const { handleErrors } = require('./util');
|
const { handleErrors } = require('./util');
|
||||||
const extractUser = require('../../extract-user');
|
const extractUser = require('../../extract-user');
|
||||||
|
const {
|
||||||
|
UPDATE_FEATURE,
|
||||||
|
DELETE_FEATURE,
|
||||||
|
CREATE_FEATURE,
|
||||||
|
} = require('../../permissions');
|
||||||
const { featureShema, nameSchema } = require('./feature-schema');
|
const { featureShema, nameSchema } = require('./feature-schema');
|
||||||
const version = 1;
|
const version = 1;
|
||||||
|
|
||||||
class FeatureController extends Controller {
|
class FeatureController extends Controller {
|
||||||
constructor({ featureToggleStore, eventStore }) {
|
constructor(extendedPerms, { featureToggleStore, eventStore }) {
|
||||||
super();
|
super(extendedPerms);
|
||||||
this.featureToggleStore = featureToggleStore;
|
this.featureToggleStore = featureToggleStore;
|
||||||
this.eventStore = eventStore;
|
this.eventStore = eventStore;
|
||||||
|
|
||||||
this.get('/', this.getAllToggles);
|
this.get('/', this.getAllToggles);
|
||||||
this.post('/', this.createToggle);
|
this.post('/', this.createToggle, CREATE_FEATURE);
|
||||||
this.get('/:featureName', this.getToggle);
|
this.get('/:featureName', this.getToggle);
|
||||||
this.put('/:featureName', this.updateToggle);
|
this.put('/:featureName', this.updateToggle, UPDATE_FEATURE);
|
||||||
this.delete('/:featureName', this.deleteToggle);
|
this.delete('/:featureName', this.deleteToggle, DELETE_FEATURE);
|
||||||
this.post('/validate', this.validate);
|
this.post('/validate', this.validate);
|
||||||
this.post('/:featureName/toggle', this.toggle);
|
this.post('/:featureName/toggle', this.toggle, UPDATE_FEATURE);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllToggles(req, res) {
|
async getAllToggles(req, res) {
|
||||||
|
@ -2,8 +2,10 @@
|
|||||||
|
|
||||||
const test = require('ava');
|
const test = require('ava');
|
||||||
const store = require('./../../../test/fixtures/store');
|
const store = require('./../../../test/fixtures/store');
|
||||||
|
const permissions = require('../../../test/fixtures/permissions');
|
||||||
const supertest = require('supertest');
|
const supertest = require('supertest');
|
||||||
const getApp = require('../../app');
|
const getApp = require('../../app');
|
||||||
|
const { UPDATE_FEATURE, CREATE_FEATURE } = require('../../permissions');
|
||||||
|
|
||||||
const { EventEmitter } = require('events');
|
const { EventEmitter } = require('events');
|
||||||
const eventBus = new EventEmitter();
|
const eventBus = new EventEmitter();
|
||||||
@ -11,14 +13,18 @@ const eventBus = new EventEmitter();
|
|||||||
function getSetup() {
|
function getSetup() {
|
||||||
const base = `/random${Math.round(Math.random() * 1000)}`;
|
const base = `/random${Math.round(Math.random() * 1000)}`;
|
||||||
const stores = store.createStores();
|
const stores = store.createStores();
|
||||||
|
const perms = permissions();
|
||||||
const app = getApp({
|
const app = getApp({
|
||||||
baseUriPath: base,
|
baseUriPath: base,
|
||||||
stores,
|
stores,
|
||||||
eventBus,
|
eventBus,
|
||||||
|
extendedPermissions: true,
|
||||||
|
preRouterHook: perms.hook,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
base,
|
base,
|
||||||
|
perms,
|
||||||
featureToggleStore: stores.featureToggleStore,
|
featureToggleStore: stores.featureToggleStore,
|
||||||
request: supertest(app),
|
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 => {
|
test('should require at least one strategy when creating a feature toggle', t => {
|
||||||
t.plan(0);
|
t.plan(0);
|
||||||
const { request, base } = getSetup();
|
const { request, base, perms } = getSetup();
|
||||||
|
perms.withPerms(CREATE_FEATURE);
|
||||||
|
|
||||||
return request
|
return request
|
||||||
.post(`${base}/api/admin/features`)
|
.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 => {
|
test('should be allowed to use new toggle name', t => {
|
||||||
t.plan(0);
|
t.plan(0);
|
||||||
const { request, base } = getSetup();
|
const { request, base, perms } = getSetup();
|
||||||
|
perms.withPerms(CREATE_FEATURE);
|
||||||
|
|
||||||
return request
|
return request
|
||||||
.post(`${base}/api/admin/features/validate`)
|
.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 => {
|
test('should require at least one strategy when updating a feature toggle', t => {
|
||||||
t.plan(0);
|
t.plan(0);
|
||||||
const { request, featureToggleStore, base } = getSetup();
|
const { request, featureToggleStore, base, perms } = getSetup();
|
||||||
|
perms.withPerms(UPDATE_FEATURE);
|
||||||
featureToggleStore.addFeature({
|
featureToggleStore.addFeature({
|
||||||
name: 'ts',
|
name: 'ts',
|
||||||
strategies: [{ name: 'default' }],
|
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 => {
|
test('valid feature names should pass validation', t => {
|
||||||
t.plan(0);
|
t.plan(0);
|
||||||
const { request, base } = getSetup();
|
const { request, base, perms } = getSetup();
|
||||||
|
perms.withPerms(CREATE_FEATURE);
|
||||||
|
|
||||||
const validNames = [
|
const validNames = [
|
||||||
'com.example',
|
'com.example',
|
||||||
@ -179,7 +189,8 @@ test('valid feature names should pass validation', t => {
|
|||||||
|
|
||||||
test('invalid feature names should not pass validation', t => {
|
test('invalid feature names should not pass validation', t => {
|
||||||
t.plan(0);
|
t.plan(0);
|
||||||
const { request, base } = getSetup();
|
const { request, base, perms } = getSetup();
|
||||||
|
perms.withPerms(CREATE_FEATURE);
|
||||||
|
|
||||||
const invalidNames = [
|
const invalidNames = [
|
||||||
'some example',
|
'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.
|
// Make sure current UI works. Should align on joi errors in future.
|
||||||
test('invalid feature names should have error msg', t => {
|
test('invalid feature names should have error msg', t => {
|
||||||
t.plan(1);
|
t.plan(1);
|
||||||
const { request, base } = getSetup();
|
const { request, base, perms } = getSetup();
|
||||||
|
perms.withPerms(CREATE_FEATURE);
|
||||||
|
|
||||||
const name = 'ØÆ`';
|
const name = 'ØÆ`';
|
||||||
|
|
||||||
|
@ -14,13 +14,17 @@ class AdminApi extends Controller {
|
|||||||
super();
|
super();
|
||||||
|
|
||||||
const stores = config.stores;
|
const stores = config.stores;
|
||||||
|
const perms = config.extendedPermissions;
|
||||||
|
|
||||||
this.app.get('/', this.index);
|
this.app.get('/', this.index);
|
||||||
this.app.use('/features', new FeatureController(stores).router);
|
this.app.use('/features', new FeatureController(perms, stores).router);
|
||||||
this.app.use('/archive', new ArchiveController(stores).router);
|
this.app.use('/archive', new ArchiveController(perms, stores).router);
|
||||||
this.app.use('/strategies', new StrategyController(stores).router);
|
this.app.use(
|
||||||
|
'/strategies',
|
||||||
|
new StrategyController(perms, stores).router
|
||||||
|
);
|
||||||
this.app.use('/events', new EventController(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);
|
this.app.use('/user', new UserController().router);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
22
lib/routes/admin-api/metrics-schema.js
Normal file
22
lib/routes/admin-api/metrics-schema.js
Normal file
@ -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;
|
@ -1,18 +1,24 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
const joi = require('joi');
|
||||||
const Controller = require('../controller');
|
const Controller = require('../controller');
|
||||||
const logger = require('../../logger')('/admin-api/metrics.js');
|
const logger = require('../../logger')('/admin-api/metrics.js');
|
||||||
const ClientMetrics = require('../../client-metrics');
|
const ClientMetrics = require('../../client-metrics');
|
||||||
|
const schema = require('./metrics-schema');
|
||||||
|
const { UPDATE_APPLICATION } = require('../../permissions');
|
||||||
|
|
||||||
class MetricsController extends Controller {
|
class MetricsController extends Controller {
|
||||||
constructor({
|
constructor(
|
||||||
clientMetricsStore,
|
extendedPerms,
|
||||||
clientInstanceStore,
|
{
|
||||||
clientApplicationsStore,
|
clientMetricsStore,
|
||||||
strategyStore,
|
clientInstanceStore,
|
||||||
featureToggleStore,
|
clientApplicationsStore,
|
||||||
}) {
|
strategyStore,
|
||||||
super();
|
featureToggleStore,
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
super(extendedPerms);
|
||||||
this.metrics = new ClientMetrics(clientMetricsStore);
|
this.metrics = new ClientMetrics(clientMetricsStore);
|
||||||
this.clientInstanceStore = clientInstanceStore;
|
this.clientInstanceStore = clientInstanceStore;
|
||||||
this.clientApplicationsStore = clientApplicationsStore;
|
this.clientApplicationsStore = clientApplicationsStore;
|
||||||
@ -23,7 +29,11 @@ class MetricsController extends Controller {
|
|||||||
this.get('/seen-apps', this.getSeenApps);
|
this.get('/seen-apps', this.getSeenApps);
|
||||||
this.get('/feature-toggles', this.getFeatureToggles);
|
this.get('/feature-toggles', this.getFeatureToggles);
|
||||||
this.get('/feature-toggles/:name', this.getFeatureToggle);
|
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/', this.getApplications);
|
||||||
this.get('/applications/:appName', this.getApplication);
|
this.get('/applications/:appName', this.getApplication);
|
||||||
}
|
}
|
||||||
@ -67,14 +77,19 @@ class MetricsController extends Controller {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Todo: add joi-schema validation
|
|
||||||
async createApplication(req, res) {
|
async createApplication(req, res) {
|
||||||
const input = Object.assign({}, req.body, {
|
const input = Object.assign({}, req.body, {
|
||||||
appName: req.params.appName,
|
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 {
|
try {
|
||||||
await this.clientApplicationsStore.upsert(input);
|
await this.clientApplicationsStore.upsert(applicationData);
|
||||||
res.status(202).end();
|
res.status(202).end();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(err);
|
logger.error(err);
|
||||||
|
@ -2,23 +2,29 @@
|
|||||||
|
|
||||||
const test = require('ava');
|
const test = require('ava');
|
||||||
const store = require('./../../../test/fixtures/store');
|
const store = require('./../../../test/fixtures/store');
|
||||||
|
const permissions = require('../../../test/fixtures/permissions');
|
||||||
const supertest = require('supertest');
|
const supertest = require('supertest');
|
||||||
const getApp = require('../../app');
|
const getApp = require('../../app');
|
||||||
|
const { UPDATE_APPLICATION } = require('../../permissions');
|
||||||
|
|
||||||
const { EventEmitter } = require('events');
|
const { EventEmitter } = require('events');
|
||||||
const eventBus = new EventEmitter();
|
const eventBus = new EventEmitter();
|
||||||
|
|
||||||
function getSetup() {
|
function getSetup() {
|
||||||
const stores = store.createStores();
|
const stores = store.createStores();
|
||||||
|
const perms = permissions();
|
||||||
const app = getApp({
|
const app = getApp({
|
||||||
baseUriPath: '',
|
baseUriPath: '',
|
||||||
stores,
|
stores,
|
||||||
eventBus,
|
eventBus,
|
||||||
|
extendedPermissions: true,
|
||||||
|
preRouterHook: perms.hook,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
request: supertest(app),
|
request: supertest(app),
|
||||||
stores,
|
stores,
|
||||||
|
perms,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,11 +131,12 @@ test('should return applications', t => {
|
|||||||
|
|
||||||
test('should store application', t => {
|
test('should store application', t => {
|
||||||
t.plan(0);
|
t.plan(0);
|
||||||
const { request } = getSetup();
|
const { request, perms } = getSetup();
|
||||||
const appName = '123!23';
|
const appName = '123!23';
|
||||||
|
perms.withPerms(UPDATE_APPLICATION);
|
||||||
|
|
||||||
return request
|
return request
|
||||||
.post(`/api/admin/metrics/applications/${appName}`)
|
.post(`/api/admin/metrics/applications/${appName}`)
|
||||||
.send({ appName })
|
.send({ appName, strategies: ['default'] })
|
||||||
.expect(202);
|
.expect(202);
|
||||||
});
|
});
|
||||||
|
@ -8,19 +8,24 @@ const NameExistsError = require('../../error/name-exists-error');
|
|||||||
const extractUser = require('../../extract-user');
|
const extractUser = require('../../extract-user');
|
||||||
const strategySchema = require('./strategy-schema');
|
const strategySchema = require('./strategy-schema');
|
||||||
const { handleErrors } = require('./util');
|
const { handleErrors } = require('./util');
|
||||||
|
const {
|
||||||
|
DELETE_STRATEGY,
|
||||||
|
CREATE_STRATEGY,
|
||||||
|
UPDATE_STRATEGY,
|
||||||
|
} = require('../../permissions');
|
||||||
const version = 1;
|
const version = 1;
|
||||||
|
|
||||||
class StrategyController extends Controller {
|
class StrategyController extends Controller {
|
||||||
constructor({ strategyStore, eventStore }) {
|
constructor(extendedPerms, { strategyStore, eventStore }) {
|
||||||
super();
|
super(extendedPerms);
|
||||||
this.strategyStore = strategyStore;
|
this.strategyStore = strategyStore;
|
||||||
this.eventStore = eventStore;
|
this.eventStore = eventStore;
|
||||||
|
|
||||||
this.get('/', this.getAllStratgies);
|
this.get('/', this.getAllStratgies);
|
||||||
this.get('/:name', this.getStrategy);
|
this.get('/:name', this.getStrategy);
|
||||||
this.delete('/:name', this.removeStrategy);
|
this.delete('/:name', this.removeStrategy, DELETE_STRATEGY);
|
||||||
this.post('/', this.createStrategy);
|
this.post('/', this.createStrategy, CREATE_STRATEGY);
|
||||||
this.put('/:strategyName', this.updateStrategy);
|
this.put('/:strategyName', this.updateStrategy, UPDATE_STRATEGY);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllStratgies(req, res) {
|
async getAllStratgies(req, res) {
|
||||||
|
@ -2,25 +2,35 @@
|
|||||||
|
|
||||||
const test = require('ava');
|
const test = require('ava');
|
||||||
const store = require('./../../../test/fixtures/store');
|
const store = require('./../../../test/fixtures/store');
|
||||||
|
const permissions = require('../../../test/fixtures/permissions');
|
||||||
const supertest = require('supertest');
|
const supertest = require('supertest');
|
||||||
const getApp = require('../../app');
|
const getApp = require('../../app');
|
||||||
|
const {
|
||||||
|
DELETE_STRATEGY,
|
||||||
|
CREATE_STRATEGY,
|
||||||
|
UPDATE_STRATEGY,
|
||||||
|
} = require('../../permissions');
|
||||||
|
|
||||||
const { EventEmitter } = require('events');
|
const { EventEmitter } = require('events');
|
||||||
const eventBus = new EventEmitter();
|
const eventBus = new EventEmitter();
|
||||||
|
|
||||||
function getSetup() {
|
function getSetup() {
|
||||||
const base = `/random${Math.round(Math.random() * 1000)}`;
|
const base = `/random${Math.round(Math.random() * 1000)}`;
|
||||||
|
const perms = permissions();
|
||||||
const stores = store.createStores();
|
const stores = store.createStores();
|
||||||
const app = getApp({
|
const app = getApp({
|
||||||
baseUriPath: base,
|
baseUriPath: base,
|
||||||
stores,
|
stores,
|
||||||
eventBus,
|
eventBus,
|
||||||
|
extendedPermissions: true,
|
||||||
|
preRouterHook: perms.hook,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
base,
|
base,
|
||||||
strategyStore: stores.strategyStore,
|
strategyStore: stores.strategyStore,
|
||||||
request: supertest(app),
|
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 => {
|
test('require a name when creating a new stratey', t => {
|
||||||
t.plan(1);
|
t.plan(1);
|
||||||
const { request, base } = getSetup();
|
const { request, base, perms } = getSetup();
|
||||||
|
perms.withPerms(CREATE_STRATEGY);
|
||||||
|
|
||||||
return request
|
return request
|
||||||
.post(`${base}/api/admin/strategies`)
|
.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 => {
|
test('require parameters array when creating a new stratey', t => {
|
||||||
t.plan(1);
|
t.plan(1);
|
||||||
const { request, base } = getSetup();
|
const { request, base, perms } = getSetup();
|
||||||
|
perms.withPerms(CREATE_STRATEGY);
|
||||||
|
|
||||||
return request
|
return request
|
||||||
.post(`${base}/api/admin/strategies`)
|
.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 => {
|
test('create a new stratey with empty parameters', t => {
|
||||||
t.plan(0);
|
t.plan(0);
|
||||||
const { request, base } = getSetup();
|
const { request, base, perms } = getSetup();
|
||||||
|
perms.withPerms(CREATE_STRATEGY);
|
||||||
|
|
||||||
return request
|
return request
|
||||||
.post(`${base}/api/admin/strategies`)
|
.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 => {
|
test('not be possible to override name', t => {
|
||||||
t.plan(0);
|
t.plan(0);
|
||||||
const { request, base, strategyStore } = getSetup();
|
const { request, base, strategyStore, perms } = getSetup();
|
||||||
|
perms.withPerms(CREATE_STRATEGY);
|
||||||
strategyStore.addStrategy({ name: 'Testing', parameters: [] });
|
strategyStore.addStrategy({ name: 'Testing', parameters: [] });
|
||||||
|
|
||||||
return request
|
return request
|
||||||
@ -87,7 +101,8 @@ test('not be possible to override name', t => {
|
|||||||
test('update strategy', t => {
|
test('update strategy', t => {
|
||||||
t.plan(0);
|
t.plan(0);
|
||||||
const name = 'AnotherStrat';
|
const name = 'AnotherStrat';
|
||||||
const { request, base, strategyStore } = getSetup();
|
const { request, base, strategyStore, perms } = getSetup();
|
||||||
|
perms.withPerms(UPDATE_STRATEGY);
|
||||||
strategyStore.addStrategy({ name, parameters: [] });
|
strategyStore.addStrategy({ name, parameters: [] });
|
||||||
|
|
||||||
return request
|
return request
|
||||||
@ -96,10 +111,11 @@ test('update strategy', t => {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('not update uknown strategy', t => {
|
test('not update unknown strategy', t => {
|
||||||
t.plan(0);
|
t.plan(0);
|
||||||
const name = 'UnknownStrat';
|
const name = 'UnknownStrat';
|
||||||
const { request, base } = getSetup();
|
const { request, base, perms } = getSetup();
|
||||||
|
perms.withPerms(UPDATE_STRATEGY);
|
||||||
|
|
||||||
return request
|
return request
|
||||||
.put(`${base}/api/admin/strategies/${name}`)
|
.put(`${base}/api/admin/strategies/${name}`)
|
||||||
@ -110,7 +126,8 @@ test('not update uknown strategy', t => {
|
|||||||
test('validate format when updating strategy', t => {
|
test('validate format when updating strategy', t => {
|
||||||
t.plan(0);
|
t.plan(0);
|
||||||
const name = 'AnotherStrat';
|
const name = 'AnotherStrat';
|
||||||
const { request, base, strategyStore } = getSetup();
|
const { request, base, strategyStore, perms } = getSetup();
|
||||||
|
perms.withPerms(UPDATE_STRATEGY);
|
||||||
strategyStore.addStrategy({ name, parameters: [] });
|
strategyStore.addStrategy({ name, parameters: [] });
|
||||||
|
|
||||||
return request
|
return request
|
||||||
@ -122,7 +139,8 @@ test('validate format when updating strategy', t => {
|
|||||||
test('editable=false will stop delete request', t => {
|
test('editable=false will stop delete request', t => {
|
||||||
t.plan(0);
|
t.plan(0);
|
||||||
const name = 'default';
|
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);
|
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 => {
|
test('editable=false will stop edit request', t => {
|
||||||
t.plan(0);
|
t.plan(0);
|
||||||
const name = 'default';
|
const name = 'default';
|
||||||
const { request, base } = getSetup();
|
const { request, base, perms } = getSetup();
|
||||||
|
perms.withPerms(UPDATE_STRATEGY);
|
||||||
|
|
||||||
return request
|
return request
|
||||||
.put(`${base}/api/admin/strategies/${name}`)
|
.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 => {
|
test('editable=true will allow delete request', t => {
|
||||||
t.plan(0);
|
t.plan(0);
|
||||||
const name = 'deleteStrat';
|
const name = 'deleteStrat';
|
||||||
const { request, base, strategyStore } = getSetup();
|
const { request, base, strategyStore, perms } = getSetup();
|
||||||
|
perms.withPerms(DELETE_STRATEGY);
|
||||||
strategyStore.addStrategy({ name, parameters: [] });
|
strategyStore.addStrategy({ name, parameters: [] });
|
||||||
|
|
||||||
return request
|
return request
|
||||||
@ -153,7 +173,8 @@ test('editable=true will allow delete request', t => {
|
|||||||
test('editable=true will allow edit request', t => {
|
test('editable=true will allow edit request', t => {
|
||||||
t.plan(0);
|
t.plan(0);
|
||||||
const name = 'editStrat';
|
const name = 'editStrat';
|
||||||
const { request, base, strategyStore } = getSetup();
|
const { request, base, strategyStore, perms } = getSetup();
|
||||||
|
perms.withPerms(UPDATE_STRATEGY);
|
||||||
strategyStore.addStrategy({ name, parameters: [] });
|
strategyStore.addStrategy({ name, parameters: [] });
|
||||||
|
|
||||||
return request
|
return request
|
||||||
|
@ -1,28 +1,42 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const { Router } = require('express');
|
const { Router } = require('express');
|
||||||
|
const { requirePerms } = require('./../permissions');
|
||||||
/**
|
/**
|
||||||
* Base class for Controllers to standardize binding to express Router.
|
* Base class for Controllers to standardize binding to express Router.
|
||||||
*/
|
*/
|
||||||
class Controller {
|
class Controller {
|
||||||
constructor() {
|
constructor(extendedPerms) {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
this.app = 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));
|
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));
|
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));
|
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));
|
this.app.delete(path, handler.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ const gravatar = require('gravatar');
|
|||||||
const Joi = require('joi');
|
const Joi = require('joi');
|
||||||
|
|
||||||
module.exports = class User {
|
module.exports = class User {
|
||||||
constructor({ name, email, imageUrl } = {}) {
|
constructor({ name, email, permissions, imageUrl } = {}) {
|
||||||
Joi.assert(
|
Joi.assert(
|
||||||
email,
|
email,
|
||||||
Joi.string()
|
Joi.string()
|
||||||
@ -14,6 +14,7 @@ module.exports = class User {
|
|||||||
);
|
);
|
||||||
this.email = email;
|
this.email = email;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
|
this.permissions = permissions;
|
||||||
this.imageUrl =
|
this.imageUrl =
|
||||||
imageUrl || gravatar.url(email, { s: '42', d: 'retro' });
|
imageUrl || gravatar.url(email, { s: '42', d: 'retro' });
|
||||||
}
|
}
|
||||||
|
17
test/fixtures/permissions.js
vendored
Normal file
17
test/fixtures/permissions.js
vendored
Normal file
@ -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;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user