From b3455a0c7c6f1ea81fce97b7ffa305a81c6ebe8a Mon Sep 17 00:00:00 2001 From: chrisronline Date: Tue, 12 Nov 2019 14:31:45 -0500 Subject: [PATCH] Add error messages when setup mode is not enabled, disable it for users without the necessary permissions, and change one query to relax the privilege requirements --- .../monitoring/public/lib/setup_mode.js | 19 +++++- .../monitoring/public/lib/setup_mode.test.js | 37 +++++++++++- .../setup/collection/get_collection_status.js | 38 +++++++++++- .../apis/monitoring/setup/collection/index.js | 1 + .../monitoring/setup/collection/security.js | 60 +++++++++++++++++++ 5 files changed, 150 insertions(+), 5 deletions(-) create mode 100644 x-pack/test/api_integration/apis/monitoring/setup/collection/security.js diff --git a/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.js b/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.js index 7da419719e70c..178820415d2a9 100644 --- a/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.js +++ b/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.js @@ -7,6 +7,7 @@ import { ajaxErrorHandlersProvider } from './ajax_error_handler'; import { get, contains } from 'lodash'; import chrome from 'ui/chrome'; +import { toastNotifications } from 'ui/notify'; import { i18n } from '@kbn/i18n'; function isOnPage(hash) { @@ -81,7 +82,23 @@ export const updateSetupModeData = async (uuid, fetchWithoutClusterUuid = false) const oldData = setupModeState.data; const data = await fetchCollectionData(uuid, fetchWithoutClusterUuid); setupModeState.data = data; - if (chrome.getInjected('isOnCloud')) { + if (chrome.getInjected('isOnCloud') || data.hasPermissions === false) { + const text = data.hasPermissions === false + ? i18n.translate('xpack.monitoring.setupMode.notAvailablePermissions', { + defaultMessage: 'You do not have the necessary permissions to do this.' + }) + : i18n.translate('xpack.monitoring.setupMode.notAvailableCloud', { + defaultMessage: 'This feature is not available on cloud.' + }); + + angularState.scope.$evalAsync(() => { + toastNotifications.addDanger({ + title: i18n.translate('xpack.monitoring.setupMode.notAvailableTitle', { + defaultMessage: 'Setup mode is not available' + }), + text, + }); + }); return toggleSetupMode(false); // eslint-disable-line no-use-before-define } notifySetupModeDataChange(oldData); diff --git a/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.test.js b/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.test.js index 4e3a8045048ae..5c66b633363e3 100644 --- a/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.test.js +++ b/x-pack/legacy/plugins/monitoring/public/lib/setup_mode.test.js @@ -40,7 +40,8 @@ const angularStateMock = { } }, scope: { - $apply: fn => fn && fn() + $apply: fn => fn && fn(), + $evalAsync: fn => fn && fn() } }; @@ -123,6 +124,7 @@ describe('setup_mode', () => { }); it('should not fetch data if on cloud', async (done) => { + const addDanger = jest.fn(); jest.doMock('ui/chrome', () => ({ getInjected: (key) => { if (key === 'isOnCloud') { @@ -130,12 +132,45 @@ describe('setup_mode', () => { } } })); + jest.doMock('ui/notify', () => ({ + toastNotifications: { + addDanger, + } + })); + setModules(); + initSetupModeState(angularStateMock.scope, angularStateMock.injector); + await toggleSetupMode(true); + waitForSetupModeData(() => { + const state = getSetupModeState(); + expect(state.enabled).toBe(false); + expect(addDanger).toHaveBeenCalledWith({ + title: 'Setup mode is not available', + text: 'This feature is not available on cloud.' + }); + done(); + }); + }); + + it('should not fetch data if the user does not have sufficient permissions', async (done) => { + const addDanger = jest.fn(); + jest.doMock('ui/notify', () => ({ + toastNotifications: { + addDanger, + } + })); + data = { + hasPermissions: false + }; setModules(); initSetupModeState(angularStateMock.scope, angularStateMock.injector); await toggleSetupMode(true); waitForSetupModeData(() => { const state = getSetupModeState(); expect(state.enabled).toBe(false); + expect(addDanger).toHaveBeenCalledWith({ + title: 'Setup mode is not available', + text: 'You do not have the necessary permissions to do this.' + }); done(); }); }); diff --git a/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/get_collection_status.js b/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/get_collection_status.js index cd5781ccf5344..9ca8f5a6c489a 100644 --- a/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/get_collection_status.js +++ b/x-pack/legacy/plugins/monitoring/server/lib/setup/collection/get_collection_status.js @@ -145,6 +145,21 @@ const getRecentMonitoringDocuments = async (req, indexPatterns, clusterUuid, nod return await callWithRequest(req, 'search', params); }; +async function doesIndexExist(req, index) { + const params = { + index, + size: 0, + terminate_after: 1, + ignoreUnavailable: true, + filterPath: [ + 'hits.total.value' + ], + }; + const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('monitoring'); + const response = await callWithRequest(req, 'search', params); + return get(response, 'hits.total.value', 0) > 0; +} + async function detectProducts(req, isLiveCluster) { const result = { [KIBANA_SYSTEM_ID]: { @@ -188,10 +203,9 @@ async function detectProducts(req, isLiveCluster) { ]; if (isLiveCluster) { - const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('data'); for (const { id, indices } of detectionSearch) { - const response = await callWithRequest(req, 'cat.indices', { index: indices, format: 'json' }); - if (response.length) { + const exists = await doesIndexExist(req, indices.join(',')); + if (exists) { result[id].mightExist = true; } } @@ -223,6 +237,18 @@ function isBeatFromAPM(bucket) { return get(beatType, 'buckets[0].key') === 'apm-server'; } +async function hasNecessaryPermissions(req) { + const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('data'); + const response = await callWithRequest(req, 'transport.request', { + method: 'POST', + path: '/_security/user/_has_privileges', + body: { + cluster: ['monitor'], + } + }); + return get(response, 'has_all_requested', true); +} + /** * Determines if we should ignore this bucket from this product. * @@ -316,6 +342,12 @@ async function getLiveElasticsearchCollectionEnabled(req) { export const getCollectionStatus = async (req, indexPatterns, clusterUuid, nodeUuid, skipLiveData) => { const config = req.server.config(); const kibanaUuid = config.get('server.uuid'); + const hasPermissions = await hasNecessaryPermissions(req); + if (!hasPermissions) { + return { + hasPermissions: false + }; + } const liveClusterUuid = skipLiveData ? null : await getLiveElasticsearchClusterUuid(req); const isLiveCluster = !clusterUuid || liveClusterUuid === clusterUuid; diff --git a/x-pack/test/api_integration/apis/monitoring/setup/collection/index.js b/x-pack/test/api_integration/apis/monitoring/setup/collection/index.js index a88dc2f43c9b4..e5860bbc7ffc6 100644 --- a/x-pack/test/api_integration/apis/monitoring/setup/collection/index.js +++ b/x-pack/test/api_integration/apis/monitoring/setup/collection/index.js @@ -15,5 +15,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./detect_logstash')); loadTestFile(require.resolve('./detect_logstash_management')); loadTestFile(require.resolve('./detect_apm')); + loadTestFile(require.resolve('./security')); }); } diff --git a/x-pack/test/api_integration/apis/monitoring/setup/collection/security.js b/x-pack/test/api_integration/apis/monitoring/setup/collection/security.js new file mode 100644 index 0000000000000..1850580fd9f39 --- /dev/null +++ b/x-pack/test/api_integration/apis/monitoring/setup/collection/security.js @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +export default function ({ getService }) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + describe('security', () => { + const archive = 'monitoring/setup/collection/kibana_exclusive_mb'; + const timeRange = { + min: '2019-04-09T00:00:00.741Z', + max: '2019-04-09T23:59:59.741Z' + }; + + before('load archive', () => { + return esArchiver.load(archive); + }); + + after('unload archive', () => { + return esArchiver.unload(archive); + }); + + it('should allow access to elevated user', async () => { + const { body } = await supertest + .post('/api/monitoring/v1/setup/collection/cluster?skipLiveData=true') + .set('kbn-xsrf', 'xxx') + .send({ timeRange }) + .expect(200); + + expect(body.hasPermissions).to.not.be(false); + }); + + it('should say permission denied for limited user', async () => { + const username = 'limited_user'; + const password = 'changeme'; + + await security.user.create(username, { + password: password, + full_name: 'Limited User', + roles: ['kibana_user', 'monitoring_user'] + }); + + const { body } = await supertestWithoutAuth + .post('/api/monitoring/v1/setup/collection/cluster?skipLiveData=true') + .auth(username, password) + .set('kbn-xsrf', 'xxx') + .send({ timeRange }) + .expect(200); + + expect(body.hasPermissions).to.be(false); + }); + }); +}