Skip to content

Commit

Permalink
[Monitoring] Improve permissions required around setup mode (#50421)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
chrisronline committed Nov 18, 2019
1 parent bde2895 commit 66038f5
Show file tree
Hide file tree
Showing 14 changed files with 186 additions and 16 deletions.
22 changes: 21 additions & 1 deletion x-pack/legacy/plugins/monitoring/public/lib/setup_mode.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down
50 changes: 47 additions & 3 deletions x-pack/legacy/plugins/monitoring/public/lib/setup_mode.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ const angularStateMock = {
}
},
scope: {
$apply: fn => fn && fn()
$apply: fn => fn && fn(),
$evalAsync: fn => fn && fn()
}
};

Expand Down Expand Up @@ -123,19 +124,60 @@ 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') {
return true;
}
}
}));
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();
});
});
Expand All @@ -144,7 +186,8 @@ describe('setup_mode', () => {
const clusterUuid = '1ajy';
data = {
_meta: {
liveClusterUuid: clusterUuid
liveClusterUuid: clusterUuid,
hasPermissions: true
},
elasticsearch: {
byUuid: {
Expand All @@ -166,7 +209,8 @@ describe('setup_mode', () => {
const clusterUuid = '1ajy';
data = {
_meta: {
liveClusterUuid: clusterUuid
liveClusterUuid: clusterUuid,
hasPermissions: true
},
elasticsearch: {
byUuid: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]: {
Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
},
"_meta": {
"secondsAgo": 30,
"liveClusterUuid": null
"liveClusterUuid": null,
"hasPermissions": true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
},
"_meta": {
"secondsAgo": 30,
"liveClusterUuid": null
"liveClusterUuid": null,
"hasPermissions": true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
},
"_meta": {
"secondsAgo": 30,
"liveClusterUuid": null
"liveClusterUuid": null,
"hasPermissions": true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
},
"_meta": {
"secondsAgo": 30,
"liveClusterUuid": null
"liveClusterUuid": null,
"hasPermissions": true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
},
"_meta": {
"secondsAgo": 30,
"liveClusterUuid": null
"liveClusterUuid": null,
"hasPermissions": true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
},
"_meta": {
"secondsAgo": 30,
"liveClusterUuid": null
"liveClusterUuid": null,
"hasPermissions": true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
},
"_meta": {
"secondsAgo": 30,
"liveClusterUuid": null
"liveClusterUuid": null,
"hasPermissions": true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
},
"_meta": {
"secondsAgo": 30,
"liveClusterUuid": null
"liveClusterUuid": null,
"hasPermissions": true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
},
"_meta": {
"secondsAgo": 30,
"liveClusterUuid": null
"liveClusterUuid": null,
"hasPermissions": true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
});
}
Original file line number Diff line number Diff line change
@@ -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);
});
});
}

0 comments on commit 66038f5

Please sign in to comment.