mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
377 lines
14 KiB
TypeScript
377 lines
14 KiB
TypeScript
/*
|
|
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
|
* or more contributor license agreements. Licensed under the Elastic License
|
|
* 2.0; you may not use this file except in compliance with the Elastic License
|
|
* 2.0.
|
|
*/
|
|
|
|
import expect from '@kbn/expect';
|
|
import moment from 'moment';
|
|
import type SuperTest from 'supertest';
|
|
import deepmerge from 'deepmerge';
|
|
|
|
import ossRootTelemetrySchema from '@kbn/telemetry-plugin/schema/oss_root.json';
|
|
import xpackRootTelemetrySchema from '@kbn/telemetry-collection-xpack-plugin/schema/xpack_root.json';
|
|
import monitoringRootTelemetrySchema from '@kbn/telemetry-collection-xpack-plugin/schema/xpack_monitoring.json';
|
|
import ossPluginsTelemetrySchema from '@kbn/telemetry-plugin/schema/oss_plugins.json';
|
|
import xpackPluginsTelemetrySchema from '@kbn/telemetry-collection-xpack-plugin/schema/xpack_plugins.json';
|
|
import type { UnencryptedTelemetryPayload } from '@kbn/telemetry-plugin/common/types';
|
|
import type {
|
|
UsageStatsPayload,
|
|
CacheDetails,
|
|
} from '@kbn/telemetry-collection-manager-plugin/server/types';
|
|
import { assertTelemetryPayload } from '@kbn/telemetry-tools';
|
|
import {
|
|
ELASTIC_HTTP_VERSION_HEADER,
|
|
X_ELASTIC_INTERNAL_ORIGIN_REQUEST,
|
|
} from '@kbn/core-http-common';
|
|
import basicClusterFixture from './fixtures/basiccluster.json';
|
|
import multiClusterFixture from './fixtures/multicluster.json';
|
|
import type { SecurityService } from '../../../../../test/common/services/security/security';
|
|
import type { FtrProviderContext } from '../../ftr_provider_context';
|
|
|
|
function omitCacheDetails(usagePayload: Array<Record<string, unknown>>) {
|
|
return usagePayload.map(({ cacheDetails, ...item }) => item);
|
|
}
|
|
|
|
function updateFixtureTimestamps(fixture: Array<Record<string, unknown>>, timestamp: string) {
|
|
return fixture.map((item) => ({ ...item, timestamp }));
|
|
}
|
|
|
|
function getCacheDetails(body: UnencryptedTelemetryPayload): CacheDetails[] {
|
|
return body.map(({ stats }) => (stats as UsageStatsPayload).cacheDetails);
|
|
}
|
|
|
|
/**
|
|
* Update the .monitoring-* documents loaded via the archiver to the recent `timestamp`
|
|
* @param esSupertest The client to send requests to ES
|
|
* @param fromTimestamp The lower timestamp limit to query the documents from
|
|
* @param toTimestamp The upper timestamp limit to query the documents from
|
|
* @param timestamp The new timestamp to be set
|
|
*/
|
|
function updateMonitoringDates(
|
|
esSupertest: SuperTest.SuperTest<SuperTest.Test>,
|
|
fromTimestamp: string,
|
|
toTimestamp: string,
|
|
timestamp: string
|
|
) {
|
|
return esSupertest
|
|
.post('/.monitoring-*/_update_by_query?refresh=true')
|
|
.send({
|
|
query: {
|
|
range: {
|
|
timestamp: {
|
|
format: 'epoch_millis',
|
|
gte: moment(fromTimestamp).valueOf(),
|
|
lte: moment(toTimestamp).valueOf(),
|
|
},
|
|
},
|
|
},
|
|
script: {
|
|
source: `ctx._source.timestamp='${timestamp}'`,
|
|
lang: 'painless',
|
|
},
|
|
})
|
|
.expect(200);
|
|
}
|
|
|
|
async function createUserWithRole(
|
|
security: SecurityService,
|
|
userName: string,
|
|
roleName: string,
|
|
role: unknown
|
|
) {
|
|
await security.role.create(roleName, role);
|
|
|
|
await security.user.create(userName, {
|
|
password: password(userName),
|
|
roles: [roleName],
|
|
full_name: `User ${userName}`,
|
|
});
|
|
}
|
|
|
|
function password(userName: string) {
|
|
return `${userName}-password`;
|
|
}
|
|
|
|
export default function ({ getService }: FtrProviderContext) {
|
|
const supertest = getService('supertest');
|
|
const supertestWithoutAuth = getService('supertestWithoutAuth'); // We need this because `.auth` in the already authed one does not work as expected
|
|
const esArchiver = getService('esArchiver');
|
|
const esSupertest = getService('esSupertest');
|
|
const security = getService('security');
|
|
|
|
describe('/internal/telemetry/clusters/_stats', () => {
|
|
const timestamp = new Date().toISOString();
|
|
describe('monitoring/multicluster', () => {
|
|
let localXPack: Record<string, unknown>;
|
|
let monitoring: Array<Record<string, unknown>>;
|
|
|
|
const archive = 'x-pack/test/functional/es_archives/monitoring/multicluster';
|
|
const fromTimestamp = '2017-08-15T21:00:00.000Z';
|
|
const toTimestamp = '2017-08-16T00:00:00.000Z';
|
|
|
|
before(async () => {
|
|
await esArchiver.load(archive);
|
|
await updateMonitoringDates(esSupertest, fromTimestamp, toTimestamp, timestamp);
|
|
|
|
const { body }: { body: UnencryptedTelemetryPayload } = await supertest
|
|
.post('/internal/telemetry/clusters/_stats')
|
|
.set('kbn-xsrf', 'xxx')
|
|
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
|
|
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
|
|
.send({ unencrypted: true, refreshCache: true })
|
|
.expect(200);
|
|
|
|
expect(body.length).to.be.greaterThan(1);
|
|
const telemetryStats = body.map(({ stats }) => stats);
|
|
localXPack = telemetryStats.shift() as Record<string, unknown>;
|
|
monitoring = telemetryStats as Array<Record<string, unknown>>;
|
|
});
|
|
after(() => esArchiver.unload(archive));
|
|
|
|
it('should pass the schema validations', () => {
|
|
const root = deepmerge(ossRootTelemetrySchema, xpackRootTelemetrySchema);
|
|
|
|
// Merging root to monitoring because `kibana` may be passed in some cases for old collection methods reporting to a newer monitoring cluster
|
|
const monitoringRoot = deepmerge(
|
|
root,
|
|
// It's nested because of the way it's collected and declared
|
|
monitoringRootTelemetrySchema.properties.monitoringTelemetry.properties.stats.items
|
|
);
|
|
const plugins = deepmerge(ossPluginsTelemetrySchema, xpackPluginsTelemetrySchema);
|
|
|
|
try {
|
|
assertTelemetryPayload({ root, plugins }, localXPack);
|
|
monitoring.forEach((stats) => {
|
|
assertTelemetryPayload({ root: monitoringRoot, plugins }, stats);
|
|
});
|
|
} catch (err) {
|
|
err.message = `The telemetry schemas in 'x-pack/plugins/telemetry_collection_xpack/schema/' are out-of-date, please update it as required: ${err.message}`;
|
|
throw err;
|
|
}
|
|
});
|
|
|
|
it('should load multiple trial-license clusters', async () => {
|
|
expect(monitoring).length(3);
|
|
expect(localXPack.collectionSource).to.eql('local_xpack');
|
|
|
|
expect(omitCacheDetails(monitoring)).to.eql(
|
|
updateFixtureTimestamps(multiClusterFixture, timestamp)
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('with basic cluster and reporting and canvas usage info', () => {
|
|
const archive = 'x-pack/test/functional/es_archives/monitoring/basic_6.3.x';
|
|
const fromTimestamp = '2018-07-23T22:54:59.087Z';
|
|
const toTimestamp = '2018-07-23T22:55:05.933Z';
|
|
before(async () => {
|
|
await esArchiver.load(archive);
|
|
await updateMonitoringDates(esSupertest, fromTimestamp, toTimestamp, timestamp);
|
|
});
|
|
after(() => esArchiver.unload(archive));
|
|
it('should load non-expiring basic cluster', async () => {
|
|
const { body }: { body: UnencryptedTelemetryPayload } = await supertest
|
|
.post('/internal/telemetry/clusters/_stats')
|
|
.set('kbn-xsrf', 'xxx')
|
|
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
|
|
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
|
|
.send({ unencrypted: true, refreshCache: true })
|
|
.expect(200);
|
|
|
|
expect(body).length(2);
|
|
const telemetryStats = body.map(({ stats }) => stats);
|
|
|
|
const [localXPack, ...monitoring] = telemetryStats as Array<Record<string, unknown>>;
|
|
expect(localXPack.collectionSource).to.eql('local_xpack');
|
|
expect(omitCacheDetails(monitoring)).to.eql(
|
|
updateFixtureTimestamps(basicClusterFixture, timestamp)
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Telemetry caching', () => {
|
|
const archive = 'x-pack/test/functional/es_archives/monitoring/basic_6.3.x';
|
|
const fromTimestamp = '2018-07-23T22:54:59.087Z';
|
|
const toTimestamp = '2018-07-23T22:55:05.933Z';
|
|
|
|
before(async () => {
|
|
await esArchiver.load(archive);
|
|
await updateMonitoringDates(esSupertest, fromTimestamp, toTimestamp, timestamp);
|
|
// hit the endpoint to cache results
|
|
await supertest
|
|
.post('/internal/telemetry/clusters/_stats')
|
|
.set('kbn-xsrf', 'xxx')
|
|
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
|
|
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
|
|
.send({ unencrypted: true, refreshCache: true })
|
|
.expect(200);
|
|
});
|
|
after(() => esArchiver.unload(archive));
|
|
});
|
|
|
|
it('returns non-cached results when unencrypted', async () => {
|
|
const now = Date.now();
|
|
const { body }: { body: UnencryptedTelemetryPayload } = await supertest
|
|
.post('/internal/telemetry/clusters/_stats')
|
|
.set('kbn-xsrf', 'xxx')
|
|
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
|
|
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
|
|
.send({ unencrypted: true })
|
|
.expect(200);
|
|
|
|
expect(body).length(1);
|
|
|
|
const cacheDetails = getCacheDetails(body);
|
|
cacheDetails.forEach(({ fetchedAt, updatedAt }) => {
|
|
// Check that the cache is fresh by comparing updatedAt timestamp with
|
|
// the timestamp the data was fetched.
|
|
expect(new Date(updatedAt).getTime()).to.be.greaterThan(now);
|
|
// Check that the fetchedAt timestamp is updated when the data is fetched
|
|
expect(new Date(fetchedAt).getTime()).to.be.greaterThan(now);
|
|
});
|
|
});
|
|
|
|
it('grabs a fresh copy on refresh', async () => {
|
|
const now = Date.now();
|
|
const { body }: { body: UnencryptedTelemetryPayload } = await supertest
|
|
.post('/internal/telemetry/clusters/_stats')
|
|
.set('kbn-xsrf', 'xxx')
|
|
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
|
|
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
|
|
.send({ unencrypted: true, refreshCache: true })
|
|
.expect(200);
|
|
|
|
expect(body).length(1);
|
|
getCacheDetails(body).forEach(({ updatedAt, fetchedAt }) => {
|
|
// Check that the cache is fresh by comparing updatedAt timestamp with
|
|
// the timestamp the data was fetched.
|
|
expect(new Date(updatedAt).getTime()).to.be.greaterThan(now);
|
|
// Check that the fetchedAt timestamp is updated when the data is fetched
|
|
expect(new Date(fetchedAt).getTime()).to.be.greaterThan(now);
|
|
});
|
|
});
|
|
|
|
describe('Only global read+ users can fetch unencrypted telemetry', () => {
|
|
describe('superadmin user', () => {
|
|
it('should return unencrypted telemetry for the admin user', async () => {
|
|
await supertest
|
|
.post('/internal/telemetry/clusters/_stats')
|
|
.set('kbn-xsrf', 'xxx')
|
|
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
|
|
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
|
|
.send({ unencrypted: true })
|
|
.expect(200);
|
|
});
|
|
|
|
it('should return encrypted telemetry for the admin user', async () => {
|
|
await supertest
|
|
.post('/internal/telemetry/clusters/_stats')
|
|
.set('kbn-xsrf', 'xxx')
|
|
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
|
|
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
|
|
.send({ unencrypted: false })
|
|
.expect(200);
|
|
});
|
|
});
|
|
|
|
describe('global-read user', () => {
|
|
const globalReadOnlyUser = 'telemetry-global-read-only-user';
|
|
const globalReadOnlyRole = 'telemetry-global-read-only-role';
|
|
|
|
before('create user', async () => {
|
|
await createUserWithRole(security, globalReadOnlyUser, globalReadOnlyRole, {
|
|
kibana: [
|
|
{
|
|
spaces: ['*'],
|
|
base: ['read'],
|
|
feature: {},
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
after(async () => {
|
|
await security.user.delete(globalReadOnlyUser);
|
|
await security.role.delete(globalReadOnlyRole);
|
|
});
|
|
|
|
it('should return encrypted telemetry for the global-read user', async () => {
|
|
await supertestWithoutAuth
|
|
.post('/internal/telemetry/clusters/_stats')
|
|
.auth(globalReadOnlyUser, password(globalReadOnlyUser))
|
|
.set('kbn-xsrf', 'xxx')
|
|
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
|
|
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
|
|
.send({ unencrypted: false })
|
|
.expect(200);
|
|
});
|
|
|
|
it('should return unencrypted telemetry for the global-read user', async () => {
|
|
await supertestWithoutAuth
|
|
.post('/internal/telemetry/clusters/_stats')
|
|
.auth(globalReadOnlyUser, password(globalReadOnlyUser))
|
|
.set('kbn-xsrf', 'xxx')
|
|
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
|
|
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
|
|
.send({ unencrypted: true })
|
|
.expect(200);
|
|
});
|
|
});
|
|
|
|
describe('non global-read user', () => {
|
|
const noGlobalUser = 'telemetry-no-global-user';
|
|
const noGlobalRole = 'telemetry-no-global-role';
|
|
|
|
before('create user', async () => {
|
|
await createUserWithRole(security, noGlobalUser, noGlobalRole, {
|
|
kibana: [
|
|
{
|
|
spaces: ['*'],
|
|
base: [],
|
|
feature: {
|
|
// It has access to many features specified individually but not a global one
|
|
discover: ['all'],
|
|
dashboard: ['all'],
|
|
canvas: ['all'],
|
|
maps: ['all'],
|
|
ml: ['all'],
|
|
visualize: ['all'],
|
|
dev_tools: ['all'],
|
|
},
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
after(async () => {
|
|
await security.user.delete(noGlobalUser);
|
|
await security.role.delete(noGlobalRole);
|
|
});
|
|
|
|
it('should return encrypted telemetry for the read-only user', async () => {
|
|
await supertestWithoutAuth
|
|
.post('/internal/telemetry/clusters/_stats')
|
|
.auth(noGlobalUser, password(noGlobalUser))
|
|
.set('kbn-xsrf', 'xxx')
|
|
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
|
|
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
|
|
.send({ unencrypted: false })
|
|
.expect(200);
|
|
});
|
|
|
|
it('should return 403 when the read-only user requests unencrypted telemetry', async () => {
|
|
await supertestWithoutAuth
|
|
.post('/internal/telemetry/clusters/_stats')
|
|
.auth(noGlobalUser, password(noGlobalUser))
|
|
.set('kbn-xsrf', 'xxx')
|
|
.set(ELASTIC_HTTP_VERSION_HEADER, '2')
|
|
.set(X_ELASTIC_INTERNAL_ORIGIN_REQUEST, 'kibana')
|
|
.send({ unencrypted: true })
|
|
.expect(403);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
}
|