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

[7.4] [Monitoring] Improve permissions required around setup mode (#50421) #50922

Merged
merged 2 commits into from
Nov 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
});
});
}