mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-17 01:17:29 +02:00
fix: Addons should support sensitive params
This commit is contained in:
parent
17c8fe7710
commit
2bb38fe3e8
1
.github/workflows/build.yaml
vendored
1
.github/workflows/build.yaml
vendored
@ -37,6 +37,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
- run: yarn
|
- run: yarn
|
||||||
|
- run: yarn lint
|
||||||
- run: yarn run test:coverage
|
- run: yarn run test:coverage
|
||||||
env:
|
env:
|
||||||
CI: true
|
CI: true
|
||||||
|
@ -18,6 +18,7 @@ const addonDefinitionSchema = joi.object().keys({
|
|||||||
description: joi.string(),
|
description: joi.string(),
|
||||||
placeholder: joi.string().allow(''),
|
placeholder: joi.string().allow(''),
|
||||||
required: joi.boolean().default(false),
|
required: joi.boolean().default(false),
|
||||||
|
sensitive: joi.boolean().default(false),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
events: joi
|
events: joi
|
||||||
|
@ -24,6 +24,7 @@ module.exports = {
|
|||||||
'Add a new key at https://id.atlassian.com/manage-profile/security/api-tokens when logged in as the user you want Unleash to use',
|
'Add a new key at https://id.atlassian.com/manage-profile/security/api-tokens when logged in as the user you want Unleash to use',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
required: true,
|
required: true,
|
||||||
|
sensitive: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'user',
|
name: 'user',
|
||||||
|
@ -18,6 +18,7 @@ module.exports = {
|
|||||||
displayName: 'Slack webhook URL',
|
displayName: 'Slack webhook URL',
|
||||||
type: 'url',
|
type: 'url',
|
||||||
required: true,
|
required: true,
|
||||||
|
sensitive: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'username',
|
name: 'username',
|
||||||
|
@ -19,6 +19,7 @@ module.exports = {
|
|||||||
'(Required) Unleash will perform a HTTP Post to the specified URL (one retry if first attempt fails)',
|
'(Required) Unleash will perform a HTTP Post to the specified URL (one retry if first attempt fails)',
|
||||||
type: 'url',
|
type: 'url',
|
||||||
required: true,
|
required: true,
|
||||||
|
sensitive: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'contentType',
|
name: 'contentType',
|
||||||
|
@ -10,6 +10,8 @@ const SUPPORTED_EVENTS = Object.keys(events).map(k => events[k]);
|
|||||||
|
|
||||||
const ADDONS_CACHE_TIME = 60 * 1000; // 60s
|
const ADDONS_CACHE_TIME = 60 * 1000; // 60s
|
||||||
|
|
||||||
|
const MASKED_VALUE = '*****';
|
||||||
|
|
||||||
class AddonService {
|
class AddonService {
|
||||||
constructor(
|
constructor(
|
||||||
{ addonStore, eventStore, featureToggleStore },
|
{ addonStore, eventStore, featureToggleStore },
|
||||||
@ -23,7 +25,21 @@ class AddonService {
|
|||||||
this.logger = getLogger('services/addon-service.js');
|
this.logger = getLogger('services/addon-service.js');
|
||||||
this.tagTypeService = tagTypeService;
|
this.tagTypeService = tagTypeService;
|
||||||
|
|
||||||
this.addonProviders = addonProvidersClasses.reduce((map, Provider) => {
|
this.addonProviders = this.loadProviders(getLogger);
|
||||||
|
this.sensitiveParams = this.loadSensitiveParams(this.addonProviders);
|
||||||
|
if (addonStore) {
|
||||||
|
this.registerEventHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memoized private function
|
||||||
|
this.fetchAddonConfigs = memoize(
|
||||||
|
() => addonStore.getAll({ enabled: true }),
|
||||||
|
{ promise: true, maxAge: ADDONS_CACHE_TIME },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadProviders(getLogger) {
|
||||||
|
return addonProvidersClasses.reduce((map, Provider) => {
|
||||||
try {
|
try {
|
||||||
const provider = new Provider({ getLogger });
|
const provider = new Provider({ getLogger });
|
||||||
// eslint-disable-next-line no-param-reassign
|
// eslint-disable-next-line no-param-reassign
|
||||||
@ -33,15 +49,21 @@ class AddonService {
|
|||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
}, {});
|
}, {});
|
||||||
if (addonStore) {
|
}
|
||||||
this.registerEventHandler();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Memoized function
|
loadSensitiveParams(addonProviders) {
|
||||||
this.fetchAddonConfigs = memoize(
|
const providerDefinitions = Object.values(addonProviders).map(
|
||||||
() => addonStore.getAll({ enabled: true }),
|
p => p.definition,
|
||||||
{ promise: true, maxAge: ADDONS_CACHE_TIME },
|
|
||||||
);
|
);
|
||||||
|
return providerDefinitions.reduce((obj, definition) => {
|
||||||
|
const sensitiveParams = definition.parameters
|
||||||
|
.filter(p => p.sensitive)
|
||||||
|
.map(p => p.name);
|
||||||
|
|
||||||
|
const o = { ...obj };
|
||||||
|
o[definition.name] = sensitiveParams;
|
||||||
|
return o;
|
||||||
|
}, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
registerEventHandler() {
|
registerEventHandler() {
|
||||||
@ -67,12 +89,31 @@ class AddonService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Should be used by the controller.
|
||||||
async getAddons() {
|
async getAddons() {
|
||||||
return this.addonStore.getAll();
|
const addonConfigs = await this.addonStore.getAll();
|
||||||
|
return addonConfigs.map(a => this.filterSensitiveFields(a));
|
||||||
|
}
|
||||||
|
|
||||||
|
filterSensitiveFields(addonConfig) {
|
||||||
|
const { sensitiveParams } = this;
|
||||||
|
const a = { ...addonConfig };
|
||||||
|
a.parameters = Object.keys(a.parameters).reduce((obj, paramKey) => {
|
||||||
|
const o = { ...obj };
|
||||||
|
if (sensitiveParams[a.provider].includes(paramKey)) {
|
||||||
|
o[paramKey] = MASKED_VALUE;
|
||||||
|
} else {
|
||||||
|
o[paramKey] = a.parameters[paramKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
return o;
|
||||||
|
}, {});
|
||||||
|
return a;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAddon(id) {
|
async getAddon(id) {
|
||||||
return this.addonStore.get(id);
|
const addonConfig = await this.addonStore.get(id);
|
||||||
|
return this.filterSensitiveFields(addonConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
getProviderDefinition() {
|
getProviderDefinition() {
|
||||||
@ -122,6 +163,21 @@ class AddonService {
|
|||||||
|
|
||||||
async updateAddon(id, data, userName) {
|
async updateAddon(id, data, userName) {
|
||||||
const addonConfig = await addonSchema.validateAsync(data);
|
const addonConfig = await addonSchema.validateAsync(data);
|
||||||
|
if (this.sensitiveParams[addonConfig.provider].length > 0) {
|
||||||
|
const existingConfig = await this.addonStore.get(id);
|
||||||
|
addonConfig.parameters = Object.keys(addonConfig.parameters).reduce(
|
||||||
|
(params, key) => {
|
||||||
|
const o = { ...params };
|
||||||
|
if (addonConfig.parameters[key] === MASKED_VALUE) {
|
||||||
|
o[key] = existingConfig.parameters[key];
|
||||||
|
} else {
|
||||||
|
o[key] = addonConfig.parameters[key];
|
||||||
|
}
|
||||||
|
return o;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
}
|
||||||
await this.addonStore.update(id, addonConfig);
|
await this.addonStore.update(id, addonConfig);
|
||||||
await this.eventStore.store({
|
await this.eventStore.store({
|
||||||
type: events.ADDON_CONFIG_UPDATED,
|
type: events.ADDON_CONFIG_UPDATED,
|
||||||
|
@ -18,6 +18,8 @@ const {
|
|||||||
ADDON_CONFIG_DELETED,
|
ADDON_CONFIG_DELETED,
|
||||||
} = require('../event-type');
|
} = require('../event-type');
|
||||||
|
|
||||||
|
const MASKED_VALUE = '*****';
|
||||||
|
|
||||||
const definition = {
|
const definition = {
|
||||||
name: 'simple',
|
name: 'simple',
|
||||||
displayName: 'Simple ADdon',
|
displayName: 'Simple ADdon',
|
||||||
@ -36,6 +38,14 @@ const definition = {
|
|||||||
type: 'text',
|
type: 'text',
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'sensitiveParam',
|
||||||
|
displayName: 'Some sensitive param',
|
||||||
|
description: 'Some variable to inject',
|
||||||
|
type: 'text',
|
||||||
|
required: false,
|
||||||
|
sensitive: true,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
events: [
|
events: [
|
||||||
FEATURE_CREATED,
|
FEATURE_CREATED,
|
||||||
@ -259,3 +269,53 @@ test('should store ADDON_CONFIG_REMOVE event', async t => {
|
|||||||
t.is(events[2].type, ADDON_CONFIG_DELETED);
|
t.is(events[2].type, ADDON_CONFIG_DELETED);
|
||||||
t.is(events[2].data.id, addonConfig.id);
|
t.is(events[2].data.id, addonConfig.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should hide sensitive fields when fetching', async t => {
|
||||||
|
const { addonService } = getSetup();
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
provider: 'simple',
|
||||||
|
enabled: true,
|
||||||
|
parameters: {
|
||||||
|
url: 'http://localhost/wh',
|
||||||
|
var: 'some-value',
|
||||||
|
sensitiveParam: 'should be hidden when fetching',
|
||||||
|
},
|
||||||
|
events: [FEATURE_CREATED],
|
||||||
|
};
|
||||||
|
|
||||||
|
const createdConfig = await addonService.createAddon(config, 'me@mail.com');
|
||||||
|
const addons = await addonService.getAddons();
|
||||||
|
const addonRetrieved = await addonService.getAddon(createdConfig.id);
|
||||||
|
|
||||||
|
t.is(addons.length, 1);
|
||||||
|
t.is(addons[0].parameters.sensitiveParam, MASKED_VALUE);
|
||||||
|
t.is(addonRetrieved.parameters.sensitiveParam, MASKED_VALUE);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not overwrite masked values when updating', async t => {
|
||||||
|
const { addonService, stores } = getSetup();
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
provider: 'simple',
|
||||||
|
enabled: true,
|
||||||
|
parameters: {
|
||||||
|
url: 'http://localhost/wh',
|
||||||
|
var: 'some-value',
|
||||||
|
},
|
||||||
|
events: [FEATURE_CREATED],
|
||||||
|
};
|
||||||
|
|
||||||
|
const addonConfig = await addonService.createAddon(config, 'me@mail.com');
|
||||||
|
|
||||||
|
const updated = {
|
||||||
|
...addonConfig,
|
||||||
|
parameters: { url: MASKED_VALUE, var: 'some-new-value' },
|
||||||
|
description: 'test',
|
||||||
|
};
|
||||||
|
await addonService.updateAddon(addonConfig.id, updated, 'me@mail.com');
|
||||||
|
|
||||||
|
const updatedConfig = await stores.addonStore.get(addonConfig.id);
|
||||||
|
t.is(updatedConfig.parameters.url, 'http://localhost/wh');
|
||||||
|
t.is(updatedConfig.parameters.var, 'some-new-value');
|
||||||
|
});
|
||||||
|
@ -37,7 +37,6 @@
|
|||||||
"start:dev": "NODE_ENV=development supervisor --ignore ./node_modules/,website server-dev.js",
|
"start:dev": "NODE_ENV=development supervisor --ignore ./node_modules/,website server-dev.js",
|
||||||
"db-migrate": "db-migrate",
|
"db-migrate": "db-migrate",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"pretest": "yarn run lint",
|
|
||||||
"test": "NODE_ENV=test PORT=4243 ava",
|
"test": "NODE_ENV=test PORT=4243 ava",
|
||||||
"test:docker": "./scripts/docker-postgres.sh",
|
"test:docker": "./scripts/docker-postgres.sh",
|
||||||
"test:watch": "yarn test --watch",
|
"test:watch": "yarn test --watch",
|
||||||
|
@ -6,6 +6,8 @@ const dbInit = require('../../helpers/database-init');
|
|||||||
const { setupApp } = require('../../helpers/test-helper');
|
const { setupApp } = require('../../helpers/test-helper');
|
||||||
const getLogger = require('../../../fixtures/no-logger');
|
const getLogger = require('../../../fixtures/no-logger');
|
||||||
|
|
||||||
|
const MASKED_VALUE = '*****';
|
||||||
|
|
||||||
let stores;
|
let stores;
|
||||||
|
|
||||||
test.before(async () => {
|
test.before(async () => {
|
||||||
@ -84,7 +86,7 @@ test.serial('should delete addon configuration', async t => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test.serial('should update addon configuration', async t => {
|
test.serial('should update addon configuration', async t => {
|
||||||
t.plan(1);
|
t.plan(2);
|
||||||
const request = await setupApp(stores);
|
const request = await setupApp(stores);
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
@ -122,7 +124,11 @@ test.serial('should update addon configuration', async t => {
|
|||||||
.send(config)
|
.send(config)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.expect(r => {
|
.expect(r => {
|
||||||
t.is(r.body.parameters.url, updatedConfig.parameters.url);
|
t.is(r.body.parameters.url, MASKED_VALUE);
|
||||||
|
t.is(
|
||||||
|
r.body.parameters.bodyTemplate,
|
||||||
|
updatedConfig.parameters.bodyTemplate,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -145,7 +151,7 @@ test.serial('should not update with invalid addon configuration', async t => {
|
|||||||
.expect(400);
|
.expect(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
test.serial('should not update unknwn addon configuration', async t => {
|
test.serial('should not update unknown addon configuration', async t => {
|
||||||
t.plan(0);
|
t.plan(0);
|
||||||
const request = await setupApp(stores);
|
const request = await setupApp(stores);
|
||||||
|
|
||||||
@ -166,7 +172,7 @@ test.serial('should not update unknwn addon configuration', async t => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test.serial('should get addon configuration', async t => {
|
test.serial('should get addon configuration', async t => {
|
||||||
t.plan(1);
|
t.plan(3);
|
||||||
const request = await setupApp(stores);
|
const request = await setupApp(stores);
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
@ -191,10 +197,15 @@ test.serial('should get addon configuration', async t => {
|
|||||||
.expect(200)
|
.expect(200)
|
||||||
.expect(r => {
|
.expect(r => {
|
||||||
t.is(r.body.provider, config.provider);
|
t.is(r.body.provider, config.provider);
|
||||||
|
t.is(
|
||||||
|
r.body.parameters.bodyTemplate,
|
||||||
|
config.parameters.bodyTemplate,
|
||||||
|
);
|
||||||
|
t.is(r.body.parameters.url, MASKED_VALUE);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.serial('should not get unkown addon configuration', async t => {
|
test.serial('should not get unknown addon configuration', async t => {
|
||||||
t.plan(0);
|
t.plan(0);
|
||||||
const request = await setupApp(stores);
|
const request = await setupApp(stores);
|
||||||
|
|
||||||
|
2
test/fixtures/fake-addon-store.js
vendored
2
test/fixtures/fake-addon-store.js
vendored
@ -18,7 +18,7 @@ module.exports = () => {
|
|||||||
Promise.resolve();
|
Promise.resolve();
|
||||||
},
|
},
|
||||||
get: async id => {
|
get: async id => {
|
||||||
return _addons.find(id);
|
return _addons[id];
|
||||||
},
|
},
|
||||||
getAll: () => Promise.resolve(_addons),
|
getAll: () => Promise.resolve(_addons),
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user