Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Monitoring] Improve permissions required around setup mode #50421

Merged
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
}
};
}
chrisronline marked this conversation as resolved.
Show resolved Hide resolved
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);
});
});
}