Skip to content

Commit

Permalink
Add error messages when setup mode is not enabled, disable it for use…
Browse files Browse the repository at this point in the history
…rs without the necessary permissions, and change one query to relax the privilege requirements
  • Loading branch information
chrisronline committed Nov 12, 2019
1 parent 19f7e99 commit b3455a0
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 5 deletions.
19 changes: 18 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,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);
Expand Down
37 changes: 36 additions & 1 deletion 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,53 @@ 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;
}
}
}));
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();
});
});
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,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.
*
Expand Down Expand Up @@ -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;

Expand Down
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.hasPermissions).to.be(false);
});
});
}

0 comments on commit b3455a0

Please sign in to comment.