Skip to content

Commit

Permalink
[X-Pack Usage API] use authentication from request headers (#19613)
Browse files Browse the repository at this point in the history
* [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
  • Loading branch information
tsullivan authored Jun 5, 2018
1 parent 194e427 commit 1617da9
Show file tree
Hide file tree
Showing 14 changed files with 77 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -42,6 +43,7 @@ export class CollectorSet {
this._interval = interval;
this._combineTypes = combineTypes;
this._onPayload = onPayload;
this._callClusterInternal = callClusterFactory(server).getCallClusterInternal();
}

/*
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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);
Expand All @@ -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) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}`,
Expand All @@ -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;
Expand All @@ -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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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));

Expand Down
4 changes: 1 addition & 3 deletions x-pack/plugins/reporting/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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));
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand All @@ -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 () => {
Expand All @@ -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 () => {
Expand All @@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
19 changes: 10 additions & 9 deletions x-pack/plugins/xpack_main/server/routes/api/v1/xpack_usage.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,36 +6,37 @@

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;
};

/*
* @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) {
server.route({
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({
Expand Down
4 changes: 4 additions & 0 deletions x-pack/test/api_integration/apis/xpack_main/usage/usage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
9 changes: 9 additions & 0 deletions x-pack/test/api_integration/services/usage_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down

0 comments on commit 1617da9

Please sign in to comment.