Skip to content

Commit

Permalink
[7.4] [Monitoring] Improve permissions required around setup mode (#5…
Browse files Browse the repository at this point in the history
…0421) (#50922)

* Cherry-pick 66038f5

* Remove unnecessary field
  • Loading branch information
chrisronline committed Nov 18, 2019
1 parent cd625d2 commit 6d2191c
Show file tree
Hide file tree
Showing 13 changed files with 150 additions and 30 deletions.
23 changes: 22 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 @@ -6,6 +6,9 @@

import { ajaxErrorHandlersProvider } from './ajax_error_handler';
import { get } from 'lodash';
import chrome from 'ui/chrome';
import { toastNotifications } from 'ui/notify';
import { i18n } from '@kbn/i18n';

const angularState = {
injector: null,
Expand Down Expand Up @@ -75,7 +78,25 @@ export const updateSetupModeData = async (uuid, fetchWithoutClusterUuid = false)
const oldData = setupModeState.data;
const data = await fetchCollectionData(uuid, fetchWithoutClusterUuid);
setupModeState.data = data;
if (get(data, '_meta.isOnCloud', false)) {
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
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,22 @@ const getRecentMonitoringDocuments = async (req, indexPatterns, clusterUuid, nod
return await callWithRequest(req, 'search', params);
};

async function detectProducts(req) {
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]: {
doesExist: true,
Expand Down Expand Up @@ -181,11 +196,12 @@ async function detectProducts(req) {
}
];

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) {
result[id].mightExist = true;
if (isLiveCluster) {
for (const { id, indices } of detectionSearch) {
const exists = await doesIndexExist(req, indices.join(','));
if (exists) {
result[id].mightExist = true;
}
}
}

Expand Down Expand Up @@ -215,6 +231,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 @@ -308,6 +337,16 @@ 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;

const PRODUCTS = [
{ name: KIBANA_SYSTEM_ID },
Expand All @@ -322,12 +361,11 @@ export const getCollectionStatus = async (req, indexPatterns, clusterUuid, nodeU
detectedProducts
] = await Promise.all([
await getRecentMonitoringDocuments(req, indexPatterns, clusterUuid, nodeUuid),
await detectProducts(req)
await detectProducts(req, isLiveCluster)
]);

const liveClusterUuid = skipLiveData ? null : await getLiveElasticsearchClusterUuid(req);
const liveEsNodes = skipLiveData || (clusterUuid && liveClusterUuid !== clusterUuid) ? [] : await getLivesNodes(req);
const liveKibanaInstance = skipLiveData || (clusterUuid && liveClusterUuid !== clusterUuid) ? {} : await getLiveKibanaInstance(req);
const liveEsNodes = skipLiveData || !isLiveCluster ? [] : await getLivesNodes(req);
const liveKibanaInstance = skipLiveData || !isLiveCluster ? {} : await getLiveKibanaInstance(req);
const indicesBuckets = get(recentDocuments, 'aggregations.indices.buckets', []);
const liveClusterInternalCollectionEnabled = await getLiveElasticsearchCollectionEnabled(req);

Expand Down Expand Up @@ -527,7 +565,7 @@ export const getCollectionStatus = async (req, indexPatterns, clusterUuid, nodeU
status._meta = {
secondsAgo: NUMBER_OF_SECONDS_AGO_TO_LOOK,
liveClusterUuid,
isOnCloud: get(req.server.plugins, 'cloud.config.isCloudEnabled', false)
hasPermissions,
};

return status;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
},
"_meta": {
"secondsAgo": 30,
"isOnCloud": false,
"liveClusterUuid": null
"liveClusterUuid": null,
"hasPermissions": true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
},
"_meta": {
"secondsAgo": 30,
"isOnCloud": false,
"liveClusterUuid": null
"liveClusterUuid": null,
"hasPermissions": true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
},
"_meta": {
"secondsAgo": 30,
"isOnCloud": false,
"liveClusterUuid": null
"liveClusterUuid": null,
"hasPermissions": true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
},
"_meta": {
"secondsAgo": 30,
"isOnCloud": false,
"liveClusterUuid": null
"liveClusterUuid": null,
"hasPermissions": true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
},
"_meta": {
"secondsAgo": 30,
"isOnCloud": false,
"liveClusterUuid": null
"liveClusterUuid": null,
"hasPermissions": true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
},
"_meta": {
"secondsAgo": 30,
"isOnCloud": false,
"liveClusterUuid": null
"liveClusterUuid": null,
"hasPermissions": true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
},
"_meta": {
"secondsAgo": 30,
"isOnCloud": false,
"liveClusterUuid": null
"liveClusterUuid": null,
"hasPermissions": true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
},
"_meta": {
"secondsAgo": 30,
"isOnCloud": false,
"liveClusterUuid": null
"liveClusterUuid": null,
"hasPermissions": true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
},
"_meta": {
"secondsAgo": 30,
"isOnCloud": false,
"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 6d2191c

Please sign in to comment.