From ca87ab96de47d0c6f702c58acd3c12cebe5af36b Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Mon, 18 Nov 2019 12:00:55 -0500 Subject: [PATCH] [Monitoring] Improve permissions required around setup mode (#50421) (#50918) * 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 * Fix default value * PR feedback * Forgot to update this part * Fix tests --- .../monitoring/public/lib/setup_mode.js | 22 ++++++- .../monitoring/public/lib/setup_mode.test.js | 50 +++++++++++++++- .../setup/collection/get_collection_status.js | 42 ++++++++++++- .../setup/collection/fixtures/detect_apm.json | 3 +- .../collection/fixtures/detect_beats.json | 3 +- .../fixtures/detect_beats_management.json | 3 +- .../collection/fixtures/detect_logstash.json | 3 +- .../fixtures/detect_logstash_management.json | 3 +- .../fixtures/es_and_kibana_exclusive_mb.json | 3 +- .../collection/fixtures/es_and_kibana_mb.json | 3 +- .../fixtures/kibana_exclusive_mb.json | 3 +- .../setup/collection/fixtures/kibana_mb.json | 3 +- .../apis/monitoring/setup/collection/index.js | 1 + .../monitoring/setup/collection/security.js | 60 +++++++++++++++++++ 14 files changed, 186 insertions(+), 16 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 7da419719e70c7..607edbd1e8709f 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,26 @@ export const updateSetupModeData = async (uuid, fetchWithoutClusterUuid = false) const oldData = setupModeState.data; const data = await fetchCollectionData(uuid, fetchWithoutClusterUuid); setupModeState.data = data; - if (chrome.getInjected('isOnCloud')) { + + const isCloud = chrome.getInjected('isOnCloud'); + const hasPermissions = get(data, '_meta.hasPermissions', false); + if (isCloud || !hasPermissions) { + const text = !hasPermissions + ? 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 4e3a8045048ae6..39ed049ab74926 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,52 @@ describe('setup_mode', () => { } } })); + data = { + _meta: { + hasPermissions: true + } + }; + 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 = { + _meta: { + 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(); }); }); @@ -144,7 +186,8 @@ describe('setup_mode', () => { const clusterUuid = '1ajy'; data = { _meta: { - liveClusterUuid: clusterUuid + liveClusterUuid: clusterUuid, + hasPermissions: true }, elasticsearch: { byUuid: { @@ -166,7 +209,8 @@ describe('setup_mode', () => { const clusterUuid = '1ajy'; data = { _meta: { - liveClusterUuid: clusterUuid + liveClusterUuid: clusterUuid, + hasPermissions: true }, elasticsearch: { byUuid: { 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 cd5781ccf53446..d25d8af4aaa205 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,19 @@ 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'], + } + }); + // If there is some problem, assume they do not have access + return get(response, 'has_all_requested', false); +} + /** * Determines if we should ignore this bucket from this product. * @@ -316,6 +343,14 @@ 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 { + _meta: { + hasPermissions: false + } + }; + } const liveClusterUuid = skipLiveData ? null : await getLiveElasticsearchClusterUuid(req); const isLiveCluster = !clusterUuid || liveClusterUuid === clusterUuid; @@ -547,6 +582,7 @@ export const getCollectionStatus = async (req, indexPatterns, clusterUuid, nodeU status._meta = { secondsAgo: NUMBER_OF_SECONDS_AGO_TO_LOOK, liveClusterUuid, + hasPermissions, }; return status; diff --git a/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/detect_apm.json b/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/detect_apm.json index ddd8d8c9a1de66..d8852f9555ac14 100644 --- a/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/detect_apm.json +++ b/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/detect_apm.json @@ -51,6 +51,7 @@ }, "_meta": { "secondsAgo": 30, - "liveClusterUuid": null + "liveClusterUuid": null, + "hasPermissions": true } } diff --git a/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/detect_beats.json b/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/detect_beats.json index fcde71551a4f37..bf4d07f623c398 100644 --- a/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/detect_beats.json +++ b/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/detect_beats.json @@ -60,6 +60,7 @@ }, "_meta": { "secondsAgo": 30, - "liveClusterUuid": null + "liveClusterUuid": null, + "hasPermissions": true } } diff --git a/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/detect_beats_management.json b/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/detect_beats_management.json index 9186006415759e..61f9a460818104 100644 --- a/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/detect_beats_management.json +++ b/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/detect_beats_management.json @@ -51,6 +51,7 @@ }, "_meta": { "secondsAgo": 30, - "liveClusterUuid": null + "liveClusterUuid": null, + "hasPermissions": true } } diff --git a/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/detect_logstash.json b/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/detect_logstash.json index 7311230d108e98..aaa576d05c446c 100644 --- a/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/detect_logstash.json +++ b/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/detect_logstash.json @@ -51,6 +51,7 @@ }, "_meta": { "secondsAgo": 30, - "liveClusterUuid": null + "liveClusterUuid": null, + "hasPermissions": true } } diff --git a/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/detect_logstash_management.json b/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/detect_logstash_management.json index 7311230d108e98..aaa576d05c446c 100644 --- a/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/detect_logstash_management.json +++ b/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/detect_logstash_management.json @@ -51,6 +51,7 @@ }, "_meta": { "secondsAgo": 30, - "liveClusterUuid": null + "liveClusterUuid": null, + "hasPermissions": true } } diff --git a/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/es_and_kibana_exclusive_mb.json b/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/es_and_kibana_exclusive_mb.json index 69688bc46c27eb..a0f5c2617af072 100644 --- a/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/es_and_kibana_exclusive_mb.json +++ b/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/es_and_kibana_exclusive_mb.json @@ -71,6 +71,7 @@ }, "_meta": { "secondsAgo": 30, - "liveClusterUuid": null + "liveClusterUuid": null, + "hasPermissions": true } } diff --git a/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/es_and_kibana_mb.json b/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/es_and_kibana_mb.json index 8b207b418dae74..50a59ed5c7b410 100644 --- a/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/es_and_kibana_mb.json +++ b/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/es_and_kibana_mb.json @@ -71,6 +71,7 @@ }, "_meta": { "secondsAgo": 30, - "liveClusterUuid": null + "liveClusterUuid": null, + "hasPermissions": true } } diff --git a/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/kibana_exclusive_mb.json b/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/kibana_exclusive_mb.json index 319844a7e093d9..2022ef2a99f355 100644 --- a/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/kibana_exclusive_mb.json +++ b/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/kibana_exclusive_mb.json @@ -71,6 +71,7 @@ }, "_meta": { "secondsAgo": 30, - "liveClusterUuid": null + "liveClusterUuid": null, + "hasPermissions": true } } diff --git a/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/kibana_mb.json b/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/kibana_mb.json index f65436ce28616d..d0dc8d44b29c48 100644 --- a/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/kibana_mb.json +++ b/x-pack/test/api_integration/apis/monitoring/setup/collection/fixtures/kibana_mb.json @@ -71,6 +71,7 @@ }, "_meta": { "secondsAgo": 30, - "liveClusterUuid": null + "liveClusterUuid": null, + "hasPermissions": true } } 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 a88dc2f43c9b47..e5860bbc7ffc6f 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 00000000000000..420abe26103d05 --- /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._meta.hasPermissions).to.be(false); + }); + }); +}