[X-Pack Usage API] use authentication from request headers (#19613) (#19682)

* [X-Pack Usage API] use authentication from request headers

* add test for usage api no-auth

* whitespace / syntax nits

* reduce loc changed

* remove a weird looking comment
This commit is contained in:
Tim Sullivan 2018-06-05 13:14:18 -07:00 committed by GitHub
parent 8396b1b866
commit 2579283f4c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 77 additions and 53 deletions

View file

@ -24,7 +24,14 @@ describe('CollectorSet', () => {
let fetch;
beforeEach(() => {
server = {
log: sinon.spy()
log: sinon.spy(),
plugins: {
elasticsearch: {
getCluster: () => ({
callWithInternalUser: sinon.spy() // this tests internal collection and bulk upload, not HTTP API
})
}
}
};
init = noop;
cleanup = noop;

View file

@ -12,8 +12,8 @@ export class Collector {
* @param {String} properties.type - property name as the key for the data
* @param {Function} properties.init (optional) - initialization function
* @param {Function} properties.fetch - function to query data
* @param {Function} properties.cleanup (optional) - cleanup function
* @param {Boolean} properties.fetchAfterInit (optional) - if collector should fetch immediately after init
* @param {Function} properties.cleanup (optional) - cleanup function -- TODO remove this, handle it in the collector itself
* @param {Boolean} properties.fetchAfterInit (optional) - if collector should fetch immediately after init -- TODO remove this, not useful
*/
constructor(server, { type, init, fetch, cleanup, fetchAfterInit }) {
this.type = type;
@ -24,4 +24,11 @@ export class Collector {
this.log = getCollectorLogger(server);
}
fetchInternal(callCluster) {
if (typeof callCluster !== 'function') {
throw new Error('A `callCluster` function must be passed to the fetch methods of collectors');
}
return this.fetch(callCluster);
}
}

View file

@ -4,6 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { callClusterFactory } from '../../../../xpack_main';
import { flatten, isEmpty } from 'lodash';
import Promise from 'bluebird';
import { getCollectorLogger } from '../lib';
@ -42,6 +43,7 @@ export class CollectorSet {
this._interval = interval;
this._combineTypes = combineTypes;
this._onPayload = onPayload;
this._callClusterInternal = callClusterFactory(server).getCallClusterInternal();
}
/*
@ -78,16 +80,16 @@ export class CollectorSet {
// do some fetches and bulk collect
if (initialCollectors.length > 0) {
this._fetchAndUpload(initialCollectors);
this._fetchAndUpload(this._callClusterInternal, initialCollectors);
}
this._timer = setInterval(() => {
this._fetchAndUpload(this._collectors);
this._fetchAndUpload(this._callClusterInternal, this._collectors);
}, this._interval);
}
async _fetchAndUpload(collectors) {
const data = await this._bulkFetch(collectors);
async _fetchAndUpload(callCluster, collectors) {
const data = await this._bulkFetch(callCluster, collectors);
const usableData = data.filter(d => Boolean(d) && !isEmpty(d.result));
const payload = usableData.map(({ result, type }) => {
if (!isEmpty(result)) {
@ -115,13 +117,13 @@ export class CollectorSet {
/*
* Call a bunch of fetch methods and then do them in bulk
*/
_bulkFetch(collectors) {
_bulkFetch(callCluster, collectors) {
return Promise.map(collectors, collector => {
const collectorType = collector.type;
this._log.debug(`Fetching data from ${collectorType} collector`);
return Promise.props({
type: collectorType,
result: collector.fetch()
result: collector.fetchInternal(callCluster) // use the wrapper for fetch, kicks in error checking
})
.catch(err => {
this._log.warn(err);
@ -130,9 +132,9 @@ export class CollectorSet {
});
}
async bulkFetchUsage() {
async bulkFetchUsage(callCluster) {
const usageCollectors = this._collectors.filter(c => c instanceof UsageCollector);
const bulk = await this._bulkFetch(usageCollectors);
const bulk = await this._bulkFetch(callCluster, usageCollectors);
// summarize each type of stat
return bulk.reduce((accumulatedStats, currentStat) => {

View file

@ -30,7 +30,7 @@ describe('getKibanaUsageCollector', () => {
});
it('correctly defines usage collector.', () => {
const usageCollector = getKibanaUsageCollector(serverStub, callClusterStub);
const usageCollector = getKibanaUsageCollector(serverStub);
expect(usageCollector.type).to.be('kibana');
expect(usageCollector.fetch).to.be.a(Function);
@ -45,8 +45,8 @@ describe('getKibanaUsageCollector', () => {
}
});
const usageCollector = getKibanaUsageCollector(serverStub, callClusterStub);
await usageCollector.fetch();
const usageCollector = getKibanaUsageCollector(serverStub);
await usageCollector.fetch(callClusterStub);
sinon.assert.calledOnce(clusterStub.callWithInternalUser);
sinon.assert.calledWithExactly(clusterStub.callWithInternalUser, 'search', sinon.match({

View file

@ -20,10 +20,10 @@ const TYPES = [
/**
* Fetches saved object client counts by querying the saved object index
*/
export function getKibanaUsageCollector(server, callCluster) {
export function getKibanaUsageCollector(server) {
return new UsageCollector(server, {
type: KIBANA_USAGE_TYPE,
async fetch() {
async fetch(callCluster) {
const index = server.config().get('kibana.index');
const savedObjectCountSearchParams = {
index,

View file

@ -13,14 +13,14 @@ import { Collector } from '../classes/collector';
* Check if Cluster Alert email notifications is enabled in config
* If so, use uiSettings API to fetch the X-Pack default admin email
*/
export async function getDefaultAdminEmail(config, callWithInternalUser) {
export async function getDefaultAdminEmail(config, callCluster) {
if (!config.get('xpack.monitoring.cluster_alerts.email_notifications.enabled')) {
return null;
}
const index = config.get('kibana.index');
const version = config.get('pkg.version');
const uiSettingsDoc = await callWithInternalUser('get', {
const uiSettingsDoc = await callCluster('get', {
index,
type: 'doc',
id: `config:${version}`,
@ -35,11 +35,11 @@ let shouldUseNull = true;
export async function checkForEmailValue(
config,
callWithInternalUser,
callCluster,
_shouldUseNull = shouldUseNull,
_getDefaultAdminEmail = getDefaultAdminEmail
) {
const defaultAdminEmail = await _getDefaultAdminEmail(config, callWithInternalUser);
const defaultAdminEmail = await _getDefaultAdminEmail(config, callCluster);
// Allow null so clearing the advanced setting will be reflected in the data
const isAcceptableNull = defaultAdminEmail === null && _shouldUseNull;
@ -55,14 +55,13 @@ export async function checkForEmailValue(
}
export function getSettingsCollector(server) {
const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin');
const config = server.config();
return new Collector(server, {
type: KIBANA_SETTINGS_TYPE,
async fetch() {
async fetch(callCluster) {
let kibanaSettingsData;
const defaultAdminEmail = await checkForEmailValue(config, callWithInternalUser);
const defaultAdminEmail = await checkForEmailValue(config, callCluster);
// skip everything if defaultAdminEmail === undefined
if (defaultAdminEmail || (defaultAdminEmail === null && shouldUseNull)) {

View file

@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { callClusterFactory } from '../../../xpack_main';
import { CollectorSet } from './classes/collector_set';
import { getOpsStatsCollector } from './collectors/get_ops_stats_collector';
import { getSettingsCollector } from './collectors/get_settings_collector';
@ -33,9 +32,8 @@ export function startCollectorSet(kbnServer, server, client, _sendBulkPayload =
return _sendBulkPayload(client, interval, payload);
}
});
const callCluster = callClusterFactory(server).getCallClusterInternal();
collectorSet.register(getKibanaUsageCollector(server, callCluster));
collectorSet.register(getKibanaUsageCollector(server));
collectorSet.register(getOpsStatsCollector(server));
collectorSet.register(getSettingsCollector(server));

View file

@ -18,7 +18,6 @@ import { exportTypesRegistryFactory } from './server/lib/export_types_registry';
import { createBrowserDriverFactory, getDefaultBrowser, getDefaultChromiumSandboxDisabled } from './server/browsers';
import { logConfiguration } from './log_configuration';
import { callClusterFactory } from '../xpack_main';
import { getReportingUsageCollector } from './server/usage';
const kbToBase64Length = (kb) => {
@ -160,8 +159,7 @@ export const reporting = (kibana) => {
// Register a function to with Monitoring to manage the collection of usage stats
monitoringPlugin && monitoringPlugin.status.once('green', () => {
if (monitoringPlugin.collectorSet) {
const callCluster = callClusterFactory(server).getCallClusterInternal(); // uses callWithInternal as this is for internal collection
monitoringPlugin.collectorSet.register(getReportingUsageCollector(server, callCluster));
monitoringPlugin.collectorSet.register(getReportingUsageCollector(server));
}
});

View file

@ -112,13 +112,12 @@ async function getReportingUsageWithinRange(callCluster, server, reportingAvaila
/*
* @param {Object} server
* @param {Function} callCluster - function that uses either callWithRequest or callWithInternal to fetch data from ES
* @return {Object} kibana usage stats type collection object
*/
export function getReportingUsageCollector(server, callCluster) {
export function getReportingUsageCollector(server) {
return new UsageCollector(server, {
type: KIBANA_REPORTING_TYPE,
fetch: async () => {
fetch: async callCluster => {
const xpackInfo = server.plugins.xpack_main.info;
const config = server.config();
const available = xpackInfo && xpackInfo.isAvailable(); // some form of reporting (csv at least) is available for all valid licenses

View file

@ -52,8 +52,8 @@ test('sets enabled to false when reporting is turned off', async () => {
});
const serverMock = getServerMock({ config: () => ({ get: mockConfigGet }) });
const callClusterMock = jest.fn();
const { fetch: getReportingUsage } = getReportingUsageCollector(serverMock, callClusterMock);
const usageStats = await getReportingUsage();
const { fetch: getReportingUsage } = getReportingUsageCollector(serverMock);
const usageStats = await getReportingUsage(callClusterMock);
expect(usageStats.enabled).toBe(false);
});
@ -63,8 +63,8 @@ describe('with a basic license', async () => {
const serverWithBasicLicenseMock = getServerMock();
serverWithBasicLicenseMock.plugins.xpack_main.info.license.getType = sinon.stub().returns('basic');
const callClusterMock = jest.fn(() => Promise.resolve({}));
const { fetch: getReportingUsage } = getReportingUsageCollector(serverWithBasicLicenseMock, callClusterMock);
usageStats = await getReportingUsage();
const { fetch: getReportingUsage } = getReportingUsageCollector(serverWithBasicLicenseMock);
usageStats = await getReportingUsage(callClusterMock);
});
test('sets enables to true', async () => {
@ -86,8 +86,8 @@ describe('with no license', async () => {
const serverWithNoLicenseMock = getServerMock();
serverWithNoLicenseMock.plugins.xpack_main.info.license.getType = sinon.stub().returns('none');
const callClusterMock = jest.fn(() => Promise.resolve({}));
const { fetch: getReportingUsage } = getReportingUsageCollector(serverWithNoLicenseMock, callClusterMock);
usageStats = await getReportingUsage();
const { fetch: getReportingUsage } = getReportingUsageCollector(serverWithNoLicenseMock);
usageStats = await getReportingUsage(callClusterMock);
});
test('sets enables to true', async () => {
@ -109,8 +109,8 @@ describe('with platinum license', async () => {
const serverWithPlatinumLicenseMock = getServerMock();
serverWithPlatinumLicenseMock.plugins.xpack_main.info.license.getType = sinon.stub().returns('platinum');
const callClusterMock = jest.fn(() => Promise.resolve({}));
const { fetch: getReportingUsage } = getReportingUsageCollector(serverWithPlatinumLicenseMock, callClusterMock);
usageStats = await getReportingUsage();
const { fetch: getReportingUsage } = getReportingUsageCollector(serverWithPlatinumLicenseMock);
usageStats = await getReportingUsage(callClusterMock);
});
test('sets enables to true', async () => {

View file

@ -22,12 +22,12 @@ export function kibanaStatsRoute(server) {
const callCluster = callClusterFactory(server).getCallClusterWithReq(req);
try {
const kibanaUsageCollector = getKibanaUsageCollector(server, callCluster);
const reportingUsageCollector = getReportingUsageCollector(server, callCluster);
const kibanaUsageCollector = getKibanaUsageCollector(server);
const reportingUsageCollector = getReportingUsageCollector(server);
const [ kibana, reporting ] = await Promise.all([
kibanaUsageCollector.fetch(),
reportingUsageCollector.fetch(),
kibanaUsageCollector.fetch(callCluster),
reportingUsageCollector.fetch(callCluster),
]);
reply({

View file

@ -6,10 +6,8 @@
import { wrap, serverTimeout as serverUnavailable } from 'boom';
const getClusterUuid = async req => {
const { server } = req;
const { callWithRequest, } = server.plugins.elasticsearch.getCluster('data');
const { cluster_uuid: uuid } = await callWithRequest(req, 'info', { filterPath: 'cluster_uuid', });
const getClusterUuid = async callCluster => {
const { cluster_uuid: uuid } = await callCluster('info', { filterPath: 'cluster_uuid', });
return uuid;
};
@ -17,14 +15,13 @@ const getClusterUuid = async req => {
* @return {Object} data from usage stats collectors registered with Monitoring CollectorSet
* @throws {Error} if the Monitoring CollectorSet is not ready
*/
const getUsage = async req => {
const server = req.server;
const getUsage = async (callCluster, server) => {
const { collectorSet } = server.plugins.monitoring;
if (collectorSet === undefined) {
const error = new Error('CollectorSet from Monitoring plugin is not ready for collecting usage'); // moving kibana_monitoring lib to xpack_main will make this unnecessary
throw serverUnavailable(error);
}
return collectorSet.bulkFetchUsage();
return collectorSet.bulkFetchUsage(callCluster);
};
export function xpackUsageRoute(server) {
@ -32,10 +29,14 @@ export function xpackUsageRoute(server) {
path: '/api/_xpack/usage',
method: 'GET',
async handler(req, reply) {
const { server } = req;
const { callWithRequest } = server.plugins.elasticsearch.getCluster('data');
const callCluster = (...args) => callWithRequest(req, ...args); // All queries from HTTP API must use authentication headers from the request
try {
const [ clusterUuid, xpackUsage ] = await Promise.all([
getClusterUuid(req),
getUsage(req),
getClusterUuid(callCluster),
getUsage(callCluster, server),
]);
reply({

View file

@ -19,6 +19,10 @@ export default function ({ getService }) {
await esArchiver.unload('../../../../test/functional/fixtures/es_archiver/dashboard/current/kibana');
});
it('should reject without authentication headers passed', async () => {
const rejected = await usageAPI.getUsageStatsNoAuth();
expect(rejected).to.eql({ statusCode: 401, error: 'Unauthorized' });
});
it('should return xpack usage data', async () => {
const usage = await usageAPI.getUsageStats();

View file

@ -7,8 +7,17 @@
export function UsageAPIProvider({ getService }) {
const supertest = getService('supertest');
const supertestNoAuth = getService('supertestWithoutAuth');
return {
async getUsageStatsNoAuth() {
const { body } = await supertestNoAuth
.get('/api/_xpack/usage')
.set('kbn-xsrf', 'xxx')
.expect(401);
return body;
},
async getUsageStats() {
const { body } = await supertest
.get('/api/_xpack/usage')